294 lines
9.3 KiB
TypeScript
294 lines
9.3 KiB
TypeScript
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`
|
|
<div class="maincontainer">
|
|
<sg-login-view
|
|
.providers=${this.authProviders}
|
|
.localAuthEnabled=${this.localAuthEnabled}
|
|
.loading=${this.loginLoading}
|
|
.error=${this.loginError}
|
|
@login=${(e: CustomEvent) => this.handleLocalLogin(e)}
|
|
@oauth-login=${(e: CustomEvent) => this.handleOAuthLogin(e)}
|
|
@ldap-login=${(e: CustomEvent) => this.handleLdapLogin(e)}
|
|
></sg-login-view>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html`
|
|
<div class="maincontainer">
|
|
<dees-simple-appdash
|
|
name="Stack.Gallery"
|
|
.viewTabs=${this.resolvedViewTabs}
|
|
.selectedView=${this.resolvedViewTabs.find(
|
|
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView,
|
|
) || this.resolvedViewTabs[0]}
|
|
>
|
|
</dees-simple-appdash>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
public async firstUpdated() {
|
|
// Fetch auth providers for login page
|
|
this.fetchAuthProviders();
|
|
|
|
// Resolve async view tab imports
|
|
this.allResolvedViewTabs = 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
|
|
? this.allResolvedViewTabs
|
|
: this.allResolvedViewTabs.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);
|
|
}
|
|
}
|