import * as plugins from '../plugins.js'; import * as appstate from '../appstate.js'; import * as interfaces from '../../ts_interfaces/index.js'; import { appRouter } from '../router.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; import type { SgViewDashboard } from './sg-view-dashboard.js'; import type { SgViewOrganizations } from './sg-view-organizations.js'; import type { SgViewPackages } from './sg-view-packages.js'; import type { SgViewTokens } from './sg-view-tokens.js'; import type { SgViewSettings } from './sg-view-settings.js'; import type { SgViewAdmin } from './sg-view-admin.js'; @customElement('sg-app-shell') export class SgAppShell extends DeesElement { @state() accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false }; @state() accessor uiState: appstate.IUiState = { activeView: 'dashboard' }; @state() accessor loginLoading: boolean = false; @state() accessor loginError: string = ''; @state() accessor authProviders: interfaces.data.IPublicAuthProvider[] = []; @state() accessor localAuthEnabled: boolean = true; private viewTabs = [ { name: 'Dashboard', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./sg-view-dashboard.js')).SgViewDashboard)(), }, { name: 'Organizations', iconName: 'lucide:building2', element: (async () => (await import('./sg-view-organizations.js')).SgViewOrganizations)(), }, { name: 'Packages', iconName: 'lucide:package', element: (async () => (await import('./sg-view-packages.js')).SgViewPackages)(), }, { name: 'Tokens', iconName: 'lucide:key', element: (async () => (await import('./sg-view-tokens.js')).SgViewTokens)(), }, { name: 'Settings', iconName: 'lucide:settings', element: (async () => (await import('./sg-view-settings.js')).SgViewSettings)(), }, { name: 'Admin', iconName: 'lucide:shield', element: (async () => (await import('./sg-view-admin.js')).SgViewAdmin)(), }, ]; private allResolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = []; private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = []; constructor() { super(); document.title = 'Stack.Gallery Registry'; const loginSubscription = appstate.loginStatePart .select((s) => s) .subscribe((loginState) => { this.loginState = loginState; // Re-filter tabs when login state changes if (loginState.isLoggedIn && this.allResolvedViewTabs.length > 0) { this.resolvedViewTabs = loginState.identity?.isSystemAdmin ? this.allResolvedViewTabs : this.allResolvedViewTabs.filter((t) => t.name !== 'Admin'); } }); this.rxSubscriptions.push(loginSubscription); const uiSubscription = appstate.uiStatePart .select((s) => s) .subscribe((uiState) => { this.uiState = uiState; this.syncAppdashView(uiState.activeView); }); this.rxSubscriptions.push(uiSubscription); } public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; } .maincontainer { width: 100%; height: 100vh; } `, ]; public render(): TemplateResult { if (!this.loginState.isLoggedIn) { return html`
this.handleLocalLogin(e)} @oauth-login=${(e: CustomEvent) => this.handleOAuthLogin(e)} @ldap-login=${(e: CustomEvent) => this.handleLdapLogin(e)} >
`; } return html`
t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView, ) || this.resolvedViewTabs[0]} >
`; } public async firstUpdated() { // Fetch auth providers for login page this.fetchAuthProviders(); // Resolve async view tab imports const allTabs = await Promise.all( this.viewTabs.map(async (tab) => ({ name: tab.name, iconName: tab.iconName, element: await tab.element, })), ); // Filter admin tab based on user role this.resolvedViewTabs = this.loginState.identity?.isSystemAdmin ? allTabs : allTabs.filter((t) => t.name !== 'Admin'); this.requestUpdate(); await this.updateComplete; const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any; if (appDash) { appDash.addEventListener('view-select', (e: CustomEvent) => { const viewName = e.detail.view.name.toLowerCase().replace(/\s+/g, '-'); appRouter.navigateToView(viewName); }); appDash.addEventListener('logout', async () => { await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); }); // Load initial view if (this.resolvedViewTabs.length > 0) { const currentActiveView = appstate.uiStatePart.getState().activeView; const initialView = this.resolvedViewTabs.find( (t) => t.name.toLowerCase().replace(/\s+/g, '-') === currentActiveView, ) || this.resolvedViewTabs[0]; await appDash.loadView(initialView); } } // Check for stored session const loginState = appstate.loginStatePart.getState(); if (loginState.identity?.jwt) { if (loginState.identity.expiresAt > Date.now()) { // Validate token with server in the background try { await appstate.settingsStatePart.dispatchAction(appstate.fetchMeAction, null); } catch { console.warn('Stored session invalid, returning to login'); await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } else { // Token expired, try refresh const newState = await appstate.loginStatePart.dispatchAction( appstate.refreshTokenAction, null, ); if (!newState.isLoggedIn) { // Refresh failed await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null); } } } } private async fetchAuthProviders() { try { const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetAuthProviders >('/typedrequest', 'getAuthProviders'); const response = await typedRequest.fire({}); this.authProviders = response.providers; this.localAuthEnabled = response.localAuthEnabled; } catch { // Default to local auth if we can't fetch providers this.localAuthEnabled = true; } } private async handleLocalLogin(e: CustomEvent) { const { email, password } = e.detail; this.loginLoading = true; this.loginError = ''; try { const newState = await appstate.loginStatePart.dispatchAction(appstate.loginAction, { email, password, }); if (!newState.isLoggedIn) { this.loginError = 'Invalid email or password'; } } catch { this.loginError = 'Login failed. Please try again.'; } this.loginLoading = false; } private async handleOAuthLogin(e: CustomEvent) { const { providerId } = e.detail; try { const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_OAuthAuthorize >('/typedrequest', 'oauthAuthorize'); const response = await typedRequest.fire({ providerId }); // Redirect to OAuth provider window.location.href = response.redirectUrl; } catch { this.loginError = 'OAuth login failed. Please try again.'; } } private async handleLdapLogin(e: CustomEvent) { const { providerId, username, password } = e.detail; this.loginLoading = true; this.loginError = ''; try { const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_LdapLogin >('/typedrequest', 'ldapLogin'); const response = await typedRequest.fire({ providerId, username, password }); if (response.identity) { appstate.loginStatePart.setState({ identity: response.identity, isLoggedIn: true, }); } else { this.loginError = response.errorMessage || 'LDAP login failed'; } } catch { this.loginError = 'LDAP login failed. Please try again.'; } this.loginLoading = false; } private syncAppdashView(viewName: string): void { const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any; if (!appDash || this.resolvedViewTabs.length === 0) return; const targetTab = this.resolvedViewTabs.find( (t) => t.name.toLowerCase().replace(/\s+/g, '-') === viewName, ); if (!targetTab) return; appDash.loadView(targetTab); } }