feat(admin): Add global admin functionality: backend admin APIs, model fields and UI integration
This commit is contained in:
@@ -0,0 +1,759 @@
|
||||
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();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>(
|
||||
'/typedrequest',
|
||||
'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();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_CreateGlobalApp>(
|
||||
'/typedrequest',
|
||||
'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();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_UpdateGlobalApp>(
|
||||
'/typedrequest',
|
||||
'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();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_RegenerateAppCredentials>(
|
||||
'/typedrequest',
|
||||
'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();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_DeleteGlobalApp>(
|
||||
'/typedrequest',
|
||||
'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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './adminview.js';
|
||||
export * from './appsview.js';
|
||||
export * from './baseview.js';
|
||||
export * from './orgsetup.js';
|
||||
|
||||
Reference in New Issue
Block a user