feat(app): wire dashboard administration flows
This commit is contained in:
+793
-133
@@ -1,27 +1,19 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as states from '../../states/accountstate.js';
|
||||
import { IdpState } from '../../states/idp.state.js';
|
||||
import { BulkInviteModal } from './bulk-invite-modal.js';
|
||||
import { CreateOrgModal } from './create-org-modal.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
css,
|
||||
state,
|
||||
type TemplateResult
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { LeleAccountNavigation } from './navigation.js';
|
||||
import { OrgSelectModal, type IOrgSelectResult } from './org-select-modal.js';
|
||||
import { CreateOrgModal } from './create-org-modal.js';
|
||||
import { accountDesignTokens } from './sharedstyles.js';
|
||||
|
||||
import * as views from './views/index.js';
|
||||
import * as accountstate from '../../states/accountstate.js';
|
||||
|
||||
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
|
||||
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'idp-accountcontent': IdpAccountContent;
|
||||
@@ -32,6 +24,61 @@ declare global {
|
||||
export class IdpAccountContent extends DeesElement {
|
||||
|
||||
public subrouter: plugins.deesDomtools.plugins.smartrouter.SmartRouter;
|
||||
private dataLoadRun = 0;
|
||||
|
||||
@state()
|
||||
private accessor adminPage: plugins.idpCatalog.IdpAdminShell['page'] = 'overview';
|
||||
|
||||
@state()
|
||||
private accessor adminUser: plugins.idpCatalog.IIdpAdminUser = {
|
||||
name: 'Loading account',
|
||||
email: '',
|
||||
};
|
||||
|
||||
@state()
|
||||
private accessor adminOrgs: plugins.idpCatalog.IIdpAdminOrg[] = [];
|
||||
|
||||
@state()
|
||||
private accessor selectedOrgId = '';
|
||||
|
||||
@state()
|
||||
private accessor globalAdmin = false;
|
||||
|
||||
@state()
|
||||
private accessor dataLoading = false;
|
||||
|
||||
@state()
|
||||
private accessor dataError = '';
|
||||
|
||||
@state()
|
||||
private accessor sessions: plugins.idpCatalog.IIdpAdminSession[] = [];
|
||||
|
||||
@state()
|
||||
private accessor activities: plugins.idpCatalog.IIdpAdminActivity[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgMembers: plugins.idpCatalog.IIdpAdminMember[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgInvitations: plugins.idpCatalog.IIdpAdminInvitation[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgRoleDefinitions: plugins.idpCatalog.IIdpAdminOrgRoleDefinition[] = [];
|
||||
|
||||
@state()
|
||||
private accessor orgApps: plugins.idpCatalog.IIdpAdminApp[] = [];
|
||||
|
||||
@state()
|
||||
private accessor adminApps: plugins.idpCatalog.IIdpAdminApp[] = [];
|
||||
|
||||
@state()
|
||||
private accessor passportDevices: plugins.idpCatalog.IIdpAdminPassportDevice[] = [];
|
||||
|
||||
@state()
|
||||
private accessor passportEnrollment: plugins.idpCatalog.IIdpAdminPassportEnrollment | null = null;
|
||||
|
||||
@state()
|
||||
private accessor credentialMessage = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -39,169 +86,782 @@ export class IdpAccountContent extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
background: var(--background);
|
||||
overflow: hidden;
|
||||
background: var(--idp-bg, hsl(240 10% 3.9%));
|
||||
}
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
.main {
|
||||
position: absolute;
|
||||
idp-admin-shell {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
lele-accountnavigation {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
height: 100vh;
|
||||
width: 200px;
|
||||
}
|
||||
.viewcontainer {
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
width: calc(100vw - 200px);
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.viewcontainer.changing {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<div class="main">
|
||||
<lele-accountnavigation></lele-accountnavigation>
|
||||
<div class="viewcontainer">
|
||||
<!--<lele-accountview-subscription></lele-accountview-subscription>-->
|
||||
</div>
|
||||
</div>
|
||||
<idp-admin-shell
|
||||
.page=${this.adminPage}
|
||||
.user=${this.adminUser}
|
||||
.orgs=${this.adminOrgs}
|
||||
.selectedOrgId=${this.selectedOrgId}
|
||||
.globalAdmin=${this.globalAdmin}
|
||||
.dataLoading=${this.dataLoading}
|
||||
.dataError=${this.dataError}
|
||||
.sessions=${this.sessions}
|
||||
.activities=${this.activities}
|
||||
.orgMembers=${this.orgMembers}
|
||||
.orgInvitations=${this.orgInvitations}
|
||||
.orgRoleDefinitions=${this.orgRoleDefinitions}
|
||||
.orgApps=${this.orgApps}
|
||||
.adminApps=${this.adminApps}
|
||||
.passportDevices=${this.passportDevices}
|
||||
.passportEnrollment=${this.passportEnrollment}
|
||||
.credentialMessage=${this.credentialMessage}
|
||||
@idp-admin-navigate=${this.handleAdminNavigate}
|
||||
@idp-admin-org-select=${this.handleOrgSelect}
|
||||
@idp-admin-org-create=${this.handleOrgCreate}
|
||||
@idp-admin-org-update=${this.handleOrgUpdate}
|
||||
@idp-admin-org-transfer=${this.handleOrgTransfer}
|
||||
@idp-admin-org-delete=${this.handleOrgDelete}
|
||||
@idp-admin-session-revoke=${this.handleSessionRevoke}
|
||||
@idp-admin-app-toggle=${this.handleAppToggle}
|
||||
@idp-admin-password-change=${this.handlePasswordChange}
|
||||
@idp-admin-passport-enroll=${this.handlePassportEnroll}
|
||||
@idp-admin-passport-revoke=${this.handlePassportRevoke}
|
||||
@idp-admin-member-invite=${this.handleMemberInvite}
|
||||
@idp-admin-member-remove=${this.handleMemberRemove}
|
||||
@idp-admin-member-roles-update=${this.handleMemberRolesUpdate}
|
||||
@idp-admin-invitation-resend=${this.handleInvitationResend}
|
||||
@idp-admin-invitation-cancel=${this.handleInvitationCancel}
|
||||
@idp-admin-org-role-upsert=${this.handleOrgRoleUpsert}
|
||||
@idp-admin-org-role-delete=${this.handleOrgRoleDelete}
|
||||
@idp-admin-app-role-mappings-update=${this.handleAppRoleMappingsUpdate}
|
||||
></idp-admin-shell>
|
||||
`;
|
||||
}
|
||||
|
||||
private setAdminPage(pageArg: plugins.idpCatalog.IdpAdminShell['page']) {
|
||||
this.adminPage = pageArg;
|
||||
if (this.subrouter) {
|
||||
void this.loadAdminShellData();
|
||||
}
|
||||
}
|
||||
|
||||
private getSelectedOrgSlug(): string {
|
||||
const currentState = states.accountState.getState();
|
||||
const selectedOrg = currentState.selectedOrg
|
||||
|| currentState.organizations.find((orgArg) => orgArg.id === this.selectedOrgId)
|
||||
|| currentState.organizations[0];
|
||||
return selectedOrg?.data?.slug || this.adminOrgs.find((orgArg) => orgArg.id === this.selectedOrgId)?.slug || this.adminOrgs[0]?.slug || '';
|
||||
}
|
||||
|
||||
private getPathForPage(pageArg: plugins.idpCatalog.IdpAdminShell['page']): string | null {
|
||||
const orgSlug = this.getSelectedOrgSlug();
|
||||
const orgPath = (suffixArg = '') => orgSlug ? `/org/${orgSlug}${suffixArg}` : null;
|
||||
|
||||
const pageMap: Record<plugins.idpCatalog.IdpAdminShell['page'], string | null> = {
|
||||
overview: '/overview',
|
||||
profile: '/account/profile',
|
||||
security: '/account/security',
|
||||
sessions: '/account/sessions',
|
||||
apps: '/account/apps',
|
||||
'org-general': orgPath(),
|
||||
'org-settings': orgPath('/settings'),
|
||||
'org-members': orgPath('/users'),
|
||||
'org-apps': orgPath('/apps'),
|
||||
support: '/support',
|
||||
'ga-users': '/admin/users',
|
||||
'ga-orgs': '/admin/orgs',
|
||||
'ga-apps': '/admin/apps',
|
||||
};
|
||||
|
||||
return pageMap[pageArg];
|
||||
}
|
||||
|
||||
private pushDashPath(pathArg: string) {
|
||||
const normalizedPath = pathArg || '';
|
||||
const absolutePath = `/dash${normalizedPath}`.replace(/\/$/, '') || '/dash';
|
||||
if (window.location.pathname.replace(/\/$/, '') === absolutePath) {
|
||||
return;
|
||||
}
|
||||
this.subrouter.pushUrl(normalizedPath);
|
||||
}
|
||||
|
||||
private async handleAdminNavigate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminNavigateEventDetail>) {
|
||||
const page = eventArg.detail.page;
|
||||
this.setAdminPage(page);
|
||||
const path = this.getPathForPage(page);
|
||||
if (path !== null) {
|
||||
this.pushDashPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOrgSelect(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgSelectEventDetail>) {
|
||||
const currentState = states.accountState.getState();
|
||||
const selectedOrg = currentState.organizations.find((orgArg) => orgArg.id === eventArg.detail.orgId)
|
||||
|| currentState.organizations.find((orgArg) => orgArg.data.slug === eventArg.detail.org?.slug);
|
||||
|
||||
this.selectedOrgId = eventArg.detail.orgId;
|
||||
this.setAdminPage('org-general');
|
||||
|
||||
if (selectedOrg) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, selectedOrg);
|
||||
this.pushDashPath(`/org/${selectedOrg.data.slug}`);
|
||||
} else if (eventArg.detail.org?.slug) {
|
||||
this.pushDashPath(`/org/${eventArg.detail.org.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOrgCreate() {
|
||||
const org = await CreateOrgModal.show();
|
||||
if (!org) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyAccountState();
|
||||
this.selectedOrgId = org.id;
|
||||
this.setAdminPage('org-general');
|
||||
this.pushDashPath(`/org/${org.data.slug}`);
|
||||
}
|
||||
|
||||
private async handleOrgUpdate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgUpdateEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateOrganization>('updateOrganization');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
name: eventArg.detail.name,
|
||||
slug: eventArg.detail.slug,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization update failed.');
|
||||
}
|
||||
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
const refreshedOrg = states.accountState.getState().organizations.find((orgArg) => orgArg.id === response.organization.id) || response.organization;
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, refreshedOrg);
|
||||
this.applyAccountState();
|
||||
this.selectedOrgId = refreshedOrg.id;
|
||||
this.setAdminPage('org-settings');
|
||||
this.pushDashPath(`/org/${refreshedOrg.data.slug}/settings`);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgTransfer(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgTransferEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_TransferOwnership>('transferOwnership');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
newOwnerId: eventArg.detail.newOwnerId,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Ownership transfer failed.');
|
||||
}
|
||||
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
const refreshedOrg = states.accountState.getState().organizations.find((orgArg) => orgArg.id === eventArg.detail.organizationId);
|
||||
if (refreshedOrg) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, refreshedOrg);
|
||||
this.selectedOrgId = refreshedOrg.id;
|
||||
}
|
||||
this.applyAccountState();
|
||||
this.setAdminPage('org-settings');
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgDelete(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgDeleteEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteOrganization>('deleteOrganization');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization deletion failed.');
|
||||
}
|
||||
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
const nextOrg = states.accountState.getState().organizations[0] || null;
|
||||
if (nextOrg) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, nextOrg);
|
||||
} else {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, null as any);
|
||||
}
|
||||
this.selectedOrgId = nextOrg?.id || '';
|
||||
this.applyAccountState();
|
||||
this.setAdminPage('overview');
|
||||
this.pushDashPath('/overview');
|
||||
});
|
||||
}
|
||||
|
||||
private async syncSelectedOrgFromPath() {
|
||||
const orgSlug = window.location.pathname.match(/^\/dash\/org\/([^/]+)/)?.[1];
|
||||
if (!orgSlug) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = states.accountState.getState();
|
||||
const selectedOrg = currentState.organizations.find((orgArg) => orgArg.data.slug === orgSlug);
|
||||
if (!selectedOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedOrgId = selectedOrg.id;
|
||||
if (currentState.selectedOrg?.id !== selectedOrg.id) {
|
||||
await states.accountState.dispatchAction(states.setSelectedOrg, selectedOrg);
|
||||
}
|
||||
}
|
||||
|
||||
private applyAccountState() {
|
||||
const currentState = states.accountState.getState();
|
||||
const user = currentState.user;
|
||||
|
||||
if (user) {
|
||||
this.adminUser = {
|
||||
name: user.data.name || user.data.username || user.data.email,
|
||||
email: user.data.email,
|
||||
username: user.data.username,
|
||||
mobileNumber: user.data.mobileNumber,
|
||||
status: user.data.status,
|
||||
};
|
||||
this.globalAdmin = Boolean(user.data.isGlobalAdmin);
|
||||
}
|
||||
|
||||
this.adminOrgs = currentState.organizations.map((orgArg) => {
|
||||
const role = currentState.roles.find((roleArg) => roleArg.data.organizationId === orgArg.id);
|
||||
return {
|
||||
id: orgArg.id,
|
||||
name: orgArg.data.name,
|
||||
slug: orgArg.data.slug,
|
||||
myRole: role?.data.roles?.[0] || 'member',
|
||||
};
|
||||
});
|
||||
|
||||
this.selectedOrgId = currentState.selectedOrg?.id || this.selectedOrgId || currentState.organizations[0]?.id || '';
|
||||
const selectedOrg = currentState.organizations.find((orgArg) => orgArg.id === this.selectedOrgId) || currentState.selectedOrg || currentState.organizations[0];
|
||||
this.orgRoleDefinitions = selectedOrg?.data.roleDefinitions || [];
|
||||
}
|
||||
|
||||
private async setOrgPage(pageArg: plugins.idpCatalog.IdpAdminShell['page']) {
|
||||
await this.syncSelectedOrgFromPath();
|
||||
this.setAdminPage(pageArg);
|
||||
}
|
||||
|
||||
private getSelectedOrganization(): plugins.idpInterfaces.data.IOrganization | null {
|
||||
const currentState = states.accountState.getState();
|
||||
return currentState.selectedOrg
|
||||
|| currentState.organizations.find((orgArg) => orgArg.id === this.selectedOrgId)
|
||||
|| currentState.organizations[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
private async loadSessions(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminSession[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>('getUserSessions');
|
||||
const response = await request.fire({ jwt: jwtArg });
|
||||
return (response.sessions || []).map((sessionArg) => ({
|
||||
id: sessionArg.id,
|
||||
deviceName: sessionArg.deviceName,
|
||||
browser: sessionArg.browser,
|
||||
os: sessionArg.os,
|
||||
ip: sessionArg.ip,
|
||||
lastActive: sessionArg.lastActive,
|
||||
createdAt: sessionArg.createdAt,
|
||||
isCurrent: sessionArg.isCurrent,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadActivities(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminActivity[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserActivity>('getUserActivity');
|
||||
const response = await request.fire({ jwt: jwtArg, limit: 20 });
|
||||
return (response.activities || []).map((activityArg) => ({
|
||||
id: activityArg.id,
|
||||
action: activityArg.data.action,
|
||||
description: activityArg.data.metadata.description,
|
||||
timestamp: activityArg.data.timestamp,
|
||||
ip: activityArg.data.metadata.ip,
|
||||
targetType: activityArg.data.metadata.targetType,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadOrgMembers(idpStateArg: IdpState, jwtArg: string, organizationIdArg: string): Promise<plugins.idpCatalog.IIdpAdminMember[]> {
|
||||
const currentState = states.accountState.getState();
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgMembers>('getOrgMembers');
|
||||
const response = await request.fire({ jwt: jwtArg, organizationId: organizationIdArg });
|
||||
return (response.members || []).map((memberArg) => ({
|
||||
userId: memberArg.user.id,
|
||||
name: memberArg.user.data.name || memberArg.user.data.username || memberArg.user.data.email,
|
||||
email: memberArg.user.data.email,
|
||||
roles: memberArg.role.data.roles || [],
|
||||
isCurrentUser: currentState.user?.id === memberArg.user.id,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadOrgInvitations(idpStateArg: IdpState, jwtArg: string, organizationIdArg: string): Promise<plugins.idpCatalog.IIdpAdminInvitation[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgInvitations>('getOrgInvitations');
|
||||
const response = await request.fire({ jwt: jwtArg, organizationId: organizationIdArg });
|
||||
return (response.invitations || []).map((invitationArg) => {
|
||||
const orgRef = invitationArg.data.organizationRefs.find((refArg) => refArg.organizationId === organizationIdArg)
|
||||
|| invitationArg.data.organizationRefs[0];
|
||||
return {
|
||||
id: invitationArg.id,
|
||||
email: invitationArg.data.email,
|
||||
roles: orgRef?.roles || [],
|
||||
invitedAt: orgRef?.invitedAt || invitationArg.data.createdAt,
|
||||
expiresAt: invitationArg.data.expiresAt,
|
||||
status: invitationArg.data.status,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async loadOrgApps(idpStateArg: IdpState, jwtArg: string, organizationIdArg: string): Promise<plugins.idpCatalog.IIdpAdminApp[]> {
|
||||
const appsRequest = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalApps>('getGlobalApps');
|
||||
const connectionsRequest = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>('getAppConnections');
|
||||
const [appsResponse, connectionsResponse] = await Promise.all([
|
||||
appsRequest.fire({ jwt: jwtArg }),
|
||||
connectionsRequest.fire({ jwt: jwtArg, organizationId: organizationIdArg }),
|
||||
]);
|
||||
const activeConnectionMap = new Map((connectionsResponse.connections || [])
|
||||
.filter((connectionArg) => connectionArg.data.status === 'active')
|
||||
.map((connectionArg) => [connectionArg.data.appId, connectionArg]));
|
||||
return (appsResponse.apps || []).map((appArg) => ({
|
||||
id: appArg.id,
|
||||
name: appArg.data.name,
|
||||
description: appArg.data.description,
|
||||
logoUrl: appArg.data.logoUrl,
|
||||
appUrl: appArg.data.appUrl,
|
||||
category: appArg.data.category,
|
||||
type: appArg.type,
|
||||
status: appArg.data.isActive ? 'active' : 'inactive',
|
||||
isConnected: activeConnectionMap.has(appArg.id),
|
||||
roleMappings: activeConnectionMap.get(appArg.id)?.data.roleMappings || [],
|
||||
clientId: appArg.data.oauthCredentials.clientId,
|
||||
scopes: activeConnectionMap.get(appArg.id)?.data.grantedScopes || appArg.data.oauthCredentials.allowedScopes || [],
|
||||
grants: appArg.data.oauthCredentials.grantTypes || [],
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadAdminApps(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminApp[]> {
|
||||
if (!this.globalAdmin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>('getGlobalAppStats');
|
||||
const response = await request.fire({ jwt: jwtArg });
|
||||
return (response.apps || []).map((entryArg) => ({
|
||||
id: entryArg.app.id,
|
||||
name: entryArg.app.data.name,
|
||||
description: entryArg.app.data.description,
|
||||
logoUrl: entryArg.app.data.logoUrl,
|
||||
appUrl: entryArg.app.data.appUrl,
|
||||
category: entryArg.app.data.category,
|
||||
type: entryArg.app.type,
|
||||
status: entryArg.app.data.isActive ? 'active' : 'inactive',
|
||||
connectionCount: entryArg.connectionCount,
|
||||
clientId: entryArg.app.data.oauthCredentials.clientId,
|
||||
scopes: entryArg.app.data.oauthCredentials.allowedScopes || [],
|
||||
grants: entryArg.app.data.oauthCredentials.grantTypes || [],
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadPassportDevices(idpStateArg: IdpState, jwtArg: string): Promise<plugins.idpCatalog.IIdpAdminPassportDevice[]> {
|
||||
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetPassportDevices>('getPassportDevices');
|
||||
const response = await request.fire({ jwt: jwtArg });
|
||||
return (response.devices || []).map((deviceArg) => ({
|
||||
id: deviceArg.id,
|
||||
label: deviceArg.data.label,
|
||||
platform: deviceArg.data.platform,
|
||||
status: deviceArg.data.status,
|
||||
capabilities: deviceArg.data.capabilities,
|
||||
appVersion: deviceArg.data.appVersion,
|
||||
createdAt: deviceArg.data.createdAt,
|
||||
lastSeenAt: deviceArg.data.lastSeenAt,
|
||||
lastChallengeAt: deviceArg.data.lastChallengeAt,
|
||||
pushRegistered: Boolean(deviceArg.data.pushRegistration),
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadAdminShellData() {
|
||||
const currentRun = ++this.dataLoadRun;
|
||||
this.dataLoading = true;
|
||||
this.dataError = '';
|
||||
|
||||
try {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
const orgId = selectedOrg?.id || '';
|
||||
|
||||
const [sessions, activities, members, invitations, orgApps, adminApps, passportDevices] = await Promise.all([
|
||||
this.loadSessions(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading sessions:', error);
|
||||
return this.sessions;
|
||||
}),
|
||||
this.loadActivities(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading activity:', error);
|
||||
return this.activities;
|
||||
}),
|
||||
orgId ? this.loadOrgMembers(idpState, jwt, orgId).catch((error) => {
|
||||
console.error('Error loading org members:', error);
|
||||
return this.orgMembers;
|
||||
}) : Promise.resolve([]),
|
||||
orgId ? this.loadOrgInvitations(idpState, jwt, orgId).catch((error) => {
|
||||
console.error('Error loading org invitations:', error);
|
||||
return this.orgInvitations;
|
||||
}) : Promise.resolve([]),
|
||||
orgId ? this.loadOrgApps(idpState, jwt, orgId).catch((error) => {
|
||||
console.error('Error loading org apps:', error);
|
||||
return this.orgApps;
|
||||
}) : Promise.resolve([]),
|
||||
this.loadAdminApps(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading admin apps:', error);
|
||||
return this.adminApps;
|
||||
}),
|
||||
this.loadPassportDevices(idpState, jwt).catch((error) => {
|
||||
console.error('Error loading passport devices:', error);
|
||||
return this.passportDevices;
|
||||
}),
|
||||
]);
|
||||
|
||||
if (currentRun !== this.dataLoadRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessions = sessions;
|
||||
this.activities = activities;
|
||||
this.orgMembers = members;
|
||||
this.orgInvitations = invitations;
|
||||
this.orgApps = orgApps;
|
||||
this.adminApps = adminApps;
|
||||
this.passportDevices = passportDevices;
|
||||
} catch (error) {
|
||||
console.error('Error loading admin shell data:', error);
|
||||
if (currentRun === this.dataLoadRun) {
|
||||
this.dataError = error instanceof Error ? error.message : 'Failed to load admin console data.';
|
||||
}
|
||||
} finally {
|
||||
if (currentRun === this.dataLoadRun) {
|
||||
this.dataLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async runAdminAction(actionArg: () => Promise<void>) {
|
||||
this.dataError = '';
|
||||
try {
|
||||
await actionArg();
|
||||
await this.loadAdminShellData();
|
||||
} catch (error) {
|
||||
console.error('Admin console action failed:', error);
|
||||
this.dataError = error instanceof Error ? error.message : 'Action failed. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSessionRevoke(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminSessionEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>('revokeSession');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), sessionId: eventArg.detail.sessionId });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleAppToggle(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminAppToggleEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
this.dataError = 'Select an organisation before changing app connections.';
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ToggleAppConnection>('toggleAppConnection');
|
||||
await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: selectedOrg.id,
|
||||
appId: eventArg.detail.appId,
|
||||
action: eventArg.detail.connected ? 'connect' : 'disconnect',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePasswordChange(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminPasswordChangeEventDetail>) {
|
||||
const email = states.accountState.getState().user?.data.email;
|
||||
if (!email) {
|
||||
this.credentialMessage = '';
|
||||
this.dataError = 'Cannot change password before account data is loaded.';
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SetNewPassword>('setNewPassword');
|
||||
const response = await request.fire({
|
||||
email,
|
||||
oldPassword: eventArg.detail.currentPassword,
|
||||
newPassword: eventArg.detail.newPassword,
|
||||
});
|
||||
if (response.status !== 'ok') {
|
||||
throw new Error('Password change failed.');
|
||||
}
|
||||
this.credentialMessage = 'Password changed successfully.';
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePassportEnroll(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminPassportEnrollmentEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreatePassportEnrollmentChallenge>('createPassportEnrollmentChallenge');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
deviceLabel: eventArg.detail.deviceLabel,
|
||||
platform: 'web',
|
||||
capabilities: {
|
||||
gps: false,
|
||||
nfc: false,
|
||||
push: false,
|
||||
},
|
||||
});
|
||||
this.passportEnrollment = response;
|
||||
this.credentialMessage = 'Passport enrollment challenge created.';
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePassportRevoke(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminPassportDeviceEventDetail>) {
|
||||
const device = this.passportDevices.find((deviceArg) => deviceArg.id === eventArg.detail.deviceId);
|
||||
if (!device || !confirm(`Revoke passport device ${device.label}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokePassportDevice>('revokePassportDevice');
|
||||
await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
deviceId: eventArg.detail.deviceId,
|
||||
});
|
||||
this.credentialMessage = 'Passport device revoked.';
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMemberInvite() {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
this.dataError = 'Select an organisation before inviting members.';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await BulkInviteModal.show({
|
||||
organizationId: selectedOrg.id,
|
||||
organizationName: selectedOrg.data.name,
|
||||
});
|
||||
if (result?.invitedCount) {
|
||||
await this.loadAdminShellData();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMemberRemove(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminMemberEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
const member = this.orgMembers.find((memberArg) => memberArg.userId === eventArg.detail.userId);
|
||||
if (!selectedOrg || !member || !confirm(`Remove ${member.name} from ${selectedOrg.data.name}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RemoveMember>('removeMember');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), organizationId: selectedOrg.id, userId: member.userId });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMemberRolesUpdate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminMemberRolesEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
this.dataError = 'Select an organisation before editing member roles.';
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateMemberRoles>('updateMemberRoles');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: selectedOrg.id,
|
||||
userId: eventArg.detail.userId,
|
||||
roles: eventArg.detail.roles,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Member role update failed.');
|
||||
}
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgRoleUpsert(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgRoleUpsertEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpsertOrgRoleDefinition>('upsertOrgRoleDefinition');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
roleDefinition: eventArg.detail.roleDefinition,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization role update failed.');
|
||||
}
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleOrgRoleDelete(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminOrgRoleDeleteEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteOrgRoleDefinition>('deleteOrgRoleDefinition');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
roleKey: eventArg.detail.roleKey,
|
||||
confirmationText: eventArg.detail.confirmationText,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'Organization role delete failed.');
|
||||
}
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
});
|
||||
}
|
||||
|
||||
private async handleAppRoleMappingsUpdate(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminAppRoleMappingsEventDetail>) {
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateAppRoleMappings>('updateAppRoleMappings');
|
||||
const response = await request.fire({
|
||||
jwt: await idpState.idpClient.getJwt(),
|
||||
organizationId: eventArg.detail.organizationId,
|
||||
appId: eventArg.detail.appId,
|
||||
roleMappings: eventArg.detail.roleMappings,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || 'App role mapping update failed.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInvitationResend(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminInvitationEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ResendInvitation>('resendInvitation');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), organizationId: selectedOrg.id, invitationId: eventArg.detail.invitationId });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleInvitationCancel(eventArg: CustomEvent<plugins.idpCatalog.IIdpAdminInvitationEventDetail>) {
|
||||
const selectedOrg = this.getSelectedOrganization();
|
||||
if (!selectedOrg || !confirm('Cancel this invitation?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runAdminAction(async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CancelInvitation>('cancelInvitation');
|
||||
await request.fire({ jwt: await idpState.idpClient.getJwt(), organizationId: selectedOrg.id, invitationId: eventArg.detail.invitationId });
|
||||
});
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||
super.firstUpdated(_changedProperties);
|
||||
await this.domtoolsPromise;
|
||||
this.subrouter = this.domtools.router.createSubRouter('/account');
|
||||
const viewcontainer: HTMLDivElement = this.shadowRoot.querySelector('.viewcontainer');
|
||||
|
||||
// Setup event listeners for modals
|
||||
this.addEventListener('open-org-select-modal', (async (e: CustomEvent) => {
|
||||
const result = await OrgSelectModal.show({
|
||||
targetPath: e.detail.targetPath,
|
||||
title: e.detail.title,
|
||||
description: e.detail.description,
|
||||
});
|
||||
if (result) {
|
||||
this.subrouter.pushUrl(result.path);
|
||||
}
|
||||
}) as EventListener);
|
||||
|
||||
this.addEventListener('open-create-org-modal', async () => {
|
||||
const org = await CreateOrgModal.show();
|
||||
if (org) {
|
||||
this.subrouter.pushUrl(`/org/${org.data.slug}/billing`);
|
||||
}
|
||||
});
|
||||
|
||||
const cleanupViews = async () => {
|
||||
for (const child of Array.from(viewcontainer.children)) {
|
||||
viewcontainer.removeChild(child);
|
||||
}
|
||||
};
|
||||
|
||||
viewcontainer.append(new views.BaseView());
|
||||
console.log(`loaded base view`);
|
||||
this.subrouter = this.domtools.router.createSubRouter('/dash');
|
||||
await states.accountState.dispatchAction(states.getOrganizationsAction, null);
|
||||
this.applyAccountState();
|
||||
|
||||
this.subrouter.on('', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the account overview');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.BaseView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.pushDashPath('/overview');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/billing', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the billing page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.SubscriptionView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.subrouter.on('/overview', async () => {
|
||||
this.setAdminPage('overview');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/paddlesetup', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the paddle setup page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.PaddleSetupView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.subrouter.on('/account/profile', async () => {
|
||||
this.setAdminPage('profile');
|
||||
});
|
||||
|
||||
this.subrouter.on('/account/security', async () => {
|
||||
this.setAdminPage('security');
|
||||
});
|
||||
|
||||
this.subrouter.on('/account/sessions', async () => {
|
||||
this.setAdminPage('sessions');
|
||||
});
|
||||
|
||||
this.subrouter.on('/account/apps', async () => {
|
||||
this.setAdminPage('apps');
|
||||
});
|
||||
|
||||
this.subrouter.on('/support', async () => {
|
||||
this.setAdminPage('support');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the org overview page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.OrgView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
await this.setOrgPage('org-general');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/settings', async () => {
|
||||
await this.setOrgPage('org-settings');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/apps', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the apps page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.AppsView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
await this.setOrgPage('org-apps');
|
||||
});
|
||||
|
||||
this.subrouter.on('/org/:orgName/users', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the users page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.UsersView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
await this.setOrgPage('org-members');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin', async () => {
|
||||
viewcontainer.classList.add('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
console.log('We are viewing the admin page');
|
||||
await cleanupViews();
|
||||
viewcontainer.append(new views.AdminView());
|
||||
viewcontainer.classList.remove('changing');
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
this.pushDashPath('/admin/apps');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin/users', async () => {
|
||||
this.setAdminPage('ga-users');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin/orgs', async () => {
|
||||
this.setAdminPage('ga-orgs');
|
||||
});
|
||||
|
||||
this.subrouter.on('/admin/apps', async () => {
|
||||
this.setAdminPage('ga-apps');
|
||||
});
|
||||
|
||||
this.subrouter._handleRouteState();
|
||||
|
||||
states.accountState.select((stateArg) => stateArg.user).subscribe(() => this.applyAccountState());
|
||||
states.accountState.select((stateArg) => stateArg.organizations).subscribe(() => this.applyAccountState());
|
||||
states.accountState.select((stateArg) => stateArg.roles).subscribe(() => this.applyAccountState());
|
||||
states.accountState.select((stateArg) => stateArg.selectedOrg).subscribe(() => this.applyAccountState());
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
this.subrouter.destroy();
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
description,
|
||||
});
|
||||
if (result) {
|
||||
await this.navigateTo(result.path.replace('/account', ''));
|
||||
await this.navigateTo(result.path.replace('/dash', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,8 +101,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logo dees-icon {
|
||||
font-size: 24px;
|
||||
.logo idp-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -157,13 +156,12 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.navigationOption dees-icon {
|
||||
font-size: 16px;
|
||||
.navigationOption idp-icon {
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navigationOption:hover dees-icon {
|
||||
.navigationOption:hover idp-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -172,7 +170,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.navigationOption.active dees-icon {
|
||||
.navigationOption.active idp-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -182,7 +180,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
margin: 8px 16px;
|
||||
}
|
||||
|
||||
dees-input-dropdown {
|
||||
idp-select {
|
||||
margin: 8px;
|
||||
}
|
||||
`,
|
||||
@@ -197,7 +195,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
return html`
|
||||
<div class="logoArea">
|
||||
<div class="logo">
|
||||
<dees-icon .icon=${'lucide:fingerprint'}></dees-icon>
|
||||
<idp-icon name="fingerprint" size="22"></idp-icon>
|
||||
idp.global
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,7 +206,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
class="navigationOption ${this.isActive('') ? 'active' : ''}"
|
||||
@click=${() => this.navigateTo('')}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:home'}></dees-icon>
|
||||
<idp-icon name="home" size="16"></idp-icon>
|
||||
Overview
|
||||
</div>
|
||||
<div
|
||||
@@ -217,7 +215,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:shield'}></dees-icon>
|
||||
<idp-icon name="shield" size="16"></idp-icon>
|
||||
Manage Roles
|
||||
</div>
|
||||
<div
|
||||
@@ -227,21 +225,21 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
idpState.domtools.router.pushUrl('/logout');
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:power'}></dees-icon>
|
||||
<idp-icon name="power" size="16"></idp-icon>
|
||||
Log Out
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="navigationGroupLabel">Organization</div>
|
||||
<dees-input-dropdown
|
||||
.label=${'Select organization'}
|
||||
@selectedOption=${async (eventArg: CustomEvent) => {
|
||||
<idp-select
|
||||
label="Select organization"
|
||||
@idp-select=${async (eventArg: CustomEvent<plugins.idpCatalog.IIdpSelectEventDetail>) => {
|
||||
// Handle "Create new..." option
|
||||
if (eventArg.detail.key === '__create_new__') {
|
||||
const org = await CreateOrgModal.show();
|
||||
if (org) {
|
||||
await this.navigateTo(`/org/${org.data.slug}/billing`);
|
||||
await this.navigateTo(`/org/${org.data.slug}/settings`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -252,9 +250,9 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
// Auto-navigate to new org's current page type (reactivity)
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath.includes('/org/') && newOrg) {
|
||||
// Extract the page type (apps, billing, etc.) and navigate to new org
|
||||
// Extract the page type (apps, settings, etc.) and navigate to new org
|
||||
const pathParts = currentPath.split('/');
|
||||
const pageType = pathParts[5]; // /account/org/:orgName/:pageType
|
||||
const pageType = pathParts[4]; // /dash/org/:orgName/:pageType
|
||||
if (pageType) {
|
||||
await this.navigateTo(`/org/${newOrg.data.slug}/${pageType}`);
|
||||
} else {
|
||||
@@ -262,42 +260,42 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
}
|
||||
}
|
||||
}}
|
||||
></dees-input-dropdown>
|
||||
></idp-select>
|
||||
|
||||
<div
|
||||
class="navigationOption ${this.isActive('org-overview') ? 'active' : ''}"
|
||||
@click=${() => this.navigateToOrgPage('')}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:home'}></dees-icon>
|
||||
<idp-icon name="home" size="16"></idp-icon>
|
||||
Overview
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption ${this.isActive('apps') ? 'active' : ''}"
|
||||
@click=${() => this.navigateToOrgPage('apps')}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
||||
<idp-icon name="box" size="16"></idp-icon>
|
||||
Apps
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption ${this.isActive('users') ? 'active' : ''}"
|
||||
@click=${() => this.navigateToOrgPage('users')}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:users'}></dees-icon>
|
||||
<idp-icon name="users" size="16"></idp-icon>
|
||||
Users
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
||||
<idp-icon name="activity" size="16"></idp-icon>
|
||||
Activity
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption ${this.isActive('billing') ? 'active' : ''}"
|
||||
@click=${() => this.navigateToOrgPage('billing')}
|
||||
class="navigationOption ${this.isActive('settings') ? 'active' : ''}"
|
||||
@click=${() => this.navigateToOrgPage('settings')}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:wallet'}></dees-icon>
|
||||
Billing
|
||||
<idp-icon name="settings" size="16"></idp-icon>
|
||||
Settings
|
||||
</div>
|
||||
|
||||
${this.renderAdminLink()}
|
||||
@@ -318,7 +316,7 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
class="navigationOption ${this.isActive('admin') ? 'active' : ''}"
|
||||
@click=${() => this.navigateTo('/admin')}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:shield'}></dees-icon>
|
||||
<idp-icon name="shield" size="16"></idp-icon>
|
||||
Global Admin
|
||||
</div>
|
||||
`;
|
||||
@@ -328,11 +326,11 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
const path = this.currentPath;
|
||||
if (page === '') {
|
||||
// Account overview - exact match
|
||||
return path === '/account' || path === '/account/';
|
||||
return path === '/dash' || path === '/dash/';
|
||||
}
|
||||
if (page === 'org-overview') {
|
||||
// Org overview - /account/org/:slug without trailing page type
|
||||
return /^\/account\/org\/[^\/]+\/?$/.test(path);
|
||||
// Org overview - /dash/org/:slug without trailing page type
|
||||
return /^\/dash\/org\/[^\/]+\/?$/.test(path);
|
||||
}
|
||||
// For other pages, check if the path contains the page segment
|
||||
return path.includes(`/${page}`);
|
||||
@@ -355,8 +353,8 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
};
|
||||
requestAnimationFrame(checkPath);
|
||||
|
||||
const deesInputDropdown = this.shadowRoot.querySelector('dees-input-dropdown');
|
||||
const orgToMenuEntry = (orgArg?: plugins.idpInterfaces.data.IOrganization) => {
|
||||
const orgSelect = this.shadowRoot.querySelector('idp-select') as plugins.idpCatalog.IdpSelect | null;
|
||||
const orgToMenuEntry = (orgArg?: plugins.idpInterfaces.data.IOrganization): plugins.idpCatalog.IIdpSelectOption | null => {
|
||||
if (!orgArg) {
|
||||
return null;
|
||||
}
|
||||
@@ -378,19 +376,25 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
.select((stateArg) => stateArg.organizations)
|
||||
.pipe(
|
||||
plugins.deesDomtools.plugins.smartrx.rxjs.ops.map((orgArrayArg) => {
|
||||
const orgEntries = orgArrayArg.map(orgToMenuEntry);
|
||||
const orgEntries = orgArrayArg
|
||||
.map(orgToMenuEntry)
|
||||
.filter((entryArg): entryArg is plugins.idpCatalog.IIdpSelectOption => Boolean(entryArg));
|
||||
// Add "Create new..." at the end
|
||||
return [...orgEntries, createNewOption];
|
||||
})
|
||||
)
|
||||
.subscribe((menuEntries) => {
|
||||
deesInputDropdown.options = menuEntries;
|
||||
if (orgSelect) {
|
||||
orgSelect.options = menuEntries;
|
||||
}
|
||||
});
|
||||
states.accountState
|
||||
.select((stateArg) => stateArg.selectedOrg)
|
||||
.pipe(plugins.deesDomtools.plugins.smartrx.rxjs.ops.map(orgToMenuEntry))
|
||||
.subscribe((selectedOrgArg) => {
|
||||
deesInputDropdown.selectedOption = selectedOrgArg;
|
||||
if (orgSelect) {
|
||||
orgSelect.selectedOption = selectedOrgArg;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is global admin
|
||||
|
||||
@@ -97,14 +97,12 @@ export class BaseView extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #18181b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
idp-card.card::part(card) {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card.full-width {
|
||||
idp-card.card.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@ -124,7 +122,7 @@ export class BaseView extends DeesElement {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-title dees-icon {
|
||||
.card-title idp-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -209,7 +207,7 @@ export class BaseView extends DeesElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.org-icon dees-icon {
|
||||
.org-icon idp-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -290,7 +288,7 @@ export class BaseView extends DeesElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-icon dees-icon {
|
||||
.session-icon idp-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -298,7 +296,7 @@ export class BaseView extends DeesElement {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.session-icon.current dees-icon {
|
||||
.session-icon.current idp-icon {
|
||||
color: #22c55e;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -382,8 +380,7 @@ export class BaseView extends DeesElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-icon dees-icon {
|
||||
font-size: 14px;
|
||||
.activity-icon idp-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -391,7 +388,7 @@ export class BaseView extends DeesElement {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.activity-icon.login dees-icon {
|
||||
.activity-icon.login idp-icon {
|
||||
color: #22c55e;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -400,7 +397,7 @@ export class BaseView extends DeesElement {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.activity-icon.logout dees-icon {
|
||||
.activity-icon.logout idp-icon {
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -427,8 +424,7 @@ export class BaseView extends DeesElement {
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
font-size: 32px;
|
||||
.empty-state idp-icon {
|
||||
opacity: 0.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -467,7 +463,7 @@ export class BaseView extends DeesElement {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
.create-org-btn dees-icon {
|
||||
.create-org-btn idp-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
`,
|
||||
@@ -494,10 +490,10 @@ export class BaseView extends DeesElement {
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- Profile Card -->
|
||||
<div class="card">
|
||||
<idp-card class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:user'}></dees-icon>
|
||||
<idp-icon name="user" size="16"></idp-icon>
|
||||
Profile
|
||||
</span>
|
||||
</div>
|
||||
@@ -510,50 +506,49 @@ export class BaseView extends DeesElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</idp-card>
|
||||
|
||||
<!-- Organizations Card -->
|
||||
<div class="card">
|
||||
<idp-card class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
<idp-icon name="building2" size="16"></idp-icon>
|
||||
Organizations
|
||||
</span>
|
||||
<button class="create-org-btn" @click=${this.handleCreateOrg}>
|
||||
<dees-icon .icon=${'lucide:plus'}></dees-icon>
|
||||
<idp-button variant="outline" size="sm" icon="plus" @click=${this.handleCreateOrg}>
|
||||
New
|
||||
</button>
|
||||
</idp-button>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
${this.renderOrganizations()}
|
||||
</div>
|
||||
</div>
|
||||
</idp-card>
|
||||
|
||||
<!-- Sessions Card -->
|
||||
<div class="card">
|
||||
<idp-card class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:monitor-smartphone'}></dees-icon>
|
||||
<idp-icon name="monitor-smartphone" size="16"></idp-icon>
|
||||
Active Sessions
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
${this.renderSessions()}
|
||||
</div>
|
||||
</div>
|
||||
</idp-card>
|
||||
|
||||
<!-- Activity Card -->
|
||||
<div class="card">
|
||||
<idp-card class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
||||
<idp-icon name="activity" size="16"></idp-icon>
|
||||
Recent Activity
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body no-padding">
|
||||
${this.renderActivity()}
|
||||
</div>
|
||||
</div>
|
||||
</idp-card>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -563,7 +558,7 @@ export class BaseView extends DeesElement {
|
||||
if (this.organizations.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
<idp-icon name="building2" size="32"></idp-icon>
|
||||
<p>You're not a member of any organizations yet.</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -580,13 +575,13 @@ export class BaseView extends DeesElement {
|
||||
return html`
|
||||
<div class="org-item" @click=${() => this.handleSelectOrg(org)}>
|
||||
<div class="org-icon">
|
||||
<dees-icon .icon=${'lucide:building2'}></dees-icon>
|
||||
<idp-icon name="building2" size="16"></idp-icon>
|
||||
</div>
|
||||
<div class="org-info">
|
||||
<div class="org-name">${org.data.name}</div>
|
||||
<div class="org-role">${org.data.slug}</div>
|
||||
</div>
|
||||
<span class="role-badge ${roleClass}">${roleDisplay}</span>
|
||||
<idp-badge variant=${roleClass === 'owner' ? 'accent' : roleClass === 'admin' ? 'warn' : 'outline'}>${roleDisplay}</idp-badge>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@@ -598,7 +593,7 @@ export class BaseView extends DeesElement {
|
||||
if (this.sessions.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:monitor'}></dees-icon>
|
||||
<idp-icon name="monitor" size="32"></idp-icon>
|
||||
<p>No active sessions found.</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -609,12 +604,12 @@ export class BaseView extends DeesElement {
|
||||
${this.sessions.map((session) => html`
|
||||
<div class="session-item" data-session-id=${session.id}>
|
||||
<div class="session-icon ${session.isCurrent ? 'current' : ''}">
|
||||
<dees-icon .icon=${this.getDeviceIcon(session.os)}></dees-icon>
|
||||
<idp-icon name=${this.getDeviceIcon(session.os)} size="16"></idp-icon>
|
||||
</div>
|
||||
<div class="session-info">
|
||||
<div class="session-device">
|
||||
${session.deviceName || 'Unknown Device'}
|
||||
${session.isCurrent ? html`<span class="current-badge">Current</span>` : ''}
|
||||
${session.isCurrent ? html`<idp-badge variant="ok">Current</idp-badge>` : ''}
|
||||
</div>
|
||||
<div class="session-details">
|
||||
${session.browser} · ${session.os} · Last active ${this.formatTimeAgo(session.lastActive)}
|
||||
@@ -622,9 +617,9 @@ export class BaseView extends DeesElement {
|
||||
</div>
|
||||
${!session.isCurrent ? html`
|
||||
<div class="session-actions">
|
||||
<button class="revoke-btn" @click=${() => this.handleRevokeSession(session.id)}>
|
||||
<idp-button variant="destructive" size="sm" @click=${() => this.handleRevokeSession(session.id)}>
|
||||
Revoke
|
||||
</button>
|
||||
</idp-button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -637,7 +632,7 @@ export class BaseView extends DeesElement {
|
||||
if (this.activities.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
||||
<idp-icon name="activity" size="32"></idp-icon>
|
||||
<p>No recent activity.</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -648,7 +643,7 @@ export class BaseView extends DeesElement {
|
||||
${this.activities.slice(0, 5).map((activity) => html`
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon ${this.getActivityIconClass(activity.data.action)}">
|
||||
<dees-icon .icon=${this.getActivityIcon(activity.data.action)}></dees-icon>
|
||||
<idp-icon name=${this.getActivityIcon(activity.data.action)} size="14"></idp-icon>
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<div class="activity-description">${activity.data.metadata.description}</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ export class SubscriptionView extends DeesElement {
|
||||
|
||||
<h3>Paddle</h3>
|
||||
<dees-button @click=${async () => {
|
||||
// Extract org slug from current URL: /account/org/{orgSlug}/billing
|
||||
// Extract org slug from current URL: /dash/org/{orgSlug}/settings
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const orgSlug = pathParts[3];
|
||||
// Use parent's subrouter for proper navigation within account section
|
||||
@@ -152,4 +152,4 @@ export class SubscriptionView extends DeesElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ export class UsersView extends DeesElement {
|
||||
@state()
|
||||
accessor organizationName: string = '';
|
||||
|
||||
@state()
|
||||
accessor organizationSlug: string = '';
|
||||
|
||||
@state()
|
||||
accessor inviteEmail: string = '';
|
||||
|
||||
@@ -631,6 +634,7 @@ export class UsersView extends DeesElement {
|
||||
|
||||
this.organizationId = selectedOrg.id;
|
||||
this.organizationName = selectedOrg.data.name;
|
||||
this.organizationSlug = selectedOrg.data.slug;
|
||||
this.currentUserId = currentState.user?.id || '';
|
||||
|
||||
// Check if current user is admin/owner
|
||||
@@ -855,8 +859,8 @@ export class UsersView extends DeesElement {
|
||||
}
|
||||
|
||||
private async handleTransferOwnership(newOwnerId: string, name: string) {
|
||||
const confirmed = await this.showTransferConfirmation(name);
|
||||
if (!confirmed) return;
|
||||
const confirmationText = await this.showTransferConfirmation(name);
|
||||
if (!confirmationText) return;
|
||||
|
||||
this.submitting = true;
|
||||
this.actionMessage = null;
|
||||
@@ -873,6 +877,7 @@ export class UsersView extends DeesElement {
|
||||
jwt,
|
||||
organizationId: this.organizationId,
|
||||
newOwnerId,
|
||||
confirmationText,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
@@ -889,8 +894,10 @@ export class UsersView extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async showTransferConfirmation(name: string): Promise<boolean> {
|
||||
private async showTransferConfirmation(name: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const expectedText = `transfer ${this.organizationSlug}`;
|
||||
let confirmationText = '';
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Transfer Ownership',
|
||||
content: html`
|
||||
@@ -899,11 +906,15 @@ export class UsersView extends DeesElement {
|
||||
<p style="margin: 0; color: var(--muted-foreground);">
|
||||
You will be demoted to admin role and will no longer be the owner of this organization.
|
||||
</p>
|
||||
<p style="margin: 12px 0 8px 0; color: var(--muted-foreground);">
|
||||
Type <code>${expectedText}</code> to confirm.
|
||||
</p>
|
||||
<input style="box-sizing:border-box;width:100%;padding:8px;border:1px solid var(--border);border-radius:8px;" @input=${(eventArg: Event) => { confirmationText = (eventArg.target as HTMLInputElement).value; }} />
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modal) => { modal.destroy(); resolve(false); } },
|
||||
{ name: 'Transfer Ownership', action: async (modal) => { modal.destroy(); resolve(true); } },
|
||||
{ name: 'Cancel', action: async (modal) => { modal.destroy(); resolve(null); } },
|
||||
{ name: 'Transfer Ownership', action: async (modal) => { modal.destroy(); resolve(confirmationText.trim() === expectedText ? confirmationText.trim() : null); } },
|
||||
],
|
||||
width: 420,
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ export class IdpCenterContainer extends DeesElement {
|
||||
|
||||
/* Left Panel - Branding */
|
||||
.brand-panel {
|
||||
background: linear-gradient(135deg, hsl(240 10% 8%) 0%, hsl(240 10% 4%) 50%, hsl(240 12% 6%) 100%);
|
||||
background: #09090B;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -74,8 +74,9 @@ export class IdpCenterContainer extends DeesElement {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(ellipse at 30% 20%, hsla(240 20% 20% / 0.3) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 70% 80%, hsla(240 20% 15% / 0.2) 0%, transparent 50%);
|
||||
background:
|
||||
radial-gradient(ellipse at 50% -10%, rgb(110 91 230 / 0.18) 0%, transparent 58%),
|
||||
radial-gradient(circle at 2px 2px, rgb(255 255 255 / 0.04) 1px, transparent 0) 0 0 / 32px 32px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -87,18 +88,41 @@ export class IdpCenterContainer extends DeesElement {
|
||||
|
||||
.logo {
|
||||
font-family: 'Cal Sans', 'Geist Sans', sans-serif;
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
font-size: clamp(44px, 6vw, 72px);
|
||||
font-weight: 900;
|
||||
color: var(--foreground);
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 18px;
|
||||
color: var(--muted-foreground);
|
||||
margin: 0 0 48px 0;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 44px 0;
|
||||
line-height: 1.65;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-bottom: 28px;
|
||||
padding: 5px 12px;
|
||||
border: 1px solid rgb(255 255 255 / 0.1);
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
color: rgb(255 255 255 / 0.5);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #16A34A;
|
||||
}
|
||||
|
||||
.features {
|
||||
@@ -117,17 +141,16 @@ export class IdpCenterContainer extends DeesElement {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: hsla(240 10% 20% / 0.5);
|
||||
border: 1px solid hsla(240 10% 30% / 0.3);
|
||||
background: rgb(255 255 255 / 0.045);
|
||||
border: 1px solid rgb(255 255 255 / 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-icon dees-icon {
|
||||
.feature-icon idp-icon {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.feature-text h3 {
|
||||
@@ -146,6 +169,9 @@ export class IdpCenterContainer extends DeesElement {
|
||||
|
||||
.learn-more {
|
||||
margin-top: 48px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Right Panel - Form */
|
||||
@@ -258,12 +284,13 @@ export class IdpCenterContainer extends DeesElement {
|
||||
<div class="brand-panel">
|
||||
<div class="brand-content">
|
||||
<h1 class="logo">idp.global</h1>
|
||||
<p class="tagline">Your permanent identity on the web</p>
|
||||
<div class="badge"><span class="badge-dot"></span>Open identity infrastructure</div>
|
||||
<p class="tagline">One Identity. Any Scale. Yours Forever.</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<div class="feature-icon">
|
||||
<dees-icon .icon=${'lucide:code'}></dees-icon>
|
||||
<idp-icon name="globe" size="18"></idp-icon>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<h3>Open Source</h3>
|
||||
@@ -273,7 +300,7 @@ export class IdpCenterContainer extends DeesElement {
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-icon">
|
||||
<dees-icon .icon=${'lucide:heart'}></dees-icon>
|
||||
<idp-icon name="shield" size="18"></idp-icon>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<h3>Always Free</h3>
|
||||
@@ -283,7 +310,7 @@ export class IdpCenterContainer extends DeesElement {
|
||||
|
||||
<div class="feature">
|
||||
<div class="feature-icon">
|
||||
<dees-icon .icon=${'lucide:fingerprint'}></dees-icon>
|
||||
<idp-icon name="key" size="18"></idp-icon>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<h3>Permanent Identity</h3>
|
||||
@@ -293,10 +320,14 @@ export class IdpCenterContainer extends DeesElement {
|
||||
</div>
|
||||
|
||||
<div class="learn-more">
|
||||
<dees-button
|
||||
type="secondary"
|
||||
<idp-button
|
||||
variant="outline"
|
||||
@click=${() => window.open('https://about.idp.global', '_blank')}
|
||||
>Learn more</dees-button>
|
||||
>Learn more</idp-button>
|
||||
<idp-button
|
||||
variant="ghost"
|
||||
@click=${() => window.open('https://code.foss.global/idp.global/app', '_blank')}
|
||||
>Source code</idp-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
|
||||
import '@uptime.link/webwidget';
|
||||
|
||||
import '@design.estate/dees-catalog';
|
||||
import { DeesForm, DeesFormSubmit, DeesInputText } from '@design.estate/dees-catalog';
|
||||
import { IdpState } from '../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
@@ -146,7 +144,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm | null;
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as plugins.idpCatalog.IdpForm | null;
|
||||
loginForm?.setStatus('pending', 'preparing application authorization...');
|
||||
this.oidcConsentError = '';
|
||||
|
||||
@@ -177,7 +175,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
}
|
||||
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm | null;
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as plugins.idpCatalog.IdpForm | null;
|
||||
loginForm?.setStatus('pending', 'authorizing application...');
|
||||
this.oidcConsentError = '';
|
||||
|
||||
@@ -233,7 +231,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dees-form {
|
||||
idp-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@@ -318,25 +316,6 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.consent-button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 12px 18px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.consent-button-secondary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.consent-button-primary {
|
||||
background: linear-gradient(135deg, #9b7bff, #5fd1ff);
|
||||
color: #0a0a0a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.consent-error {
|
||||
color: #ff9a9a;
|
||||
font-size: 14px;
|
||||
@@ -370,16 +349,16 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
</div>
|
||||
${this.oidcConsentError ? html`<div class="consent-error">${this.oidcConsentError}</div>` : null}
|
||||
<div class="consent-actions">
|
||||
<button
|
||||
class="consent-button consent-button-secondary"
|
||||
<idp-button
|
||||
variant="outline"
|
||||
@click=${() => {
|
||||
this.redirectOidcError('access_denied');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="consent-button consent-button-primary"
|
||||
</idp-button>
|
||||
<idp-button
|
||||
variant="accent"
|
||||
@click=${async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
@@ -391,7 +370,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
}}
|
||||
>
|
||||
Allow and continue
|
||||
</button>
|
||||
</idp-button>
|
||||
</div>
|
||||
</div>
|
||||
</idp-centercontainer>
|
||||
@@ -404,29 +383,31 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
<h2>Sign in to your account</h2>
|
||||
<p>Enter your credentials to continue</p>
|
||||
</div>
|
||||
<dees-form
|
||||
<idp-form
|
||||
id="loginForm"
|
||||
@formData=${(eventArg) => {
|
||||
@idp-submit=${(eventArg: CustomEvent<plugins.idpCatalog.IIdpFormSubmitEventDetail>) => {
|
||||
this.login({
|
||||
emailAddress: eventArg.detail.data.emailAddress,
|
||||
passwordArg: eventArg.detail.data.password,
|
||||
emailAddress: String(eventArg.detail.data.emailAddress || ''),
|
||||
passwordArg: String(eventArg.detail.data.password || ''),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<dees-input-text
|
||||
<idp-input
|
||||
id="loginEmailInput"
|
||||
.required=${true}
|
||||
key="emailAddress"
|
||||
required
|
||||
name="emailAddress"
|
||||
label="Email or Username"
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.id=${'loginPasswordInput'}
|
||||
.key=${'password'}
|
||||
.label=${'Password'}
|
||||
.isPasswordBool=${true}
|
||||
></dees-input-text>
|
||||
<dees-form-submit id="loginSubmitButton"></dees-form-submit>
|
||||
</dees-form>
|
||||
autocomplete="username"
|
||||
></idp-input>
|
||||
<idp-input
|
||||
id="loginPasswordInput"
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
></idp-input>
|
||||
<idp-form-submit id="loginSubmitButton"></idp-form-submit>
|
||||
</idp-form>
|
||||
<div class="form-footer">
|
||||
Don't have an account?
|
||||
<a @click=${async () => {
|
||||
@@ -441,9 +422,9 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
public async firstUpdated() {
|
||||
await this.domtoolsPromise;
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm;
|
||||
const loginPasswordInput = loginForm.querySelector('#loginPasswordInput') as DeesInputText;
|
||||
const loginSubmitButton = loginForm.querySelector('#loginSubmitButton') as DeesFormSubmit;
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as plugins.idpCatalog.IdpForm;
|
||||
const loginPasswordInput = loginForm.querySelector('#loginPasswordInput') as plugins.idpCatalog.IdpInput;
|
||||
const loginSubmitButton = loginForm.querySelector('#loginSubmitButton') as plugins.idpCatalog.IdpFormSubmit;
|
||||
const oidcContext = this.getOidcAuthorizationContext();
|
||||
const setButtonText = async () => {
|
||||
if (loginPasswordInput.value) {
|
||||
@@ -452,7 +433,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
loginSubmitButton.text = 'Send magic link (or enter password)';
|
||||
}
|
||||
};
|
||||
loginForm.changeSubject.subscribe(() => {
|
||||
loginForm.addEventListener('idp-input-change', () => {
|
||||
void setButtonText();
|
||||
});
|
||||
await setButtonText();
|
||||
@@ -470,17 +451,19 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
await this.handleOidcAfterLogin(jwt);
|
||||
}
|
||||
}
|
||||
} else if (await idpState.idpClient.determineLoginStatus(false)) {
|
||||
idpState.domtools.router.pushUrl('/dash/overview');
|
||||
}
|
||||
}
|
||||
|
||||
private login = async (valueArg: { emailAddress: string; passwordArg: string }) => {
|
||||
const loginSubmitButton = this.shadowRoot.querySelector(
|
||||
'#loginSubmitButton'
|
||||
) as plugins.deesCatalog.DeesFormSubmit;
|
||||
) as plugins.idpCatalog.IdpFormSubmit;
|
||||
loginSubmitButton.disabled = true;
|
||||
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm;
|
||||
const loginForm = this.shadowRoot.querySelector('#loginForm') as plugins.idpCatalog.IdpForm;
|
||||
const loginRequestWithUsernameAndPassword =
|
||||
idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
|
||||
'loginWithEmailOrUsernameAndPassword'
|
||||
@@ -512,7 +495,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
loginForm.setStatus('success', 'obtained jwt.');
|
||||
const oidcHandled = await this.handleOidcAfterLogin(jwt);
|
||||
if (!oidcHandled) {
|
||||
idpState.domtools.router.pushUrl('/account');
|
||||
idpState.domtools.router.pushUrl('/dash/overview');
|
||||
}
|
||||
} else {
|
||||
loginForm.setStatus('error', 'something went wrong');
|
||||
@@ -522,8 +505,12 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
loginForm.setStatus('pending', 'sending magic link...');
|
||||
const response = await loginRequestWithEmail.fire({
|
||||
email: valueArg.emailAddress,
|
||||
}).catch((err) => {
|
||||
const message = err?.errorText || err?.message || 'Could not send the magic link. Please try again.';
|
||||
loginForm.setStatus('error', message);
|
||||
return null;
|
||||
});
|
||||
if (response.status === 'ok') {
|
||||
if (response?.status === 'ok') {
|
||||
loginForm.setStatus('success', 'Please check your email!');
|
||||
}
|
||||
}
|
||||
@@ -547,7 +534,7 @@ export class IdpLoginPrompt extends DeesElement {
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
(this.shadowRoot.querySelector('#loginEmailInput') as plugins.deesCatalog.DeesInputText).focus();
|
||||
(this.shadowRoot.querySelector('#loginEmailInput') as plugins.idpCatalog.IdpInput).focus();
|
||||
}
|
||||
|
||||
public async show() {
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
// third party catalogs
|
||||
import '@uptime.link/webwidget';
|
||||
|
||||
import '@design.estate/dees-catalog';
|
||||
import { DeesForm, DeesFormSubmit, DeesInputText } from '@design.estate/dees-catalog';
|
||||
import { IdpState } from '../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
@@ -27,7 +25,7 @@ declare global {
|
||||
|
||||
@customElement('idp-registrationprompt')
|
||||
export class IdpRegistrationPrompt extends DeesElement {
|
||||
public static demo = () => html`<idp-login></idp-login>`;
|
||||
public static demo = () => html`<idp-registrationprompt></idp-registrationprompt>`;
|
||||
|
||||
@property()
|
||||
accessor productOfInterest: string;
|
||||
@@ -79,7 +77,7 @@ export class IdpRegistrationPrompt extends DeesElement {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dees-form {
|
||||
idp-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@@ -113,25 +111,28 @@ export class IdpRegistrationPrompt extends DeesElement {
|
||||
<h2>Create your account</h2>
|
||||
<p>Get started with your permanent identity</p>
|
||||
</div>
|
||||
<dees-form
|
||||
<idp-form
|
||||
id="registrationForm"
|
||||
@formData="${(eventArg) => {
|
||||
@idp-submit=${(eventArg: CustomEvent<plugins.idpCatalog.IIdpFormSubmitEventDetail>) => {
|
||||
this.register({
|
||||
emailAddress: eventArg.detail.data.emailAddress,
|
||||
emailAddress: String(eventArg.detail.data.emailAddress || ''),
|
||||
});
|
||||
}}"
|
||||
}}
|
||||
>
|
||||
<dees-input-text
|
||||
.required=${true}
|
||||
key="emailAddress"
|
||||
<idp-input
|
||||
required
|
||||
name="emailAddress"
|
||||
label="Email Address"
|
||||
></dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.label="${'I agree to the Terms and Conditions'}"
|
||||
.required=${true}
|
||||
></dees-input-checkbox>
|
||||
<dees-form-submit>Send Verification Email</dees-form-submit>
|
||||
</dees-form>
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
></idp-input>
|
||||
<idp-checkbox
|
||||
name="termsAccepted"
|
||||
label="I agree to the Terms and Conditions"
|
||||
required
|
||||
></idp-checkbox>
|
||||
<idp-form-submit>Send Verification Email</idp-form-submit>
|
||||
</idp-form>
|
||||
<div class="form-footer">
|
||||
Already have an account? <a @click=${async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
@@ -147,28 +148,12 @@ export class IdpRegistrationPrompt extends DeesElement {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const loggedIn = await idpState.idpClient.determineLoginStatus();
|
||||
if (loggedIn) {
|
||||
idpState.domtools.router.pushUrl('/');
|
||||
idpState.domtools.router.pushUrl('/dash/overview');
|
||||
}
|
||||
const loginForm: DeesForm = this.shadowRoot.querySelector('#loginForm');
|
||||
const loginPasswordInput: DeesInputText = loginForm.querySelector('#loginPasswordInput');
|
||||
const loginSubmitButton: DeesFormSubmit = loginForm.querySelector('#loginSubmitButton');
|
||||
const setButtonText = async () => {
|
||||
if (loginPasswordInput.value) {
|
||||
console.log('updating text of registrationprompt.');
|
||||
loginSubmitButton.text = 'Login';
|
||||
} else {
|
||||
loginSubmitButton.text = 'Send magic link (or enter password)';
|
||||
}
|
||||
};
|
||||
loginForm.changeSubject.subscribe(() => {
|
||||
console.log(`checking button text ${loginPasswordInput.value}`);
|
||||
setButtonText();
|
||||
});
|
||||
setButtonText();
|
||||
}
|
||||
|
||||
private register = async (valueArg: { emailAddress: string }) => {
|
||||
const registrationForm: DeesForm = this.shadowRoot.querySelector('#registrationForm');
|
||||
const registrationForm = this.shadowRoot.querySelector('#registrationForm') as plugins.idpCatalog.IdpForm;
|
||||
registrationForm.setStatus('pending', 'registering...');
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const firstSignupRequest =
|
||||
@@ -181,11 +166,14 @@ export class IdpRegistrationPrompt extends DeesElement {
|
||||
productSlugOfInterest: this.productOfInterest,
|
||||
})
|
||||
.catch((err) => {
|
||||
registrationForm.setStatus('error', err.message);
|
||||
const message = err?.errorText || err?.message || 'Registration request failed. Please try again.';
|
||||
registrationForm.setStatus('error', message);
|
||||
return null;
|
||||
});
|
||||
if (response.status === 'ok') {
|
||||
if (response?.status === 'ok') {
|
||||
registrationForm.setStatus('success', 'Please check your email!');
|
||||
} else if (response) {
|
||||
registrationForm.setStatus('error', 'Registration request failed. Please try again.');
|
||||
}
|
||||
console.log(response);
|
||||
};
|
||||
|
||||
@@ -497,7 +497,7 @@ export class IdpRegistrationStepper extends DeesElement {
|
||||
}
|
||||
|
||||
deesForm.setStatus('success', 'Ok! Lets Go!');
|
||||
idpState.domtools.router.pushUrl('/account');
|
||||
idpState.domtools.router.pushUrl('/dash/overview');
|
||||
}, { signal });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -102,19 +102,20 @@ export class IdpWelcome extends DeesElement {
|
||||
<p class="greeting">Signed in as <strong>${data.user.data.name}</strong></p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<dees-button
|
||||
<idp-button
|
||||
variant="accent"
|
||||
@click=${async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
idpState.domtools.router.pushUrl('/account');
|
||||
idpState.domtools.router.pushUrl('/dash/overview');
|
||||
}}
|
||||
>Manage your account</dees-button>
|
||||
<dees-button
|
||||
type="secondary"
|
||||
>Open dashboard</idp-button>
|
||||
<idp-button
|
||||
variant="outline"
|
||||
@click=${async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
idpState.domtools.router.pushUrl('/logout');
|
||||
}}
|
||||
>Sign out</dees-button>
|
||||
>Sign out</idp-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -124,29 +125,30 @@ export class IdpWelcome extends DeesElement {
|
||||
<p>Sign in to your account or create a new one</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<dees-button
|
||||
<idp-button
|
||||
variant="accent"
|
||||
@click=${async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
idpState.domtools.router.pushUrl('/login');
|
||||
}}
|
||||
>Sign In</dees-button>
|
||||
<dees-button
|
||||
type="secondary"
|
||||
>Sign In</idp-button>
|
||||
<idp-button
|
||||
variant="outline"
|
||||
@click=${async () => {
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
idpState.domtools.router.pushUrl('/register');
|
||||
}}
|
||||
>Create Account</dees-button>
|
||||
>Create Account</idp-button>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="secondary-actions">
|
||||
<dees-button
|
||||
type="discreet"
|
||||
<idp-button
|
||||
variant="ghost"
|
||||
@click=${() => {
|
||||
window.open('https://code.foss.global/idp.global/idp.global', '_blank');
|
||||
window.open('https://code.foss.global/idp.global/app', '_blank');
|
||||
}}
|
||||
>View Source Code</dees-button>
|
||||
>View Source Code</idp-button>
|
||||
</div>
|
||||
</idp-centercontainer>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user