('idp-admin-app-role-mappings-update', {
organizationId: this.currentOrg.id,
appId: this.dialogApp.id,
roleMappings,
});
this.closeDialog();
}
private renderDialogError(): TemplateResult {
return this.dialogError
? html`Action blocked
${this.dialogError}
`
: html``;
}
private renderRoleUpsertDialog(): TemplateResult {
const isEdit = this.orgRoleDefinitions.some((roleArg) => roleArg.key === this.dialogRoleKey);
return html`
eventArg.stopPropagation()}>
${isEdit ? 'Edit custom role' : 'Add custom role'}
Custom roles model organisation-specific business access.
${this.renderDialogError()}
${this.renderFormRow('Role name', 'Readable label shown to admins.', html` this.setDialogRoleField('name', eventArg)} />`, true)}
${this.renderFormRow('Role key', 'Lowercase stable identifier used in assignments and app mappings.', html` this.setDialogRoleField('key', eventArg)} ?disabled=${isEdit} />`, true)}
${this.renderFormRow('Description', 'Optional admin note describing when this role should be used.', html``)}
`;
}
private renderRoleDeleteDialog(): TemplateResult {
const expectedText = `delete role ${this.dialogRoleKey}`;
return html`
eventArg.stopPropagation()}>
Delete custom role
This removes the role from member assignments and app mappings after backend confirmation.
${this.renderDialogError()}
${this.dialogRoleName}
Type ${expectedText} to confirm deletion.
${this.renderFormRow('Confirmation', `Type ${expectedText}`, html`
this.setDialogRoleField('deleteConfirmation', eventArg)} />`, true)}
`;
}
private renderMemberRolesDialog(): TemplateResult {
const member = this.dialogMember;
return html`
eventArg.stopPropagation()}>
Edit member roles
${member?.name || member?.email || 'Member'} receives the selected organisation roles.
`;
}
private renderAppRoleMappingsDialog(): TemplateResult {
const app = this.dialogApp;
return html`
eventArg.stopPropagation()}>
Map organisation roles
Map ${this.currentOrg.name} roles to app-specific roles, permissions, and OAuth scopes for ${app?.name || 'this app'}.
${this.renderDialogError()}
${app?.scopes?.length ? html`
Available OAuth scopes: ${app.scopes.join(', ')}
` : html`
This app has no declared OAuth scopes. Role and permission mappings are still supported.
`}
${this.dialogAppMappings.map((mappingArg) => {
const role = this.availableOrgRoles.find((roleArg) => roleArg.key === mappingArg.orgRoleKey);
return html`
${role?.name || mappingArg.orgRoleKey}${mappingArg.orgRoleKey}
${this.renderFormRow('App roles', '', html` this.setDialogMappingField(mappingArg.orgRoleKey, 'appRoles', eventArg)} />`)}
${this.renderFormRow('Permissions', '', html` this.setDialogMappingField(mappingArg.orgRoleKey, 'permissions', eventArg)} />`)}
${this.renderFormRow('Scopes', '', html` this.setDialogMappingField(mappingArg.orgRoleKey, 'scopes', eventArg)} />`)}
`;
})}
`;
}
private renderDialog(): TemplateResult {
if (this.dialogMode === 'none') {
return html``;
}
const dialog = this.dialogMode === 'role-upsert'
? this.renderRoleUpsertDialog()
: this.dialogMode === 'role-delete'
? this.renderRoleDeleteDialog()
: this.dialogMode === 'member-roles'
? this.renderMemberRolesDialog()
: this.renderAppRoleMappingsDialog();
return html` this.closeDialog()}>${dialog}
`;
}
private renderNavGroup(items: TNavItem[], active = ''): TemplateResult {
return html`
${items.map((item) => html`
`)}
`;
}
private renderSidebar(): TemplateResult {
const currentOrg = this.currentOrg;
return html`
`;
}
private renderSparkline(data: number[], color: string): TemplateResult {
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
const width = 100;
const height = 22;
const points = data.map((valueArg, indexArg) => {
const x = (indexArg / (data.length - 1)) * width;
const y = height - ((valueArg - min) / range) * (height - 4) - 2;
return `${x},${y}`;
}).join(' ');
const area = `0,${height} ${points} ${width},${height}`;
return html`
`;
}
private renderKpi(kpi: TKpi): TemplateResult {
return html`
${kpi.label}
${kpi.value}${kpi.unit ? html`${kpi.unit}` : html``}
${kpi.sub}
`;
}
private renderPageHeader(titleArg: string, descriptionArg: string, actionArg?: TemplateResult): TemplateResult {
return html`
${titleArg}
${descriptionArg}
${actionArg ? html`${actionArg}
` : html``}
`;
}
private renderSectionCard(titleArg: string, descriptionArg: string, contentArg: TemplateResult, actionArg?: TemplateResult): TemplateResult {
return html`
${titleArg || actionArg ? html`
${titleArg}
${descriptionArg ? html`
${descriptionArg}
` : html``}
${actionArg}
` : html``}
${contentArg}
`;
}
private renderFormRow(labelArg: string, hintArg: string, contentArg: TemplateResult, requiredArg = false): TemplateResult {
return html`
`;
}
private renderCodeBlock(valueArg: string): TemplateResult {
return html`${valueArg}
`;
}
private renderOverview(): TemplateResult {
const firstName = this.user?.name?.split(' ')[0] || 'there';
const activeSessionCount = this.sessions.filter((sessionArg) => sessionArg.isCurrent).length || this.sessions.length;
const activePassportCount = this.passportDevices.filter((deviceArg) => deviceArg.status === 'active').length;
const kpis: TKpi[] = [
{
label: 'Organisations',
value: String(this.orgs.length),
delta: 'live',
deltaKind: 'live',
spark: [1, 1, 1, 1, 1, 1, 1, Math.max(this.orgs.length, 1)],
sparkColor: 'var(--idp-spark-info)',
accent: 'var(--idp-chart-1)',
sub: 'Memberships visible to this account',
},
{
label: 'Sessions',
value: String(this.sessions.length),
delta: `${activeSessionCount} active`,
deltaKind: 'up',
spark: [1, 2, 1, 2, 3, 2, Math.max(this.sessions.length, 1)],
sparkColor: 'var(--idp-spark-up)',
accent: 'var(--idp-chart-2)',
sub: this.sessions.length ? `Latest ${this.formatTimeAgo(Math.max(...this.sessions.map((sessionArg) => sessionArg.lastActive)))}` : 'No active sessions loaded',
},
{
label: 'Passport devices',
value: String(this.passportDevices.length),
delta: `${activePassportCount} active`,
deltaKind: 'live',
spark: [1, 1, 1, Math.max(this.passportDevices.length, 1)],
sparkColor: 'var(--idp-spark-info)',
accent: 'var(--idp-chart-5)',
sub: 'Cryptographic credentials',
},
{
label: 'Activity',
value: String(this.activities.length),
delta: 'live',
deltaKind: 'live',
spark: [1, 2, 2, 3, Math.max(this.activities.length, 1)],
sparkColor: 'var(--idp-spark-info)',
accent: 'var(--idp-info)',
sub: 'Recent account events',
},
];
return html`
Account - Overviewlive
Good morning, ${firstName}.
Account-wide operational snapshot using live identity data.
${kpis.map((kpiArg) => this.renderKpi(kpiArg))}
${this.dataLoading || this.dataError ? this.renderDataState('No overview data', 'The console has not received live account data yet.', 'activity') : html``}
Recent activity${this.activities.length} events
${this.activities.length ? this.activities.slice(0, 8).map((activityArg) => html`
${activityArg.action.replace(/_/g, ' ')} - ${activityArg.description}
${this.formatTimeAgo(activityArg.timestamp)}
`) : this.renderStateCard('No activity yet', 'Activity events will appear here after logins, app changes, and organisation updates.', 'activity')}
Current sessions${this.sessions.length} total
${this.sessions.length ? this.sessions.slice(0, 5).map((sessionArg) => html`
${sessionArg.deviceName || 'Unknown device'} - ${sessionArg.browser} ${sessionArg.os}
${this.formatTimeAgo(sessionArg.lastActive)}
`) : this.renderStateCard('No sessions loaded', 'Active session telemetry is unavailable or there are no sessions.', 'monitor')}
`;
}
private renderProfile(): TemplateResult {
const username = this.user?.username || this.user?.email?.split('@')[0] || '';
const status = this.user?.status || 'active';
return html`
${this.renderPageHeader('Profile', 'Your personal identity details visible to connected apps.')}
${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`
${this.userInitials}${this.user?.name || 'Unknown User'}
Profile update endpoints are not exposed in this console yet.
`)}
${this.renderSectionCard('Personal information', '', html`
${this.renderFormRow('Full name', '', html`
`, true)}
${this.renderFormRow('Username', 'Used in your public profile URL', html`
idp.global/user/
`)}
${this.renderFormRow('Email', 'Primary address for login and notifications', html`
`)}
${this.renderFormRow('Mobile number', 'Used for SMS verification', html`
`)}
`)}
${this.renderSectionCard('Account status', '', html`
Status
Your account is currently ${status}.
${status}${this.globalAdmin ? html`
Global admin
You have platform-wide administrative access.
Admin` : html``}`)}
`;
}
private renderSecurity(): TemplateResult {
const passportRows = this.passportDevices.map((deviceArg) => ({
cells: [
html`
${this.getInitials(deviceArg.label)}
${deviceArg.label}
${deviceArg.platform}${deviceArg.appVersion ? ` - ${deviceArg.appVersion}` : ''}
`,
html`${deviceArg.status}`,
[
deviceArg.capabilities?.push ? 'push' : '',
deviceArg.capabilities?.nfc ? 'nfc' : '',
deviceArg.capabilities?.gps ? 'gps' : '',
].filter(Boolean).join(', ') || '-',
deviceArg.lastSeenAt ? this.formatTimeAgo(deviceArg.lastSeenAt) : 'never',
html``,
],
}));
return html`
${this.renderPageHeader('Security', 'Manage how you authenticate and protect your account.')}
${this.credentialMessage ? html`
Credential update
${this.credentialMessage}
` : html``}
${this.credentialError ? html`
Credential error
${this.credentialError}
` : html``}
${this.renderSectionCard('Session security', 'Live session data is available and sessions can be revoked from the device list.', html`
Active sessions
Review and revoke sessions from the Sessions & Devices page.
${this.sessions.length}
`)}
${this.renderSectionCard('Password', 'Change your password using the existing production password endpoint. All fields are required.', html`
${this.renderFormRow('Current password', '', html`
this.setPasswordField('current', eventArg)} />`, true)}
${this.renderFormRow('New password', 'Minimum 12 characters.', html`
this.setPasswordField('new', eventArg)} />`, true)}
${this.renderFormRow('Confirm password', '', html`
this.setPasswordField('confirm', eventArg)} />`, true)}
`)}
Enroll passport device
Creates a signed enrollment challenge for the IDP Passport device flow.
${this.passportEnrollment ? html`
Enrollment challenge ready
Expires ${new Date(this.passportEnrollment.expiresAt).toLocaleString()}. Use the pairing token or payload in a passport client to complete enrollment.
${this.renderFormRow('Pairing token', '', this.renderCodeBlock(this.passportEnrollment.pairingToken))}
${this.renderFormRow('Pairing payload', '', this.renderCodeBlock(this.passportEnrollment.pairingPayload))}
${this.renderFormRow('Signing payload', '', this.renderCodeBlock(this.passportEnrollment.signingPayload))}
` : html`
No active enrollment challenge.
`}
${this.renderStateCard('TOTP controls not connected', 'No TOTP secret, enrollment, or verification endpoints exist in this backend yet, so no fake TOTP toggle is shown.', 'lock')}
${this.renderStateCard('WebAuthn passkeys not connected', 'No WebAuthn passkey credential model or assertion endpoints exist yet. Passport devices are the available cryptographic credential path.', 'key')}
`;
}
private renderSessions(): TemplateResult {
return html`
${this.renderPageHeader('Sessions & Devices', 'Active login sessions across all your devices.')}
${this.sessions.length ? this.sessions.map((sessionArg) => html`
${sessionArg.deviceName || 'Unknown device'}${sessionArg.isCurrent ? html`Current session` : html``}
${sessionArg.browser || 'Unknown browser'} - ${sessionArg.os || 'Unknown OS'} - IP: ${sessionArg.ip || 'unknown'}
Started ${this.formatTimeAgo(sessionArg.createdAt)} - Active ${this.formatTimeAgo(sessionArg.lastActive)}
${sessionArg.isCurrent ? html`` : html`
`}
`) : this.renderDataState('No sessions', 'There are no active sessions for this account or session telemetry is unavailable.', 'monitor')}
`;
}
private renderAccountApps(): TemplateResult {
const apps = this.accountApps;
return html`
${this.renderPageHeader('Connected Apps', 'Third-party apps and services that have OAuth access to your account.')}
${apps.length ? apps.map((appArg) => html`
${appArg.name.slice(0, 2).toUpperCase()}${appArg.name}
${(appArg.scopes || []).map((scopeArg) => html`${scopeArg}`)}
${appArg.description || appArg.appUrl || 'Connected application'}
`) : this.renderDataState('Account app grants not connected', 'No account-level OAuth grant endpoint is wired into this app yet. Organisation app connections are managed under Organisation > OAuth Apps.', 'grid')}
`;
}
private renderOrgGeneral(): TemplateResult {
const org = this.currentOrg;
const connectedAppCount = this.orgApps.filter((appArg) => appArg.isConnected).length;
const orgActivities = this.activities
.filter((activityArg) => {
const searchableText = `${activityArg.targetType || ''} ${activityArg.description || ''}`.toLowerCase();
return Boolean(org.slug && searchableText.includes(org.slug.toLowerCase()))
|| Boolean(org.name && searchableText.includes(org.name.toLowerCase()))
|| searchableText.includes('org')
|| searchableText.includes('organization')
|| searchableText.includes('organisation');
})
.slice(0, 5);
const kpis: TKpi[] = [
{
label: 'Members',
value: String(this.orgMembers.length),
delta: `${this.orgInvitations.length} pending`,
deltaKind: 'up',
spark: [1, 1, 2, Math.max(this.orgMembers.length, 1)],
sparkColor: 'var(--idp-spark-up)',
accent: 'var(--idp-chart-5)',
sub: org.name,
},
{
label: 'Connected apps',
value: String(connectedAppCount),
delta: `${this.orgApps.length} available`,
deltaKind: 'live',
spark: [1, 2, 2, Math.max(connectedAppCount, 1)],
sparkColor: 'var(--idp-spark-info)',
accent: 'var(--idp-info)',
sub: 'Organisation OAuth catalogue',
},
{
label: 'Role',
value: org.myRole || 'member',
delta: 'live',
deltaKind: 'live',
spark: [1, 1, 1, 1],
sparkColor: 'var(--idp-spark-info)',
accent: 'var(--idp-chart-1)',
sub: 'Your access level',
},
{
label: 'Activity',
value: String(orgActivities.length),
delta: `${this.activities.length} account events`,
deltaKind: 'up',
spark: [1, 1, Math.max(orgActivities.length, 1)],
sparkColor: 'var(--idp-spark-up)',
accent: 'var(--idp-chart-2)',
sub: 'Recent org-related events',
},
];
return html`
Organisation - Generallive
${org.name}
Selected organisation dashboard for ${org.slug ? `@${org.slug}` : org.id || 'no organisation selected'}.
${kpis.map((kpiArg) => this.renderKpi(kpiArg))}
${this.dataLoading || this.dataError ? this.renderDataState('No organisation data', 'The console has not received live organisation data yet.', 'building') : html``}
Organisation profile${org.myRole || 'member'}
${org.name.slice(0, 2).toUpperCase()}${org.name}
idp.global/org/${org.slug || 'unassigned'}
${this.renderCodeBlock(org.id || 'No organisation selected')}
Rename, slug updates, ownership transfer, and deletion are available from Settings with server-side audited confirmation.
Recent organisation activity${orgActivities.length} events
${orgActivities.length ? orgActivities.map((activityArg) => html`
${activityArg.action.replace(/_/g, ' ')} - ${activityArg.description}
${this.formatTimeAgo(activityArg.timestamp)}
`) : this.renderStateCard('No org activity yet', 'Organisation-related activity will appear here when the backend reports matching activity events.', 'activity')}
`;
}
private renderOrgSettings(): TemplateResult {
const org = this.currentOrg;
this.syncOrgSettingsState();
const transferCandidates = this.orgMembers.filter((memberArg) => !memberArg.isCurrentUser);
const transferConfirmText = `transfer ${org.slug}`;
const deleteConfirmText = `delete ${org.slug}`;
const identityChanged = this.orgNameDraft.trim() !== org.name || this.orgSlugDraft.trim().toLowerCase() !== org.slug;
return html`
${this.renderPageHeader('Organisation Settings', 'Audited configuration and owner-controlled destructive operations.')}
${this.orgSettingsError ? html`
Settings action blocked
${this.orgSettingsError}
` : html``}
${this.renderSectionCard('Organisation identity', `Type ${org.slug} to confirm name or slug changes. The backend verifies this confirmation before saving.`, html`
${org.name.slice(0, 2).toUpperCase()}${org.name}
idp.global/org/${org.slug}
${this.renderFormRow('Organisation name', '', html`
this.setOrgSettingsField('name', eventArg)} />`, true)}
${this.renderFormRow('URL slug', "Used in your org's public URLs.", html`
idp.global/org/ this.setOrgSettingsField('slug', eventArg)} />
`)}
${this.renderFormRow('Confirmation', `Type ${org.slug}`, html`
this.setOrgSettingsField('settingsConfirmation', eventArg)} />`)}
`)}
${this.renderSectionCard('Organisation ID', 'Use this identifier when making API calls.', this.renderCodeBlock(org.id))}
${this.renderSectionCard('Transfer ownership', `Owner-only operation. Type ${transferConfirmText} to confirm.`, html`
${transferCandidates.length ? html`
${this.renderFormRow('New owner', 'The target user must already be an organisation member.', html`
`, true)}
${this.renderFormRow('Confirmation', `Type ${transferConfirmText}`, html`
this.setOrgSettingsField('transferConfirmation', eventArg)} />`)}
` : this.renderStateCard('No transfer candidates loaded', 'Load organisation members before transferring ownership.', 'users')}
`)}
${this.renderSectionCard('Delete organisation', `Owner-only destructive operation. Type ${deleteConfirmText} to permanently remove this organisation, its memberships, pending invitations, billing records, and app connections.`, html`
${this.renderFormRow('Confirmation', `Type ${deleteConfirmText}`, html`
this.setOrgSettingsField('deleteConfirmation', eventArg)} />`)}
`)}
`;
}
private renderOrgMembers(): TemplateResult {
const customRoleRows = this.orgRoleDefinitions.map((roleArg) => ({
cells: [
html`
${this.getInitials(roleArg.name)}
${roleArg.name}
${roleArg.description || 'Custom organisation role'}
`,
roleArg.key,
html`
`,
],
}));
const memberRows = this.orgMembers.map((memberArg) => ({
cells: [
html`
${this.getInitials(memberArg.name || memberArg.email)}
${memberArg.name || memberArg.email}
${memberArg.email}
`,
html`${memberArg.roles.map((roleArg) => html`${roleArg}`)}
`,
memberArg.isCurrentUser ? html`You` : html`Member`,
memberArg.roles.includes('owner') || memberArg.isCurrentUser
? html``
: html``,
],
}));
const invitationRows = this.orgInvitations.map((inviteArg) => ({
cells: [
html`
${this.getInitials(inviteArg.email)}
${inviteArg.email}
Invited ${this.formatTimeAgo(inviteArg.invitedAt)}
`,
html`${inviteArg.roles.map((roleArg) => html`${roleArg}`)}
`,
new Date(inviteArg.expiresAt).toLocaleDateString(),
html`
`,
],
}));
return html`
${this.renderPageHeader('Members', `${this.orgMembers.length} members - ${this.orgInvitations.length} pending invitations`, html``)}
${this.orgSettingsError ? html`
Role action blocked
${this.orgSettingsError}
` : html``}
${this.orgInvitations.length ? html`
` : html``}
`;
}
private renderOrgApps(): TemplateResult {
const appRows = this.orgApps.map((appArg) => ({
cells: [
html`
${this.getInitials(appArg.name)}
${appArg.name}
${appArg.description || appArg.appUrl || 'Global app'}
`,
appArg.clientId || '-',
html`${(appArg.scopes || []).slice(0, 4).map((scopeArg) => html`${scopeArg}`)}
`,
html`${appArg.isConnected ? 'connected' : 'available'}`,
html`${appArg.roleMappings?.length || 0} mappings`,
html`${appArg.isConnected ? html`` : html``}
`,
],
}));
return html`
${this.renderPageHeader('Apps', "Global apps connected to this organisation.")}
${this.orgSettingsError ? html`
App mapping blocked
${this.orgSettingsError}
` : html``}
`;
}
private renderSupport(): TemplateResult {
const services = [
['Account Recovery', 'Lost access to your account or locked out of your organisation? Our team will verify your identity and restore access securely.', 'EUR149', 'per incident', 'key', '1-2 business days'],
['Organisation Recovery', 'All owners have lost access to your organisation. We can verify ownership and restore admin access.', 'EUR249', 'per incident', 'building', '2-3 business days'],
['Data Export & Migration', 'Full export of your org data - users, sessions, app connections - for migration or compliance.', 'EUR199', 'per request', 'box', '3-5 business days'],
['Identity & SSO Consulting', 'Architecture review, OIDC guidance, and custom SSO setup for your organisation stack.', 'EUR190', 'per hour', 'globe', 'Scheduled session'],
['Security Review', 'Audit of connected apps, active sessions, passkey policies, and role assignments.', 'EUR390', 'per review', 'shield', '5-7 business days'],
];
return html`
${this.renderPageHeader('Support', 'idp.global is free for everyone. Paid options cover hands-on recovery and consulting work.')}
idp.global is free, forever
All platform features - authentication, passkeys, OIDC apps, team management - are included at no cost.
${services.map((serviceArg) => html`
${serviceArg[0]}- ${serviceArg[5]}
${serviceArg[1]}
${serviceArg[2]} ${serviceArg[3]}
`)}
`;
}
private renderGAUsers(): TemplateResult {
return html`${this.renderPageHeader('All Users', 'Platform-wide user administration.')}${this.renderDataState('User directory not connected', 'The shell is ready for global user data, but no platform user-list endpoint is wired into this app yet. No demo users are shown in production mode.', 'users')}
`;
}
private renderGAOrgs(): TemplateResult {
const orgRows = this.orgs.map((orgArg) => ({
cells: [
html`
${this.getInitials(orgArg.name)}
${orgArg.name}
${orgArg.slug ? `idp.global/org/${orgArg.slug}` : 'No slug'}
`,
orgArg.slug || '-',
html`${orgArg.myRole || 'member'}`,
orgArg.id,
],
}));
return html`
${this.renderPageHeader('All Organisations', `${this.orgs.length} organisations visible to this admin session`)}
`;
}
private renderGAApps(): TemplateResult {
const apps = this.adminApps.length ? this.adminApps : this.orgApps;
const appRows = apps.map((appArg) => ({
cells: [
html`
${this.getInitials(appArg.name)}
${appArg.name}
${appArg.description || appArg.appUrl || 'Platform app'}
`,
html`${appArg.type || 'global'}`,
appArg.category || '-',
appArg.connectionCount ?? '-',
html`${appArg.status || 'active'}`,
],
}));
return html`
${this.renderPageHeader('Platform Apps', 'Global and partner apps across the platform.')}
`;
}
private renderMainContent(): TemplateResult {
const renderers: Record TemplateResult> = {
overview: () => this.renderOverview(),
profile: () => this.renderProfile(),
security: () => this.renderSecurity(),
sessions: () => this.renderSessions(),
apps: () => this.renderAccountApps(),
'org-general': () => this.renderOrgGeneral(),
'org-settings': () => this.renderOrgSettings(),
'org-members': () => this.renderOrgMembers(),
'org-apps': () => this.renderOrgApps(),
support: () => this.renderSupport(),
'ga-users': () => this.renderGAUsers(),
'ga-orgs': () => this.renderGAOrgs(),
'ga-apps': () => this.renderGAApps(),
};
return (renderers[this.page] || renderers.overview)();
}
public render(): TemplateResult {
return html`
${this.renderSidebar()}
${this.renderMainContent()}
${this.renderDialog()}
`;
}
}