2025-12-01 09:44:37 +00:00
|
|
|
import * as plugins from '../../../plugins.js';
|
|
|
|
|
import {
|
|
|
|
|
customElement,
|
|
|
|
|
DeesElement,
|
|
|
|
|
property,
|
|
|
|
|
html,
|
|
|
|
|
cssManager,
|
|
|
|
|
css,
|
|
|
|
|
state,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
|
|
|
|
|
import { IdpState } from '../../../states/idp.state.js';
|
|
|
|
|
import { accountDesignTokens } from '../sharedstyles.js';
|
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'lele-accountview-admin': AdminView;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IAppWithStats {
|
|
|
|
|
app: plugins.idpInterfaces.data.IGlobalApp;
|
|
|
|
|
connectionCount: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@customElement('lele-accountview-admin')
|
|
|
|
|
export class AdminView extends DeesElement {
|
|
|
|
|
@state()
|
|
|
|
|
accessor apps: IAppWithStats[] = [];
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor loading: boolean = true;
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor showCreateDialog: boolean = false;
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor editingApp: plugins.idpInterfaces.data.IGlobalApp | null = null;
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor newClientSecret: string | null = null;
|
|
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
accountDesignTokens,
|
|
|
|
|
css`
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
background: var(--background);
|
|
|
|
|
color: var(--foreground);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 32px 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin: 0;
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
color: #71717a;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
background: #18181b;
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #71717a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apps-section {
|
|
|
|
|
background: #18181b;
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
border-bottom: 1px solid #27272a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-list {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
border-bottom: 1px solid #27272a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-item:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-logo {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
background: #27272a;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-logo img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-logo dees-icon {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-name {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-details {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #71717a;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-status {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
border-radius: 9999px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-status.active {
|
|
|
|
|
background: rgba(34, 197, 94, 0.1);
|
|
|
|
|
color: #22c55e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-status.inactive {
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: #fafafa;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn:hover {
|
|
|
|
|
background: #27272a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.danger:hover {
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
border-color: #ef4444;
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 48px;
|
|
|
|
|
color: #71717a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-state dees-icon {
|
|
|
|
|
font-size: 48px;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 48px;
|
|
|
|
|
color: #71717a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Dialog styles */
|
|
|
|
|
.dialog-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog {
|
|
|
|
|
background: #18181b;
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: 520px;
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-header {
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
border-bottom: 1px solid #27272a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-body {
|
|
|
|
|
padding: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dialog-footer {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
border-top: 1px solid #27272a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
color: #a1a1aa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
color: #fafafa;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: #3b82f6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-textarea {
|
|
|
|
|
min-height: 80px;
|
|
|
|
|
resize: vertical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.secret-display {
|
|
|
|
|
background: #0a0a0a;
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.secret-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #71717a;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.secret-value {
|
|
|
|
|
font-family: 'Geist Mono', monospace;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
color: #22c55e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.secret-warning {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #f59e0b;
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="header">
|
|
|
|
|
<div>
|
|
|
|
|
<h1>Global Admin</h1>
|
|
|
|
|
<p class="subtitle">Manage platform-wide settings and global apps</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="stats-row">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-value">${this.apps.length}</div>
|
|
|
|
|
<div class="stat-label">Total Global Apps</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-value">${this.apps.filter(a => a.app.data.isActive).length}</div>
|
|
|
|
|
<div class="stat-label">Active Apps</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-value">${this.apps.reduce((sum, a) => sum + a.connectionCount, 0)}</div>
|
|
|
|
|
<div class="stat-label">Total Connections</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="apps-section">
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
<span class="section-title">Global Apps</span>
|
|
|
|
|
<dees-button
|
|
|
|
|
@clicked=${() => this.showCreateDialog = true}
|
|
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:plus'} slot="iconLeft"></dees-icon>
|
|
|
|
|
Create App
|
|
|
|
|
</dees-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${this.loading ? this.renderLoading() : this.renderAppList()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${this.showCreateDialog ? this.renderCreateDialog() : null}
|
|
|
|
|
${this.editingApp ? this.renderEditDialog() : null}
|
|
|
|
|
${this.newClientSecret ? this.renderSecretDialog() : null}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderLoading(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<span>Loading apps...</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderAppList(): TemplateResult {
|
|
|
|
|
if (this.apps.length === 0) {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
|
|
|
|
<h3>No Global Apps</h3>
|
|
|
|
|
<p>Create your first global app to get started.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div class="app-list">
|
|
|
|
|
${this.apps.map(({ app, connectionCount }) => html`
|
|
|
|
|
<div class="app-item">
|
|
|
|
|
<div class="app-logo">
|
|
|
|
|
${app.data.logoUrl
|
|
|
|
|
? html`<img src="${app.data.logoUrl}" alt="${app.data.name}" />`
|
|
|
|
|
: html`<dees-icon .icon=${'lucide:box'}></dees-icon>`
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="app-info">
|
|
|
|
|
<div class="app-name">${app.data.name}</div>
|
|
|
|
|
<div class="app-details">
|
|
|
|
|
<span>${app.data.category}</span>
|
|
|
|
|
<span>${connectionCount} connections</span>
|
|
|
|
|
<span>${app.data.appUrl}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="app-status ${app.data.isActive ? 'active' : 'inactive'}">
|
|
|
|
|
${app.data.isActive ? 'Active' : 'Inactive'}
|
|
|
|
|
</span>
|
|
|
|
|
<div class="app-actions">
|
|
|
|
|
<button class="action-btn" @click=${() => this.editingApp = app}>
|
|
|
|
|
Edit
|
|
|
|
|
</button>
|
|
|
|
|
<button class="action-btn" @click=${() => this.regenerateCredentials(app.id)}>
|
|
|
|
|
Regenerate
|
|
|
|
|
</button>
|
|
|
|
|
<button class="action-btn danger" @click=${() => this.deleteApp(app.id)}>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`)}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderCreateDialog(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="dialog-overlay" @click=${(e: Event) => {
|
|
|
|
|
if ((e.target as HTMLElement).classList.contains('dialog-overlay')) {
|
|
|
|
|
this.showCreateDialog = false;
|
|
|
|
|
}
|
|
|
|
|
}}>
|
|
|
|
|
<div class="dialog">
|
|
|
|
|
<div class="dialog-header">
|
|
|
|
|
<h2 class="dialog-title">Create Global App</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-body">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">App Name</label>
|
|
|
|
|
<input type="text" class="form-input" id="app-name" placeholder="e.g., foss.global" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Description</label>
|
|
|
|
|
<textarea class="form-input form-textarea" id="app-description" placeholder="Describe what this app does..."></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">App URL</label>
|
|
|
|
|
<input type="url" class="form-input" id="app-url" placeholder="https://app.example.com" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Logo URL</label>
|
|
|
|
|
<input type="url" class="form-input" id="app-logo" placeholder="https://example.com/logo.png" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Category</label>
|
|
|
|
|
<input type="text" class="form-input" id="app-category" placeholder="e.g., Productivity" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Redirect URIs (comma-separated)</label>
|
|
|
|
|
<input type="text" class="form-input" id="app-redirects" placeholder="https://app.example.com/callback" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Allowed Scopes (comma-separated)</label>
|
|
|
|
|
<input type="text" class="form-input" id="app-scopes" placeholder="openid, profile, email" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
<dees-button type="secondary" @clicked=${() => this.showCreateDialog = false}>
|
|
|
|
|
Cancel
|
|
|
|
|
</dees-button>
|
|
|
|
|
<dees-button @clicked=${this.createApp}>
|
|
|
|
|
Create App
|
|
|
|
|
</dees-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderEditDialog(): TemplateResult {
|
|
|
|
|
const app = this.editingApp!;
|
|
|
|
|
return html`
|
|
|
|
|
<div class="dialog-overlay" @click=${(e: Event) => {
|
|
|
|
|
if ((e.target as HTMLElement).classList.contains('dialog-overlay')) {
|
|
|
|
|
this.editingApp = null;
|
|
|
|
|
}
|
|
|
|
|
}}>
|
|
|
|
|
<div class="dialog">
|
|
|
|
|
<div class="dialog-header">
|
|
|
|
|
<h2 class="dialog-title">Edit ${app.data.name}</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-body">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">App Name</label>
|
|
|
|
|
<input type="text" class="form-input" id="edit-name" .value=${app.data.name} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Description</label>
|
|
|
|
|
<textarea class="form-input form-textarea" id="edit-description">${app.data.description}</textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">App URL</label>
|
|
|
|
|
<input type="url" class="form-input" id="edit-url" .value=${app.data.appUrl} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Logo URL</label>
|
|
|
|
|
<input type="url" class="form-input" id="edit-logo" .value=${app.data.logoUrl} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Category</label>
|
|
|
|
|
<input type="text" class="form-input" id="edit-category" .value=${app.data.category} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Status</label>
|
|
|
|
|
<dees-input-checkbox
|
|
|
|
|
.label=${'App is active'}
|
|
|
|
|
.value=${app.data.isActive}
|
|
|
|
|
id="edit-active"
|
|
|
|
|
></dees-input-checkbox>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
<dees-button type="secondary" @clicked=${() => this.editingApp = null}>
|
|
|
|
|
Cancel
|
|
|
|
|
</dees-button>
|
|
|
|
|
<dees-button @clicked=${this.updateApp}>
|
|
|
|
|
Save Changes
|
|
|
|
|
</dees-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderSecretDialog(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="dialog-overlay" @click=${(e: Event) => {
|
|
|
|
|
if ((e.target as HTMLElement).classList.contains('dialog-overlay')) {
|
|
|
|
|
this.newClientSecret = null;
|
|
|
|
|
}
|
|
|
|
|
}}>
|
|
|
|
|
<div class="dialog">
|
|
|
|
|
<div class="dialog-header">
|
|
|
|
|
<h2 class="dialog-title">Client Secret Generated</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-body">
|
|
|
|
|
<p>Your new client secret has been generated. Copy it now - you won't be able to see it again.</p>
|
|
|
|
|
<div class="secret-display">
|
|
|
|
|
<div class="secret-label">Client Secret</div>
|
|
|
|
|
<div class="secret-value">${this.newClientSecret}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="secret-warning">
|
|
|
|
|
<dees-icon .icon=${'lucide:alert-triangle'}></dees-icon>
|
|
|
|
|
This secret will only be shown once. Store it securely.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
<dees-button @clicked=${() => {
|
|
|
|
|
navigator.clipboard.writeText(this.newClientSecret!);
|
|
|
|
|
}}>
|
|
|
|
|
Copy to Clipboard
|
|
|
|
|
</dees-button>
|
|
|
|
|
<dees-button type="secondary" @clicked=${() => this.newClientSecret = null}>
|
|
|
|
|
Close
|
|
|
|
|
</dees-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async firstUpdated() {
|
|
|
|
|
await this.loadApps();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadApps() {
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
|
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
|
|
2025-12-04 17:45:40 +00:00
|
|
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>(
|
2025-12-01 09:44:37 +00:00
|
|
|
'getGlobalAppStats'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await typedRequest.fire({ jwt });
|
|
|
|
|
this.apps = response?.apps ?? [];
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading apps:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async createApp() {
|
|
|
|
|
const nameInput = this.shadowRoot!.querySelector('#app-name') as HTMLInputElement;
|
|
|
|
|
const descInput = this.shadowRoot!.querySelector('#app-description') as HTMLTextAreaElement;
|
|
|
|
|
const urlInput = this.shadowRoot!.querySelector('#app-url') as HTMLInputElement;
|
|
|
|
|
const logoInput = this.shadowRoot!.querySelector('#app-logo') as HTMLInputElement;
|
|
|
|
|
const categoryInput = this.shadowRoot!.querySelector('#app-category') as HTMLInputElement;
|
|
|
|
|
const redirectsInput = this.shadowRoot!.querySelector('#app-redirects') as HTMLInputElement;
|
|
|
|
|
const scopesInput = this.shadowRoot!.querySelector('#app-scopes') as HTMLInputElement;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
|
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
|
|
2025-12-04 17:45:40 +00:00
|
|
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateGlobalApp>(
|
2025-12-01 09:44:37 +00:00
|
|
|
'createGlobalApp'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await typedRequest.fire({
|
|
|
|
|
jwt,
|
|
|
|
|
name: nameInput.value,
|
|
|
|
|
description: descInput.value,
|
|
|
|
|
appUrl: urlInput.value,
|
|
|
|
|
logoUrl: logoInput.value,
|
|
|
|
|
category: categoryInput.value,
|
|
|
|
|
redirectUris: redirectsInput.value.split(',').map(s => s.trim()).filter(Boolean),
|
|
|
|
|
allowedScopes: scopesInput.value.split(',').map(s => s.trim()).filter(Boolean),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.showCreateDialog = false;
|
|
|
|
|
this.newClientSecret = response.clientSecret;
|
|
|
|
|
await this.loadApps();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error creating app:', error);
|
|
|
|
|
alert('Failed to create app');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async updateApp() {
|
|
|
|
|
const app = this.editingApp!;
|
|
|
|
|
const nameInput = this.shadowRoot!.querySelector('#edit-name') as HTMLInputElement;
|
|
|
|
|
const descInput = this.shadowRoot!.querySelector('#edit-description') as HTMLTextAreaElement;
|
|
|
|
|
const urlInput = this.shadowRoot!.querySelector('#edit-url') as HTMLInputElement;
|
|
|
|
|
const logoInput = this.shadowRoot!.querySelector('#edit-logo') as HTMLInputElement;
|
|
|
|
|
const categoryInput = this.shadowRoot!.querySelector('#edit-category') as HTMLInputElement;
|
|
|
|
|
const activeCheckbox = this.shadowRoot!.querySelector('#edit-active') as any;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
|
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
|
|
2025-12-04 17:45:40 +00:00
|
|
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateGlobalApp>(
|
2025-12-01 09:44:37 +00:00
|
|
|
'updateGlobalApp'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await typedRequest.fire({
|
|
|
|
|
jwt,
|
|
|
|
|
appId: app.id,
|
|
|
|
|
updates: {
|
|
|
|
|
name: nameInput.value,
|
|
|
|
|
description: descInput.value,
|
|
|
|
|
appUrl: urlInput.value,
|
|
|
|
|
logoUrl: logoInput.value,
|
|
|
|
|
category: categoryInput.value,
|
|
|
|
|
isActive: activeCheckbox.value,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.editingApp = null;
|
|
|
|
|
await this.loadApps();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error updating app:', error);
|
|
|
|
|
alert('Failed to update app');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async regenerateCredentials(appId: string) {
|
|
|
|
|
if (!confirm('Are you sure you want to regenerate credentials? The current credentials will stop working.')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
|
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
|
|
2025-12-04 17:45:40 +00:00
|
|
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RegenerateAppCredentials>(
|
2025-12-01 09:44:37 +00:00
|
|
|
'regenerateAppCredentials'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await typedRequest.fire({ jwt, appId });
|
|
|
|
|
this.newClientSecret = response.clientSecret;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error regenerating credentials:', error);
|
|
|
|
|
alert('Failed to regenerate credentials');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteApp(appId: string) {
|
|
|
|
|
if (!confirm('Are you sure you want to delete this app? All organizations will be disconnected.')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
|
|
|
const jwt = await idpState.idpClient.getJwt();
|
|
|
|
|
|
2025-12-04 17:45:40 +00:00
|
|
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteGlobalApp>(
|
2025-12-01 09:44:37 +00:00
|
|
|
'deleteGlobalApp'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const response = await typedRequest.fire({ jwt, appId });
|
|
|
|
|
|
|
|
|
|
if (response.disconnectedOrganizations > 0) {
|
|
|
|
|
alert(`App deleted. ${response.disconnectedOrganizations} organizations were disconnected.`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.loadApps();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error deleting app:', error);
|
|
|
|
|
alert('Failed to delete app');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|