feat(apps): Add Apps subsystem: App and AppConnection models, managers, typed request handlers, web UI routes and documentation

This commit is contained in:
2025-12-01 09:18:48 +00:00
parent f54588e877
commit 6b04c529da
28 changed files with 1491 additions and 21 deletions
+13
View File
@@ -1,5 +1,18 @@
# Changelog # Changelog
## 2025-12-01 - 1.6.0 - feat(apps)
Add Apps subsystem: App and AppConnection models, managers, typed request handlers, web UI routes and documentation
- Introduce App and AppConnection SmartData models (ts/reception/classes.app.ts, ts/reception/classes.appconnection.ts)
- Add AppManager and AppConnectionManager with typed handlers for getGlobalApps, getAppConnections and toggleAppConnection (ts/reception/classes.appmanager.ts, ts/reception/classes.appconnectionmanager.ts)
- Add request and data interfaces for apps and app connections (ts_interfaces/data/loint-reception.app.ts, ts_interfaces/data/loint-reception.appconnection.ts, ts_interfaces/request/loint-reception.app.ts)
- Seed default global apps and support OAuth credential shape (IOAuthCredentials) in app data
- Wire App managers into Reception (ts/reception/classes.reception.ts) and Reception startup
- Update idp client types to use legacy app shape where required (IAppLegacy) and adapt typed requests (ts_idpclient/*)
- Expose web UI routes and navigation for organization Apps view and export the AppsView (ts_web/elements/account/*, ts_web/elements/account/views/index.ts)
- Add registration of new stories for Apps feature (stories/*: ORG-009, ORG-010, ORG-011, DEV-008) and update story index
- Adjust typed request shapes for login/transfer flows to accept IAppLegacy where transfer/app data is exchanged
## 2025-12-01 - 1.5.0 - feat(account) ## 2025-12-01 - 1.5.0 - feat(account)
Refactor account UI styles into reusable design tokens, apply updated styles across views and fix login submit behavior Refactor account UI styles into reusable design tokens, apply updated styles across views and fix login submit behavior
+9 -5
View File
@@ -7,8 +7,8 @@ This directory contains user stories for the idp.global Identity Provider platfo
``` ```
stories/ stories/
├── end-user/ # Stories for regular users (8) ├── end-user/ # Stories for regular users (8)
├── organization-owner/ # Stories for organization admins (8) ├── organization-owner/ # Stories for organization admins (11)
├── developer/ # Stories for API/SDK consumers (7) ├── developer/ # Stories for API/SDK consumers (8)
└── admin/ # Stories for platform administrators (7) └── admin/ # Stories for platform administrators (7)
``` ```
@@ -37,6 +37,9 @@ stories/
| ORG-006 | [Configure SSO for Organization](organization-owner/ORG-006-sso-config.md) | High | New | | ORG-006 | [Configure SSO for Organization](organization-owner/ORG-006-sso-config.md) | High | New |
| ORG-007 | [View Organization Audit Logs](organization-owner/ORG-007-audit-logs.md) | Medium | New | | ORG-007 | [View Organization Audit Logs](organization-owner/ORG-007-audit-logs.md) | Medium | New |
| ORG-008 | [Manage Subscription and Billing](organization-owner/ORG-008-subscription-management.md) | Medium | Enhance | | ORG-008 | [Manage Subscription and Billing](organization-owner/ORG-008-subscription-management.md) | Medium | Enhance |
| ORG-009 | [Connect Global Apps](organization-owner/ORG-009-global-apps.md) | High | New |
| ORG-010 | [Browse and Install Partner Apps](organization-owner/ORG-010-app-store.md) | Medium | New |
| ORG-011 | [Create Custom OIDC Apps](organization-owner/ORG-011-custom-oidc-apps.md) | Medium | New |
### Developer (DEV) ### Developer (DEV)
| ID | Title | Priority | Source | | ID | Title | Priority | Source |
@@ -48,6 +51,7 @@ stories/
| DEV-005 | [Register OAuth Client App](developer/DEV-005-oauth-client.md) | Medium | New | | DEV-005 | [Register OAuth Client App](developer/DEV-005-oauth-client.md) | Medium | New |
| DEV-006 | [Understand API Rate Limits](developer/DEV-006-rate-limiting.md) | Low | New | | DEV-006 | [Understand API Rate Limits](developer/DEV-006-rate-limiting.md) | Low | New |
| DEV-007 | [Validate JWTs in My Application](developer/DEV-007-jwt-validation.md) | Medium | Enhance | | DEV-007 | [Validate JWTs in My Application](developer/DEV-007-jwt-validation.md) | Medium | Enhance |
| DEV-008 | [Submit App to AppStore](developer/DEV-008-submit-partner-app.md) | Low | New |
### Platform Admin (ADM) ### Platform Admin (ADM)
| ID | Title | Priority | Source | | ID | Title | Priority | Source |
@@ -65,9 +69,9 @@ stories/
| Priority | Count | Stories | | Priority | Count | Stories |
|----------|-------|---------| |----------|-------|---------|
| Critical | 3 | EU-002, ORG-002, ADM-001 | | Critical | 3 | EU-002, ORG-002, ADM-001 |
| High | 10 | EU-001, EU-004, ORG-001, ORG-003, ORG-006, DEV-001, DEV-002, DEV-004, ADM-002, ADM-003 | | High | 11 | EU-001, EU-004, ORG-001, ORG-003, ORG-006, ORG-009, DEV-001, DEV-002, DEV-004, ADM-002, ADM-003 |
| Medium | 12 | EU-003, EU-005, EU-006, ORG-004, ORG-005, ORG-007, ORG-008, DEV-003, DEV-005, DEV-007, ADM-004, ADM-005, ADM-007 | | Medium | 14 | EU-003, EU-005, EU-006, ORG-004, ORG-005, ORG-007, ORG-008, ORG-010, ORG-011, DEV-003, DEV-005, DEV-007, ADM-004, ADM-005, ADM-007 |
| Low | 5 | EU-007, EU-008, DEV-006, ADM-006 | | Low | 6 | EU-007, EU-008, DEV-006, DEV-008, ADM-006 |
## Source Legend ## Source Legend
@@ -0,0 +1,130 @@
# Manage Global Apps
**ID:** ADM-008
**Priority:** High
**Status:** In Development
**Phase:** 1
## User Story
As a global administrator, I want to create, configure, and manage first-party global apps (foss.global, task.vc, etc.) so that organization owners can connect to these integrated services.
## Acceptance Criteria
- [ ] Only users with `isGlobalAdmin: true` can access the admin page
- [ ] View list of all global apps with their status
- [ ] Create new global apps with OAuth credentials
- [ ] Edit existing global app details (name, description, logo, URLs)
- [ ] Activate/deactivate global apps (inactive apps hidden from org owners)
- [ ] View connection statistics per app (how many orgs connected)
- [ ] Regenerate OAuth client credentials for an app
- [ ] Delete global apps (with confirmation and impact warning)
- [ ] Admin page accessible at `/admin` route
## Technical Notes
- Global admin flag stored on user: `isGlobalAdmin: boolean`
- Separate from organization roles (platform-level permission)
- OAuth credentials generated server-side, secrets never exposed in full
- App deletion should warn about existing connections
- Audit logging for all admin actions
## Data Model
```typescript
interface IUser {
id: string;
data: {
// ... existing fields ...
isGlobalAdmin?: boolean; // Platform-level admin flag
};
}
interface IGlobalApp {
id: string;
type: 'global';
data: {
name: string;
description: string;
logoUrl: string;
appUrl: string;
oauthCredentials: IOAuthCredentials;
isActive: boolean;
category: string;
createdAt: number;
createdByUserId: string;
};
}
```
## Request Interfaces
```typescript
interface IReq_CreateGlobalApp {
method: 'createGlobalApp';
request: {
jwt: string;
name: string;
description: string;
logoUrl: string;
appUrl: string;
category: string;
redirectUris: string[];
allowedScopes: string[];
};
response: {
app: IGlobalApp;
clientSecret: string; // Only shown once on creation
};
}
interface IReq_UpdateGlobalApp {
method: 'updateGlobalApp';
request: {
jwt: string;
appId: string;
updates: Partial<IGlobalApp['data']>;
};
response: {
app: IGlobalApp;
};
}
interface IReq_DeleteGlobalApp {
method: 'deleteGlobalApp';
request: {
jwt: string;
appId: string;
};
response: {
success: boolean;
disconnectedOrganizations: number;
};
}
interface IReq_GetGlobalAppStats {
method: 'getGlobalAppStats';
request: {
jwt: string;
};
response: {
apps: Array<{
app: IGlobalApp;
connectionCount: number;
}>;
};
}
```
## UI Components
- **GlobalAdminView** (`/admin`) - Main admin dashboard
- **Global Apps Tab** - List of global apps with CRUD operations
- **Create/Edit App Dialog** - Form for app configuration
- Navigation shows "Admin" link only for global admins
## Security Considerations
- Server-side validation of `isGlobalAdmin` flag on all admin endpoints
- JWT must be validated and user's admin status checked
- Rate limiting on credential regeneration
- Audit trail for all changes
## Related Stories
- ORG-009: Connect Global Apps (organization perspective)
- ADM-003: Platform-wide Audit Logging
+19 -3
View File
@@ -19,10 +19,26 @@ As a developer, I want to properly register my application with a unique App ID
## Technical Notes ## Technical Notes
- Current client has `id: ''` placeholder (TODO in code) - Current client has `id: ''` placeholder (TODO in code)
- Need Application model in database - App ID is now part of the unified Apps model (`IApp` discriminated union)
- App credentials similar to OAuth client credentials - Three app types exist: Global Apps, Partner Apps, Custom OIDC Apps
- For custom applications, use the Custom OIDC Apps flow (ORG-011)
- App credentials stored as `IOAuthCredentials` with hashed client secret
- Validate redirect URIs to prevent open redirector attacks - Validate redirect URIs to prevent open redirector attacks
- App ID should be included in JWT claims - App ID/Client ID is included in JWT claims
## Apps Architecture
The Apps system supports three types:
1. **Global Apps** (ORG-009) - First-party platform apps (foss.global, task.vc)
2. **Partner Apps** (ORG-010, DEV-008) - AppStore model for third-party apps
3. **Custom OIDC Apps** (ORG-011) - Organization-created OAuth/OIDC clients
## Related Stories
- ORG-009: Connect Global Apps
- ORG-010: Browse and Install Partner Apps
- ORG-011: Create Custom OIDC Apps
- DEV-005: Register OAuth Client App
- DEV-008: Submit App to AppStore
## Related TODOs ## Related TODOs
- `ts_idpclient/classes.idpclient.ts:30` - `id: '', // TODO` - `ts_idpclient/classes.idpclient.ts:30` - `id: '', // TODO`
+26 -3
View File
@@ -18,11 +18,34 @@ As a developer, I want to register my application as an OAuth client so that use
- [ ] Client credentials flow for server-to-server - [ ] Client credentials flow for server-to-server
## Technical Notes ## Technical Notes
- OAuth keywords in package.json suggest this is planned - OAuth/OIDC client registration is now part of the Apps system
- Implement OAuth 2.0 authorization server endpoints - **For organization owners**: Use Custom OIDC Apps (ORG-011) to create OAuth clients
- **For third-party developers**: Submit to AppStore (DEV-008) for public apps
- Standard OAuth 2.0 / OpenID Connect flows supported
- Scopes: openid, profile, email, organizations - Scopes: openid, profile, email, organizations
- Consider OpenID Connect for identity layer
- PKCE is required for mobile and SPA security - PKCE is required for mobile and SPA security
## Implementation Path
This story's functionality is now implemented through:
1. **Custom OIDC Apps** (ORG-011) - Create org-specific OAuth clients via the Apps UI
2. **Partner Apps** (DEV-008) - Submit public apps to the AppStore
Both use the same underlying `IOAuthCredentials` model:
```typescript
interface IOAuthCredentials {
clientId: string;
clientSecretHash: string;
redirectUris: string[];
allowedScopes: string[];
grantTypes: ('authorization_code' | 'client_credentials' | 'refresh_token')[];
}
```
## Related Stories
- ORG-011: Create Custom OIDC Apps (primary implementation)
- DEV-004: Proper App ID Initialization
- DEV-008: Submit App to AppStore
## Related TODOs ## Related TODOs
- New feature - OAuth server implementation - New feature - OAuth server implementation
@@ -0,0 +1,70 @@
# Submit App to AppStore
**ID:** DEV-008
**Priority:** Low
**Status:** Planned
**Phase:** 4
## User Story
As a developer, I want to submit my application to the AppStore so that other organizations can discover and install my app.
## Acceptance Criteria
- [ ] Submit a new app to the AppStore
- [ ] Provide app name, description, and logo
- [ ] Add screenshots for the store listing
- [ ] Select app category and tags
- [ ] Set pricing model (free, paid, freemium)
- [ ] Configure OAuth credentials (redirect URIs, scopes)
- [ ] Submit for review
- [ ] View submission status (draft, pending_review, approved, rejected, suspended)
- [ ] Receive notification on approval/rejection
- [ ] Edit app listing after approval
- [ ] View app analytics (install count, usage)
## Technical Notes
- Submitter organization becomes `ownerOrganizationId`
- Apps start in `draft` status, move to `pending_review` on submit
- Platform admins review and approve/reject apps
- Approved apps become visible in the AppStore
- App updates may require re-approval
## Approval Workflow
```
draft → pending_review → approved → published
↘ rejected
approved ↔ suspended (admin action)
```
## Data Model
```typescript
interface IPartnerApp {
id: string;
type: 'partner';
data: {
ownerOrganizationId: string;
appStoreMetadata: {
shortDescription: string;
longDescription: string;
screenshots: string[];
category: string;
tags: string[];
pricing: { model: 'free' | 'paid' | 'freemium' };
};
approvalStatus: 'draft' | 'pending_review' | 'approved' | 'rejected' | 'suspended';
isPublished: boolean;
installCount: number;
// ... other fields
};
}
```
## UI Components
- **AppSubmissionView** (`/account/org/:orgName/apps/submit`) - Submit new partner app form
## Related Stories
- ORG-010: Browse and Install Partner Apps
- ORG-011: Create Custom OIDC Apps
- ADM-008: Review Partner App Submissions (new admin story)
@@ -0,0 +1,65 @@
# Connect Global Apps
**ID:** ORG-009
**Priority:** High
**Status:** In Development
**Phase:** 1
## User Story
As an organization owner, I want to connect and disconnect first-party apps (foss.global, task.vc, etc.) for my organization so that my team members can use these integrated services.
## Acceptance Criteria
- [ ] View list of available global apps (foss.global, task.vc)
- [ ] See connection status for each global app
- [ ] Connect a global app to the organization
- [ ] Disconnect a global app from the organization
- [ ] View which user connected the app and when
- [ ] See what scopes/permissions each app requires
- [ ] Toggle does not require page reload
## Technical Notes
- Global apps are pre-registered by the platform administrators
- Uses `IAppConnection` to track org-to-app relationships
- Connection creates OAuth authorization for the app
- Apps access org data via granted scopes
- No credentials shown to org owners (managed by platform)
## Data Model
```typescript
interface IGlobalApp {
id: string;
type: 'global';
data: {
name: string;
description: string;
logoUrl: string;
appUrl: string;
oauthCredentials: IOAuthCredentials;
isActive: boolean;
category: string;
};
}
interface IAppConnection {
id: string;
data: {
organizationId: string;
appId: string;
appType: 'global' | 'partner' | 'custom_oidc';
status: 'active' | 'disconnected';
connectedAt: number;
connectedByUserId: string;
grantedScopes: string[];
};
}
```
## UI Components
- **AppsView** (`/account/org/:orgName/apps`) - Main tabbed interface
- **Global Apps Tab** - List of global apps with toggle switches
## Related Stories
- ORG-010: Browse and Install Partner Apps (AppStore)
- ORG-011: Create Custom OIDC Apps
- DEV-004: Proper App ID Initialization
@@ -0,0 +1,63 @@
# Browse and Install Partner Apps
**ID:** ORG-010
**Priority:** Medium
**Status:** Planned
**Phase:** 3
## User Story
As an organization owner, I want to browse and install partner apps from the AppStore so that my organization can benefit from third-party integrations.
## Acceptance Criteria
- [ ] Browse available partner apps in the AppStore
- [ ] Search apps by name or description
- [ ] Filter apps by category
- [ ] View curated sections (Featured, Popular, New)
- [ ] View app details (description, screenshots, pricing)
- [ ] See app install count and ratings
- [ ] Install/connect a partner app to the organization
- [ ] Uninstall/disconnect a partner app
- [ ] View installed apps list
## Technical Notes
- Partner apps are submitted by other organizations (DEV-008)
- Apps must be approved by platform admins before appearing in store
- Uses `IPartnerApp` with `appStoreMetadata`
- Connection uses same `IAppConnection` as global apps
## Data Model
```typescript
interface IPartnerApp {
id: string;
type: 'partner';
data: {
name: string;
description: string;
logoUrl: string;
appUrl: string;
ownerOrganizationId: string;
oauthCredentials: IOAuthCredentials;
appStoreMetadata: {
shortDescription: string;
longDescription: string;
screenshots: string[];
category: string;
tags: string[];
pricing: { model: 'free' | 'paid' | 'freemium' };
};
approvalStatus: TAppApprovalStatus;
isPublished: boolean;
installCount: number;
};
}
```
## UI Components
- **AppsView** - App Store tab with search and categories
- **AppStoreDetailView** (`/account/org/:orgName/apps/store/:appId`) - Full app details page
## Related Stories
- ORG-009: Connect Global Apps
- ORG-011: Create Custom OIDC Apps
- DEV-008: Submit App to AppStore
@@ -0,0 +1,70 @@
# Create Custom OIDC Apps
**ID:** ORG-011
**Priority:** Medium
**Status:** Planned
**Phase:** 2
## User Story
As an organization owner, I want to create custom OAuth/OIDC client applications so that I can integrate my own internal tools and services with the identity provider.
## Acceptance Criteria
- [ ] Create a new custom OIDC application
- [ ] Configure application name and description
- [ ] Upload application logo
- [ ] Set application URL
- [ ] Configure redirect URIs
- [ ] Select allowed OAuth scopes
- [ ] Choose grant types (authorization_code, client_credentials, refresh_token)
- [ ] View client ID and client secret
- [ ] Regenerate client secret if compromised
- [ ] Edit existing applications
- [ ] Delete applications
- [ ] Configure token lifetimes
## Technical Notes
- Custom OIDC apps are organization-scoped
- Client secret is hashed in database, shown only once at creation
- Redirect URIs validated to prevent open redirect attacks
- Standard OAuth 2.0 / OpenID Connect flows supported
- PKCE support for public clients
## Data Model
```typescript
interface ICustomOidcApp {
id: string;
type: 'custom_oidc';
data: {
name: string;
description: string;
logoUrl: string;
appUrl: string;
ownerOrganizationId: string;
oauthCredentials: IOAuthCredentials;
oidcSettings: {
accessTokenLifetime: number; // seconds
refreshTokenLifetime: number; // seconds
};
};
}
interface IOAuthCredentials {
clientId: string;
clientSecretHash: string;
redirectUris: string[];
allowedScopes: string[];
grantTypes: ('authorization_code' | 'client_credentials' | 'refresh_token')[];
}
```
## UI Components
- **AppsView** - Custom OIDC tab with app list
- **OidcAppFormView** (`/account/org/:orgName/apps/custom/new`) - Create new app form
- **OidcAppFormView** (`/account/org/:orgName/apps/custom/:appId`) - Edit existing app
## Related Stories
- ORG-009: Connect Global Apps
- ORG-010: Browse and Install Partner Apps
- DEV-004: Proper App ID Initialization
- DEV-005: Register OAuth Client App
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@idp.global/idp.global', name: '@idp.global/idp.global',
version: '1.5.0', version: '1.6.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.' description: 'An identity provider software managing user authentications, registrations, and sessions.'
} }
+40
View File
@@ -0,0 +1,40 @@
import * as plugins from '../plugins.js';
import type { AppManager } from './classes.appmanager.js';
@plugins.smartdata.Manager()
export class App extends plugins.smartdata.SmartDataDbDoc<
App,
plugins.idpInterfaces.data.IAppDocument,
AppManager
> {
// INSTANCE
@plugins.smartdata.unI()
id: plugins.idpInterfaces.data.IAppDocument['id'];
@plugins.smartdata.svDb()
type: plugins.idpInterfaces.data.IAppDocument['type'];
@plugins.smartdata.svDb()
data: plugins.idpInterfaces.data.IAppDocument['data'];
/**
* Check if the app is a global app
*/
public isGlobalApp(): this is App & { type: 'global' } {
return this.type === 'global';
}
/**
* Check if the app is a partner app
*/
public isPartnerApp(): this is App & { type: 'partner' } {
return this.type === 'partner';
}
/**
* Check if the app is a custom OIDC app
*/
public isCustomOidcApp(): this is App & { type: 'custom_oidc' } {
return this.type === 'custom_oidc';
}
}
+41
View File
@@ -0,0 +1,41 @@
import * as plugins from '../plugins.js';
import type { AppConnectionManager } from './classes.appconnectionmanager.js';
@plugins.smartdata.Manager()
export class AppConnection extends plugins.smartdata.SmartDataDbDoc<
AppConnection,
plugins.idpInterfaces.data.IAppConnection,
AppConnectionManager
> {
// INSTANCE
@plugins.smartdata.unI()
id: plugins.idpInterfaces.data.IAppConnection['id'];
@plugins.smartdata.svDb()
data: plugins.idpInterfaces.data.IAppConnection['data'];
/**
* Check if the connection is active
*/
public isActive(): boolean {
return this.data.status === 'active';
}
/**
* Disconnect the app
*/
public async disconnect(): Promise<void> {
this.data.status = 'disconnected';
await this.save();
}
/**
* Reconnect the app
*/
public async reconnect(userId: string): Promise<void> {
this.data.status = 'active';
this.data.connectedAt = Date.now();
this.data.connectedByUserId = userId;
await this.save();
}
}
@@ -0,0 +1,187 @@
import * as plugins from '../plugins.js';
import type { Reception } from './classes.reception.js';
import { AppConnection } from './classes.appconnection.js';
export class AppConnectionManager {
public receptionRef: Reception;
public get db() {
return this.receptionRef.db.smartdataDb;
}
public typedrouter = new plugins.typedrequest.TypedRouter();
public CAppConnection = plugins.smartdata.setDefaultManagerForDoc(this, AppConnection);
constructor(receptionRefArg: Reception) {
this.receptionRef = receptionRefArg;
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
// Handler: Get app connections for an organization
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetAppConnections>(
'getAppConnections',
async (requestArg) => {
// Verify JWT and get user
const jwtData = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt);
const user = await this.receptionRef.userManager.CUser.getInstance({
id: jwtData.data.userId,
});
// Check user has access to the organization
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
id: requestArg.organizationId,
});
if (!organization) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
const role = await this.receptionRef.roleManager.CRole.getInstance({
data: {
organizationId: organization.id,
userId: user.id,
},
});
if (!role) {
throw new plugins.typedrequest.TypedResponseError(
'User not authorized for this organization'
);
}
// Get all connections for this organization
const connections = await this.CAppConnection.getInstances({
'data.organizationId': requestArg.organizationId,
});
const connectionObjects = await Promise.all(
connections.map(async (conn) => await conn.createSavableObject())
);
return {
connections: connectionObjects,
};
}
)
);
// Handler: Toggle app connection (connect/disconnect)
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ToggleAppConnection>(
'toggleAppConnection',
async (requestArg) => {
// Verify JWT and get user
const jwtData = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt);
const user = await this.receptionRef.userManager.CUser.getInstance({
id: jwtData.data.userId,
});
// Check user has admin access to the organization
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
id: requestArg.organizationId,
});
if (!organization) {
throw new plugins.typedrequest.TypedResponseError('Organization not found');
}
const isAdmin = await organization.checkIfUserIsAdmin(user);
if (!isAdmin) {
throw new plugins.typedrequest.TypedResponseError(
'Only organization admins can manage app connections'
);
}
// Get the app
const app = await this.receptionRef.appManager.getAppById(requestArg.appId);
if (!app) {
throw new plugins.typedrequest.TypedResponseError('App not found');
}
// Find existing connection
let connection = await this.CAppConnection.getInstance({
'data.organizationId': requestArg.organizationId,
'data.appId': requestArg.appId,
});
if (requestArg.action === 'connect') {
if (connection && connection.isActive()) {
// Already connected
return {
success: true,
connection: await connection.createSavableObject(),
};
}
if (connection) {
// Reconnect existing connection
await connection.reconnect(user.id);
} else {
// Create new connection
connection = new AppConnection();
connection.id = plugins.smartunique.shortId();
connection.data = {
organizationId: requestArg.organizationId,
appId: requestArg.appId,
appType: app.type,
status: 'active',
connectedAt: Date.now(),
connectedByUserId: user.id,
grantedScopes: app.data.oauthCredentials?.allowedScopes || [],
};
await connection.save();
}
return {
success: true,
connection: await connection.createSavableObject(),
};
} else {
// Disconnect
if (!connection) {
return {
success: true,
};
}
await connection.disconnect();
return {
success: true,
connection: await connection.createSavableObject(),
};
}
}
)
);
}
/**
* Get all connections for an organization
*/
public async getConnectionsForOrganization(organizationId: string): Promise<AppConnection[]> {
return await this.CAppConnection.getInstances({
'data.organizationId': organizationId,
});
}
/**
* Get connection for a specific app and organization
*/
public async getConnection(
organizationId: string,
appId: string
): Promise<AppConnection | null> {
return await this.CAppConnection.getInstance({
'data.organizationId': organizationId,
'data.appId': appId,
});
}
/**
* Check if an app is connected to an organization
*/
public async isAppConnected(organizationId: string, appId: string): Promise<boolean> {
const connection = await this.getConnection(organizationId, appId);
return connection?.isActive() || false;
}
}
+117
View File
@@ -0,0 +1,117 @@
import * as plugins from '../plugins.js';
import type { Reception } from './classes.reception.js';
import { App } from './classes.app.js';
export class AppManager {
public receptionRef: Reception;
public get db() {
return this.receptionRef.db.smartdataDb;
}
public typedrouter = new plugins.typedrequest.TypedRouter();
public CApp = plugins.smartdata.setDefaultManagerForDoc(this, App);
constructor(receptionRefArg: Reception) {
this.receptionRef = receptionRefArg;
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
// Handler: Get all global apps
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetGlobalApps>(
'getGlobalApps',
async (requestArg) => {
// Verify JWT
await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt);
// Get all active global apps
const globalApps = await this.CApp.getInstances({
type: 'global',
});
const appObjects = await Promise.all(
globalApps.map(async (app) => await app.createSavableObject() as plugins.idpInterfaces.data.IGlobalApp)
);
return {
apps: appObjects,
};
}
)
);
}
/**
* Get all global apps
*/
public async getGlobalApps(): Promise<App[]> {
return await this.CApp.getInstances({
type: 'global',
});
}
/**
* Get app by ID
*/
public async getAppById(appId: string): Promise<App | null> {
return await this.CApp.getInstance({
id: appId,
});
}
/**
* Seed initial global apps (for development/testing)
*/
public async seedGlobalApps() {
const defaultGlobalApps: Partial<plugins.idpInterfaces.data.IGlobalApp>[] = [
{
id: 'app-foss-global',
type: 'global',
data: {
name: 'foss.global',
description: 'Open Source Package Registry and Collaboration Platform',
logoUrl: 'https://foss.global/assets/logo.png',
appUrl: 'https://foss.global',
oauthCredentials: {
clientId: 'foss-global-client',
clientSecretHash: '', // Will be set when OAuth is configured
redirectUris: ['https://foss.global/auth/callback'],
allowedScopes: ['openid', 'profile', 'email', 'organizations'],
grantTypes: ['authorization_code', 'refresh_token'],
},
isActive: true,
category: 'Development',
},
},
{
id: 'app-task-vc',
type: 'global',
data: {
name: 'task.vc',
description: 'Task Management and Project Collaboration',
logoUrl: 'https://task.vc/assets/logo.png',
appUrl: 'https://task.vc',
oauthCredentials: {
clientId: 'task-vc-client',
clientSecretHash: '',
redirectUris: ['https://task.vc/auth/callback'],
allowedScopes: ['openid', 'profile', 'email', 'organizations'],
grantTypes: ['authorization_code', 'refresh_token'],
},
isActive: true,
category: 'Productivity',
},
},
];
for (const appData of defaultGlobalApps) {
const existing = await this.CApp.getInstance({ id: appData.id });
if (!existing) {
const app = new App();
app.id = appData.id!;
app.type = appData.type!;
app.data = appData.data as any;
await app.save();
}
}
}
}
+4
View File
@@ -13,6 +13,8 @@ import { ReceptionHousekeeping } from './classes.housekeeping.js';
import { OrganizationManager } from './classes.organizationmanager.js'; import { OrganizationManager } from './classes.organizationmanager.js';
import { RoleManager } from './classes.rolemanager.js'; import { RoleManager } from './classes.rolemanager.js';
import { BillingPlanManager } from './classes.billingplanmanager.js'; import { BillingPlanManager } from './classes.billingplanmanager.js';
import { AppManager } from './classes.appmanager.js';
import { AppConnectionManager } from './classes.appconnectionmanager.js';
export interface IReceptionOptions { export interface IReceptionOptions {
/** /**
@@ -41,6 +43,8 @@ export class Reception {
public organizationmanager = new OrganizationManager(this); public organizationmanager = new OrganizationManager(this);
public roleManager = new RoleManager(this); public roleManager = new RoleManager(this);
public billingPlanManager = new BillingPlanManager(this); public billingPlanManager = new BillingPlanManager(this);
public appManager = new AppManager(this);
public appConnectionManager = new AppConnectionManager(this);
housekeeping = new ReceptionHousekeeping(this); housekeeping = new ReceptionHousekeeping(this);
constructor(public options: IReceptionOptions) { constructor(public options: IReceptionOptions) {
+3 -3
View File
@@ -11,12 +11,12 @@ export class IdpClient {
// INSTANCE PUBLIC // INSTANCE PUBLIC
public appData: plugins.idpInterfaces.data.IApp; public appData: plugins.idpInterfaces.data.IAppLegacy;
public rolesReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1); public rolesReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
public organizationsReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1); public organizationsReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
public parsedReceptionUrl: plugins.smarturl.Smarturl; public parsedReceptionUrl: plugins.smarturl.Smarturl;
constructor(receptionBaseUrlArg: string, appDataArg?: plugins.idpInterfaces.data.IApp) { constructor(receptionBaseUrlArg: string, appDataArg?: plugins.idpInterfaces.data.IAppLegacy) {
if (receptionBaseUrlArg.endsWith('/')) { if (receptionBaseUrlArg.endsWith('/')) {
receptionBaseUrlArg = receptionBaseUrlArg.slice(0, -1); receptionBaseUrlArg = receptionBaseUrlArg.slice(0, -1);
} }
@@ -146,7 +146,7 @@ export class IdpClient {
/** /**
* can be used to switch between pages * can be used to switch between pages
*/ */
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IApp): Promise<string> { public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string> {
const jwt = await this.performJwtHousekeeping(); const jwt = await this.performJwtHousekeeping();
const extractedJwt = await this.helpers.extractDataFromJwtString(jwt); const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
const getTransferToken = const getTransferToken =
+1
View File
@@ -1,4 +1,5 @@
export * from './loint-reception.app.js'; export * from './loint-reception.app.js';
export * from './loint-reception.appconnection.js';
export * from './loint-reception.billingplan.js'; export * from './loint-reception.billingplan.js';
export * from './loint-reception.device.js'; export * from './loint-reception.device.js';
export * from './loint-reception.jwt.js'; export * from './loint-reception.jwt.js';
+85 -1
View File
@@ -1,4 +1,78 @@
export interface IApp { // App Types
export type TAppType = 'global' | 'partner' | 'custom_oidc';
export type TAppApprovalStatus = 'draft' | 'pending_review' | 'approved' | 'rejected' | 'suspended';
// OAuth Credentials
export interface IOAuthCredentials {
clientId: string;
clientSecretHash: string;
redirectUris: string[];
allowedScopes: string[];
grantTypes: ('authorization_code' | 'client_credentials' | 'refresh_token')[];
}
// Base app data shared by all app types
export interface IAppBaseData {
name: string;
description: string;
logoUrl: string;
appUrl: string;
}
// Global App - First-party apps managed by platform (foss.global, task.vc, etc.)
export interface IGlobalApp {
id: string;
type: 'global';
data: IAppBaseData & {
oauthCredentials: IOAuthCredentials;
isActive: boolean;
category: string;
};
}
// Partner App - Third-party apps submitted to AppStore
export interface IPartnerApp {
id: string;
type: 'partner';
data: IAppBaseData & {
ownerOrganizationId: string;
oauthCredentials: IOAuthCredentials;
appStoreMetadata: {
shortDescription: string;
longDescription: string;
screenshots: string[];
category: string;
tags: string[];
pricing: { model: 'free' | 'paid' | 'freemium' };
};
approvalStatus: TAppApprovalStatus;
isPublished: boolean;
installCount: number;
};
}
// Custom OIDC App - Organization-created OAuth clients
export interface ICustomOidcApp {
id: string;
type: 'custom_oidc';
data: IAppBaseData & {
ownerOrganizationId: string;
oauthCredentials: IOAuthCredentials;
oidcSettings: {
accessTokenLifetime: number; // seconds
refreshTokenLifetime: number; // seconds
};
};
}
// Union type for all app types
export type IApp = IGlobalApp | IPartnerApp | ICustomOidcApp;
/**
* Legacy interface for backwards compatibility with existing code
* that expects a flat app structure (e.g., idpclient, transfermanager)
*/
export interface IAppLegacy {
/** /**
* must be unique * must be unique
*/ */
@@ -11,3 +85,13 @@ export interface IApp {
logoUrl: string; logoUrl: string;
appUrl: string; appUrl: string;
} }
/**
* Storage interface for SmartData documents
* Uses the discriminated union approach with a 'type' field
*/
export interface IAppDocument {
id: string;
type: TAppType;
data: IGlobalApp['data'] | IPartnerApp['data'] | ICustomOidcApp['data'];
}
@@ -0,0 +1,16 @@
import type { TAppType } from './loint-reception.app.js';
export type TAppConnectionStatus = 'active' | 'disconnected';
export interface IAppConnection {
id: string;
data: {
organizationId: string;
appId: string;
appType: TAppType;
status: TAppConnectionStatus;
connectedAt: number;
connectedByUserId: string;
grantedScopes: string[];
};
}
+1
View File
@@ -1,4 +1,5 @@
export * from './loint-reception.apitoken.js'; export * from './loint-reception.apitoken.js';
export * from './loint-reception.app.js';
export * from './loint-reception.authorization.js'; export * from './loint-reception.authorization.js';
export * from './loint-reception.billingplan.js'; export * from './loint-reception.billingplan.js';
export * from './loint-reception.jwt.js'; export * from './loint-reception.jwt.js';
@@ -0,0 +1,52 @@
import * as data from '../data/index.js';
import * as plugins from '../loint-reception.plugins.js';
// Get all global apps
export interface IReq_GetGlobalApps
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetGlobalApps
> {
method: 'getGlobalApps';
request: {
jwt: string;
};
response: {
apps: data.IGlobalApp[];
};
}
// Get app connections for an organization
export interface IReq_GetAppConnections
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetAppConnections
> {
method: 'getAppConnections';
request: {
jwt: string;
organizationId: string;
};
response: {
connections: data.IAppConnection[];
};
}
// Connect/disconnect an app for an organization
export interface IReq_ToggleAppConnection
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_ToggleAppConnection
> {
method: 'toggleAppConnection';
request: {
jwt: string;
organizationId: string;
appId: string;
action: 'connect' | 'disconnect';
};
response: {
success: boolean;
connection?: data.IAppConnection;
};
}
@@ -103,7 +103,7 @@ export interface IReq_ExchangeRefreshTokenAndTransferToken
request: { request: {
transferToken?: string; transferToken?: string;
refreshToken?: string; refreshToken?: string;
appData: data.IApp; appData: data.IAppLegacy;
}; };
response: { response: {
refreshToken?: string; refreshToken?: string;
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@idp.global/idp.global', name: '@idp.global/idp.global',
version: '1.5.0', version: '1.6.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.' description: 'An identity provider software managing user authentications, registrations, and sessions.'
} }
+10
View File
@@ -139,6 +139,16 @@ export class IdpAccountContent extends DeesElement {
await this.domtools.convenience.smartdelay.delayFor(300); 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.subrouter._handleRouteState();
this.registerGarbageFunction(async () => { this.registerGarbageFunction(async () => {
+14 -2
View File
@@ -214,7 +214,13 @@ export class LeleAccountNavigation extends DeesElement {
<div <div
class="navigationOption" 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> <dees-icon .icon=${'lucide:box'}></dees-icon>
Apps Apps
@@ -235,7 +241,13 @@ export class LeleAccountNavigation extends DeesElement {
</div> </div>
<div <div
class="navigationOption" 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> <dees-icon .icon=${'lucide:wallet'}></dees-icon>
Billing Billing
+450
View File
@@ -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
View File
@@ -1,3 +1,4 @@
export * from './appsview.js';
export * from './baseview.js'; export * from './baseview.js';
export * from './orgsetup.js'; export * from './orgsetup.js';
export * from './paddlesetup.js'; export * from './paddlesetup.js';
+1 -1
View File
@@ -21,7 +21,7 @@ declare global {
@customElement('idp-transfermanager') @customElement('idp-transfermanager')
export class IdpTransfermanager extends DeesElement { export class IdpTransfermanager extends DeesElement {
public appData: plugins.idpInterfaces.data.IApp; public appData: plugins.idpInterfaces.data.IAppLegacy;
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,