feat(app): wire dashboard administration flows

This commit is contained in:
2026-05-07 15:35:37 +00:00
parent e9eb9b4172
commit 91f06ccae1
91 changed files with 4087 additions and 5863 deletions
+793 -133
View File
@@ -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();
})
+40 -36
View File
@@ -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
+36 -41
View File
@@ -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>
`;
}
}
}
+16 -5
View File
@@ -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,
});