feat(apps): Add Apps subsystem: App and AppConnection models, managers, typed request handlers, web UI routes and documentation
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@idp.global/idp.global',
|
||||
version: '1.5.0',
|
||||
version: '1.6.0',
|
||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||
}
|
||||
|
||||
@@ -139,6 +139,16 @@ export class IdpAccountContent extends DeesElement {
|
||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
this.subrouter._handleRouteState();
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
|
||||
@@ -214,7 +214,13 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {}}
|
||||
@click=${async () => {
|
||||
const currentState = states.accountState.getState();
|
||||
if (currentState.selectedOrg) {
|
||||
const subrouter = await this.getAccountRouter();
|
||||
subrouter.pushUrl(`/org/${currentState.selectedOrg.data.slug}/apps`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
||||
Apps
|
||||
@@ -235,7 +241,13 @@ export class LeleAccountNavigation extends DeesElement {
|
||||
</div>
|
||||
<div
|
||||
class="navigationOption"
|
||||
@click=${async () => {}}
|
||||
@click=${async () => {
|
||||
const currentState = states.accountState.getState();
|
||||
if (currentState.selectedOrg) {
|
||||
const subrouter = await this.getAccountRouter();
|
||||
subrouter.pushUrl(`/org/${currentState.selectedOrg.data.slug}/billing`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:wallet'}></dees-icon>
|
||||
Billing
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
|
||||
import * as accountState from '../../../states/accountstate.js';
|
||||
import { IdpState } from '../../../states/idp.state.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'lele-accountview-apps': AppsView;
|
||||
}
|
||||
}
|
||||
|
||||
interface IAppDisplay {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
appUrl: string;
|
||||
category: string;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
@customElement('lele-accountview-apps')
|
||||
export class AppsView extends DeesElement {
|
||||
@state()
|
||||
accessor globalApps: IAppDisplay[] = [];
|
||||
|
||||
@state()
|
||||
accessor loading: boolean = true;
|
||||
|
||||
@state()
|
||||
accessor activeTab: 'global' | 'store' | 'custom' = 'global';
|
||||
|
||||
@state()
|
||||
accessor organizationId: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
accountDesignTokens,
|
||||
cardStyles,
|
||||
typographyStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 48px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--foreground);
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--foreground);
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
border-color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: var(--muted);
|
||||
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: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.app-category {
|
||||
font-size: 12px;
|
||||
color: var(--muted-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
font-size: 14px;
|
||||
color: var(--muted-foreground);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.app-link {
|
||||
font-size: 13px;
|
||||
color: var(--muted-foreground);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.app-link:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.app-link dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.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: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.coming-soon dees-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<h1>Apps</h1>
|
||||
<p>Manage apps connected to your organization. Connect global apps, browse the AppStore, or create custom OAuth clients.</p>
|
||||
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="tab ${this.activeTab === 'global' ? 'active' : ''}"
|
||||
@click=${() => this.activeTab = 'global'}
|
||||
>
|
||||
Global Apps
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.activeTab === 'store' ? 'active' : ''}"
|
||||
@click=${() => this.activeTab = 'store'}
|
||||
>
|
||||
App Store
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.activeTab === 'custom' ? 'active' : ''}"
|
||||
@click=${() => this.activeTab = 'custom'}
|
||||
>
|
||||
Custom OIDC
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this.renderTabContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTabContent() {
|
||||
switch (this.activeTab) {
|
||||
case 'global':
|
||||
return this.renderGlobalApps();
|
||||
case 'store':
|
||||
return this.renderAppStore();
|
||||
case 'custom':
|
||||
return this.renderCustomOidc();
|
||||
}
|
||||
}
|
||||
|
||||
private renderGlobalApps() {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="loading">
|
||||
<span>Loading apps...</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.globalApps.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
||||
<h2>No Global Apps Available</h2>
|
||||
<p>There are no global apps configured yet.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="app-grid">
|
||||
${this.globalApps.map(app => html`
|
||||
<div class="app-card">
|
||||
<div class="app-header">
|
||||
<div class="app-logo">
|
||||
${app.logoUrl ? html`<img src="${app.logoUrl}" alt="${app.name}" />` : html`<dees-icon .icon=${'lucide:box'}></dees-icon>`}
|
||||
</div>
|
||||
<div class="app-info">
|
||||
<h3 class="app-name">${app.name}</h3>
|
||||
<span class="app-category">${app.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="app-description">${app.description}</p>
|
||||
<div class="app-actions">
|
||||
<a class="app-link" href="${app.appUrl}" target="_blank">
|
||||
<dees-icon .icon=${'lucide:external-link'}></dees-icon>
|
||||
Visit App
|
||||
</a>
|
||||
<div class="toggle-container">
|
||||
<span class="toggle-label">${app.isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
<dees-input-checkbox
|
||||
.value=${app.isConnected}
|
||||
@change=${(e: CustomEvent) => this.toggleAppConnection(app.id, e.detail)}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAppStore() {
|
||||
return html`
|
||||
<div class="coming-soon">
|
||||
<dees-icon .icon=${'lucide:store'}></dees-icon>
|
||||
<h2>App Store</h2>
|
||||
<p>Browse and install partner apps from other organizations.</p>
|
||||
<p><em>Coming soon in Phase 3</em></p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCustomOidc() {
|
||||
return html`
|
||||
<div class="coming-soon">
|
||||
<dees-icon .icon=${'lucide:key'}></dees-icon>
|
||||
<h2>Custom OIDC Apps</h2>
|
||||
<p>Create and manage your own OAuth/OIDC client applications.</p>
|
||||
<p><em>Coming soon in Phase 2</em></p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.loadApps();
|
||||
}
|
||||
|
||||
private async loadApps() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// Get the organization ID from the URL
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const orgSlug = pathParts[3];
|
||||
|
||||
const currentState = accountState.accountState.getState();
|
||||
const selectedOrg = currentState.organizations.find(org => org.data.slug === orgSlug);
|
||||
|
||||
if (!selectedOrg) {
|
||||
console.error('Organization not found');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.organizationId = selectedOrg.id;
|
||||
|
||||
// Get JWT from IdpState
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
|
||||
// Fetch global apps
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalApps>(
|
||||
'/typedrequest',
|
||||
'getGlobalApps'
|
||||
);
|
||||
|
||||
const appsResponse = await typedRequest.fire({
|
||||
jwt,
|
||||
});
|
||||
|
||||
// Fetch connections for this organization
|
||||
const connectionsRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
||||
'/typedrequest',
|
||||
'getAppConnections'
|
||||
);
|
||||
|
||||
const connectionsResponse = await connectionsRequest.fire({
|
||||
jwt,
|
||||
organizationId: this.organizationId,
|
||||
});
|
||||
|
||||
// Map apps with connection status
|
||||
const connectionMap = new Map(
|
||||
connectionsResponse.connections
|
||||
.filter(c => c.data.status === 'active')
|
||||
.map(c => [c.data.appId, true])
|
||||
);
|
||||
|
||||
this.globalApps = appsResponse.apps.map(app => ({
|
||||
id: app.id,
|
||||
name: app.data.name,
|
||||
description: app.data.description,
|
||||
logoUrl: app.data.logoUrl,
|
||||
appUrl: app.data.appUrl,
|
||||
category: app.data.category,
|
||||
isConnected: connectionMap.has(app.id),
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading apps:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async toggleAppConnection(appId: string, isConnected: boolean) {
|
||||
try {
|
||||
// Get JWT from IdpState
|
||||
const idpState = await IdpState.getSingletonInstance();
|
||||
const jwt = await idpState.idpClient.getJwt();
|
||||
|
||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_ToggleAppConnection>(
|
||||
'/typedrequest',
|
||||
'toggleAppConnection'
|
||||
);
|
||||
|
||||
await typedRequest.fire({
|
||||
jwt,
|
||||
organizationId: this.organizationId,
|
||||
appId: appId,
|
||||
action: isConnected ? 'connect' : 'disconnect',
|
||||
});
|
||||
|
||||
// Update local state
|
||||
this.globalApps = this.globalApps.map(app =>
|
||||
app.id === appId ? { ...app, isConnected } : app
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error toggling app connection:', error);
|
||||
// Revert the checkbox on error
|
||||
await this.loadApps();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './appsview.js';
|
||||
export * from './baseview.js';
|
||||
export * from './orgsetup.js';
|
||||
export * from './paddlesetup.js';
|
||||
|
||||
@@ -21,7 +21,7 @@ declare global {
|
||||
@customElement('idp-transfermanager')
|
||||
export class IdpTransfermanager extends DeesElement {
|
||||
|
||||
public appData: plugins.idpInterfaces.data.IApp;
|
||||
public appData: plugins.idpInterfaces.data.IAppLegacy;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
|
||||
Reference in New Issue
Block a user