update
This commit is contained in:
+8
-8
@@ -16,12 +16,12 @@
|
|||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.1.10",
|
"@api.global/typedrequest": "^3.1.11",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^3.0.80",
|
"@api.global/typedserver": "^7.7.0",
|
||||||
"@api.global/typedsocket": "^3.0.1",
|
"@api.global/typedsocket": "^4.1.0",
|
||||||
"@consent.software/catalog": "^2.0.1",
|
"@consent.software/catalog": "^2.0.1",
|
||||||
"@design.estate/dees-catalog": "^2.0.2",
|
"@design.estate/dees-catalog": "^2.0.3",
|
||||||
"@design.estate/dees-domtools": "^2.3.6",
|
"@design.estate/dees-domtools": "^2.3.6",
|
||||||
"@design.estate/dees-element": "^2.1.3",
|
"@design.estate/dees-element": "^2.1.3",
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
@@ -40,19 +40,19 @@
|
|||||||
"@push.rocks/smarttime": "^4.1.1",
|
"@push.rocks/smarttime": "^4.1.1",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smarturl": "^3.1.0",
|
"@push.rocks/smarturl": "^3.1.0",
|
||||||
"@push.rocks/taskbuffer": "^3.4.0",
|
"@push.rocks/taskbuffer": "^3.5.0",
|
||||||
"@push.rocks/webjwt": "^1.0.9",
|
"@push.rocks/webjwt": "^1.0.9",
|
||||||
"@push.rocks/websetup": "^3.0.15",
|
"@push.rocks/websetup": "^3.0.15",
|
||||||
"@push.rocks/webstore": "^2.0.20",
|
"@push.rocks/webstore": "^2.0.20",
|
||||||
"@serve.zone/platformclient": "^1.1.2",
|
"@serve.zone/platformclient": "^1.1.2",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
"@uptime.link/webwidget": "^1.2.4"
|
"@uptime.link/webwidget": "^1.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^3.1.2",
|
"@git.zone/tsbuild": "^3.1.2",
|
||||||
"@git.zone/tsbundle": "^2.6.2",
|
"@git.zone/tsbundle": "^2.6.3",
|
||||||
"@git.zone/tsrun": "^2.0.0",
|
"@git.zone/tsrun": "^2.0.0",
|
||||||
"@git.zone/tswatch": "^2.2.2",
|
"@git.zone/tswatch": "^2.2.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@types/node": "^24.10.1"
|
"@types/node": "^24.10.1"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+411
-306
File diff suppressed because it is too large
Load Diff
@@ -2,27 +2,127 @@
|
|||||||
|
|
||||||
**ID:** ORG-002
|
**ID:** ORG-002
|
||||||
**Priority:** Critical
|
**Priority:** Critical
|
||||||
**Status:** Planned
|
**Status:** In Development
|
||||||
|
|
||||||
## User Story
|
## User Story
|
||||||
As an organization owner, I want to invite team members to my organization and manage their access so that my team can collaborate securely.
|
As an organization owner, I want to invite team members to my organization and manage their access so that my team can collaborate securely.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
- [ ] Owner can invite users via email address
|
- [x] Owner can invite users via email address
|
||||||
- [ ] Invited user receives email with invitation link
|
- [x] Invited user receives email with invitation link
|
||||||
- [ ] Invitation can be accepted by existing users or during registration
|
- [x] Invitation can be accepted by existing users or during registration
|
||||||
- [ ] Owner can view pending invitations and resend/cancel them
|
- [x] Owner can view pending invitations and resend/cancel them
|
||||||
- [ ] Owner can see all current members with their roles
|
- [x] Owner can see all current members with their roles
|
||||||
- [ ] Owner can remove members from organization
|
- [x] Owner can remove members from organization
|
||||||
- [ ] Owner can transfer ownership to another member
|
- [ ] Owner can transfer ownership to another member
|
||||||
- [ ] Bulk invite via CSV upload
|
- [ ] Bulk invite via CSV upload
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### UserInvitation System
|
||||||
|
|
||||||
|
The invitation system uses a shared `UserInvitation` model that supports multiple organizations inviting the same email address.
|
||||||
|
|
||||||
|
#### Invitation Lifecycle
|
||||||
|
|
||||||
|
1. **Create**: Org admin invites email → `UserInvitation` created (or existing one is updated)
|
||||||
|
2. **Share**: Multiple orgs can link to the same invitation (by email)
|
||||||
|
3. **Convert**: When user registers with that email → invitation converts to real User
|
||||||
|
4. **Fold**: If existing user adds that email as secondary → invitation folds into existing user
|
||||||
|
5. **Expire**: Auto-delete after 90 days with cleanup of all org refs
|
||||||
|
|
||||||
|
#### Data Model
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// IUserInvitation
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
email: string; // Unique key for sharing
|
||||||
|
token: string; // Secure invitation link token
|
||||||
|
status: 'pending' | 'accepted' | 'expired' | 'cancelled';
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number; // 90 days from creation
|
||||||
|
organizationRefs: Array<{ // Multiple orgs can share
|
||||||
|
organizationId: string;
|
||||||
|
invitedByUserId: string;
|
||||||
|
invitedAt: number;
|
||||||
|
roles: string[]; // Roles to assign on acceptance
|
||||||
|
}>;
|
||||||
|
acceptedAt?: number;
|
||||||
|
convertedToUserId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role System Enhancement
|
||||||
|
|
||||||
|
Users can have multiple roles within an organization:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// IRole
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
userId: string;
|
||||||
|
organizationId: string;
|
||||||
|
roles: string[]; // e.g., ['owner', 'billing-admin', 'developer']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Standard roles: `owner`, `admin`, `editor`, `viewer`, `guest`
|
||||||
|
Custom roles are also supported.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `createInvitation` | Invite email to org with roles |
|
||||||
|
| `getOrgInvitations` | List pending invitations |
|
||||||
|
| `getOrgMembers` | List members with roles |
|
||||||
|
| `cancelInvitation` | Cancel pending invitation |
|
||||||
|
| `resendInvitation` | Resend invitation email |
|
||||||
|
| `removeMember` | Remove user from org |
|
||||||
|
| `updateMemberRoles` | Change member's roles |
|
||||||
|
| `transferOwnership` | Transfer org ownership |
|
||||||
|
| `acceptInvitation` | Accept invitation |
|
||||||
|
| `getInvitationByToken` | Get invitation details for landing page |
|
||||||
|
|
||||||
|
### Frontend Implementation
|
||||||
|
|
||||||
|
The Users page (`/account/org/:orgName/users`) provides:
|
||||||
|
|
||||||
|
- **Members tab**: List all members with roles, remove/edit actions
|
||||||
|
- **Pending tab**: List pending invitations with resend/cancel
|
||||||
|
- **Invite tab**: Form to invite by email with role selection
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `ts_interfaces/data/loint-reception.userinvitation.ts` - Data interface
|
||||||
|
- `ts_interfaces/request/loint-reception.userinvitation.ts` - API contracts
|
||||||
|
- `ts/reception/classes.userinvitation.ts` - Model
|
||||||
|
- `ts/reception/classes.userinvitationmanager.ts` - Manager with handlers
|
||||||
|
- `ts/reception/classes.receptionmailer.ts` - Invitation email
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `ts_web/elements/account/views/usersview.ts` - Users page component
|
||||||
|
- `ts_web/elements/account/content.ts` - Route registration
|
||||||
|
- `ts_web/elements/account/navigation.ts` - Nav link
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
- Organization and User models exist with association
|
- Organization and User models exist with association
|
||||||
- Need new Invitation model with token and expiry
|
- UserInvitation model stores invitation data with 90-day expiry
|
||||||
- Use `ReceptionMailer` for invitation emails
|
- `ReceptionMailer.sendInvitationEmail()` handles email delivery
|
||||||
- RoleManager can be leveraged for role assignment
|
- RoleManager updated to support `roles: string[]` array
|
||||||
- Consider invitation expiry (7 days default)
|
- Backward compatible with existing single-role data
|
||||||
|
|
||||||
|
## Related Stories
|
||||||
|
- ORG-003: Assign Roles to Members (enhanced with multi-role support)
|
||||||
|
|
||||||
## Related TODOs
|
## Related TODOs
|
||||||
- New feature - core organizational functionality
|
- [ ] Integrate invitation acceptance into registration flow
|
||||||
|
- [ ] Add email verification flow for secondary emails (folding)
|
||||||
|
- [ ] Implement scheduled cleanup job for expired invitations
|
||||||
|
- [ ] Add CSV bulk invite feature
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ export class Organization extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
public async checkIfUserIsAdmin(userArg: User) {
|
public async checkIfUserIsAdmin(userArg: User) {
|
||||||
const role = await this.manager.receptionRef.roleManager.getRoleForUserAndOrg(userArg, this);
|
const role = await this.manager.receptionRef.roleManager.getRoleForUserAndOrg(userArg, this);
|
||||||
return role.data.role === 'admin';
|
return role.data.roles?.includes('admin') || role.data.roles?.includes('owner');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { BillingPlanManager } from './classes.billingplanmanager.js';
|
|||||||
import { AppManager } from './classes.appmanager.js';
|
import { AppManager } from './classes.appmanager.js';
|
||||||
import { AppConnectionManager } from './classes.appconnectionmanager.js';
|
import { AppConnectionManager } from './classes.appconnectionmanager.js';
|
||||||
import { ActivityLogManager } from './classes.activitylogmanager.js';
|
import { ActivityLogManager } from './classes.activitylogmanager.js';
|
||||||
|
import { UserInvitationManager } from './classes.userinvitationmanager.js';
|
||||||
|
|
||||||
export interface IReceptionOptions {
|
export interface IReceptionOptions {
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +48,7 @@ export class Reception {
|
|||||||
public appManager = new AppManager(this);
|
public appManager = new AppManager(this);
|
||||||
public appConnectionManager = new AppConnectionManager(this);
|
public appConnectionManager = new AppConnectionManager(this);
|
||||||
public activityLogManager = new ActivityLogManager(this);
|
public activityLogManager = new ActivityLogManager(this);
|
||||||
|
public userInvitationManager = new UserInvitationManager(this);
|
||||||
housekeeping = new ReceptionHousekeeping(this);
|
housekeeping = new ReceptionHousekeeping(this);
|
||||||
|
|
||||||
constructor(public options: IReceptionOptions) {
|
constructor(public options: IReceptionOptions) {
|
||||||
|
|||||||
@@ -268,4 +268,33 @@ export class ReceptionMailer {
|
|||||||
`),
|
`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sendInvitationEmail(
|
||||||
|
email: string,
|
||||||
|
organizationName: string,
|
||||||
|
invitationToken: string,
|
||||||
|
baseUrl: string
|
||||||
|
) {
|
||||||
|
const invitationUrl = `${baseUrl}/invite?token=${encodeURI(invitationToken)}`;
|
||||||
|
|
||||||
|
this.receptionRef.szPlatformClient.emailConnector.sendEmail({
|
||||||
|
from: `idp.global@${this.receptionRef.options.baseUrl} <noreply@mail.workspace.global>`,
|
||||||
|
title: `You've been invited to join ${organizationName}`,
|
||||||
|
to: email,
|
||||||
|
body: this.createBodyString(`
|
||||||
|
<h1>You're Invited!</h1>
|
||||||
|
<p>You've been invited to join <b>${organizationName}</b> on idp.global.</p>
|
||||||
|
<p>Click the button below to accept the invitation and join the organization.</p>
|
||||||
|
<a href="${invitationUrl}"><div class="button">
|
||||||
|
Accept Invitation
|
||||||
|
</div></a>
|
||||||
|
<p style="color: #888888; font-size: 12px; margin-top: 20px;">
|
||||||
|
If you don't have an account yet, you'll be able to create one when you accept the invitation.
|
||||||
|
</p>
|
||||||
|
<p style="color: #888888; font-size: 12px;">
|
||||||
|
This invitation will expire in 90 days.
|
||||||
|
</p>
|
||||||
|
`),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,24 @@ export class RoleManager {
|
|||||||
this.receptionRef = receptionRefArg;
|
this.receptionRef = receptionRefArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create, change, or delete a role for a user in an organization.
|
||||||
|
* Supports both old single-role and new multi-role patterns.
|
||||||
|
*/
|
||||||
public async modifyRoleForUserAtOrg(optionsArg: {
|
public async modifyRoleForUserAtOrg(optionsArg: {
|
||||||
action: 'create' | 'change' | 'delete';
|
action: 'create' | 'change' | 'delete';
|
||||||
userId: string;
|
userId: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
role: plugins.idpInterfaces.data.IRole['data']['role'];
|
/** @deprecated Use `roles` instead */
|
||||||
|
role?: string;
|
||||||
|
/** Array of roles to assign */
|
||||||
|
roles?: string[];
|
||||||
}) {
|
}) {
|
||||||
let returnRole: Role;
|
let returnRole: Role;
|
||||||
|
|
||||||
|
// Support both old single role and new roles array
|
||||||
|
const roles = optionsArg.roles || (optionsArg.role ? [optionsArg.role] : ['viewer']);
|
||||||
|
|
||||||
switch (optionsArg.action) {
|
switch (optionsArg.action) {
|
||||||
case 'create':
|
case 'create':
|
||||||
returnRole = new this.CRole();
|
returnRole = new this.CRole();
|
||||||
@@ -29,9 +40,35 @@ export class RoleManager {
|
|||||||
returnRole.data = {
|
returnRole.data = {
|
||||||
userId: optionsArg.userId,
|
userId: optionsArg.userId,
|
||||||
organizationId: optionsArg.organizationId,
|
organizationId: optionsArg.organizationId,
|
||||||
role: optionsArg.role,
|
roles: roles,
|
||||||
};
|
};
|
||||||
await returnRole.save();
|
await returnRole.save();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'change':
|
||||||
|
returnRole = await this.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: optionsArg.userId,
|
||||||
|
organizationId: optionsArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (returnRole) {
|
||||||
|
returnRole.data.roles = roles;
|
||||||
|
await returnRole.save();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
returnRole = await this.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: optionsArg.userId,
|
||||||
|
organizationId: optionsArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (returnRole) {
|
||||||
|
await returnRole.delete();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return returnRole;
|
return returnRole;
|
||||||
}
|
}
|
||||||
@@ -54,4 +91,13 @@ export class RoleManager {
|
|||||||
});
|
});
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAllRolesForOrg(organizationId: string) {
|
||||||
|
const roles = await this.CRole.getInstances({
|
||||||
|
data: {
|
||||||
|
organizationId: organizationId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserInvitation represents an invitation to join one or more organizations.
|
||||||
|
*
|
||||||
|
* Key characteristics:
|
||||||
|
* - Unique by email (multiple orgs can share the same invitation)
|
||||||
|
* - Converts to real User on registration
|
||||||
|
* - Can fold into existing user if they add the email as secondary
|
||||||
|
* - Auto-expires after 90 days
|
||||||
|
*/
|
||||||
|
@plugins.smartdata.Manager()
|
||||||
|
export class UserInvitation extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
UserInvitation,
|
||||||
|
plugins.idpInterfaces.data.IUserInvitation
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static readonly EXPIRY_DAYS = 90;
|
||||||
|
|
||||||
|
public static generateToken(): string {
|
||||||
|
return plugins.smartunique.shortId() + '-' + plugins.smartunique.shortId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createNewInvitation(
|
||||||
|
email: string,
|
||||||
|
organizationId: string,
|
||||||
|
invitedByUserId: string,
|
||||||
|
roles: string[]
|
||||||
|
): Promise<UserInvitation> {
|
||||||
|
const invitation = new UserInvitation();
|
||||||
|
invitation.id = plugins.smartunique.shortId();
|
||||||
|
const now = Date.now();
|
||||||
|
const expiresAt = now + (UserInvitation.EXPIRY_DAYS * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
invitation.data = {
|
||||||
|
email: email.toLowerCase().trim(),
|
||||||
|
token: UserInvitation.generateToken(),
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: now,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
organizationRefs: [{
|
||||||
|
organizationId,
|
||||||
|
invitedByUserId,
|
||||||
|
invitedAt: now,
|
||||||
|
roles,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
await invitation.save();
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.idpInterfaces.data.IUserInvitation['data'];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add another organization to this invitation
|
||||||
|
*/
|
||||||
|
public async addOrganization(
|
||||||
|
organizationId: string,
|
||||||
|
invitedByUserId: string,
|
||||||
|
roles: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
// Check if org already exists
|
||||||
|
const existingRef = this.data.organizationRefs.find(
|
||||||
|
ref => ref.organizationId === organizationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingRef) {
|
||||||
|
// Update roles for existing org ref
|
||||||
|
existingRef.roles = roles;
|
||||||
|
existingRef.invitedAt = Date.now();
|
||||||
|
existingRef.invitedByUserId = invitedByUserId;
|
||||||
|
} else {
|
||||||
|
// Add new org ref
|
||||||
|
this.data.organizationRefs.push({
|
||||||
|
organizationId,
|
||||||
|
invitedByUserId,
|
||||||
|
invitedAt: Date.now(),
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an organization from this invitation
|
||||||
|
*/
|
||||||
|
public async removeOrganization(organizationId: string): Promise<void> {
|
||||||
|
this.data.organizationRefs = this.data.organizationRefs.filter(
|
||||||
|
ref => ref.organizationId !== organizationId
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no more org refs, cancel the invitation
|
||||||
|
if (this.data.organizationRefs.length === 0) {
|
||||||
|
this.data.status = 'cancelled';
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if invitation is expired
|
||||||
|
*/
|
||||||
|
public isExpired(): boolean {
|
||||||
|
return Date.now() > this.data.expiresAt || this.data.status === 'expired';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark invitation as accepted and record the user ID
|
||||||
|
*/
|
||||||
|
public async accept(userId: string): Promise<void> {
|
||||||
|
this.data.status = 'accepted';
|
||||||
|
this.data.acceptedAt = Date.now();
|
||||||
|
this.data.convertedToUserId = userId;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate token and extend expiry (for resend)
|
||||||
|
*/
|
||||||
|
public async regenerateToken(): Promise<void> {
|
||||||
|
this.data.token = UserInvitation.generateToken();
|
||||||
|
this.data.expiresAt = Date.now() + (UserInvitation.EXPIRY_DAYS * 24 * 60 * 60 * 1000);
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,556 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Reception } from './classes.reception.js';
|
||||||
|
import { UserInvitation } from './classes.userinvitation.js';
|
||||||
|
import { Organization } from './classes.organization.js';
|
||||||
|
import { User } from './classes.user.js';
|
||||||
|
import { Role } from './classes.role.js';
|
||||||
|
|
||||||
|
export class UserInvitationManager {
|
||||||
|
public receptionRef: Reception;
|
||||||
|
public get db() {
|
||||||
|
return this.receptionRef.db.smartdataDb;
|
||||||
|
}
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
public CUserInvitation = plugins.smartdata.setDefaultManagerForDoc(this, UserInvitation);
|
||||||
|
|
||||||
|
constructor(receptionRefArg: Reception) {
|
||||||
|
this.receptionRef = receptionRefArg;
|
||||||
|
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
// Create invitation
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_CreateInvitation>(
|
||||||
|
'createInvitation',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
const email = requestArg.email.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Check if user with this email already exists
|
||||||
|
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
data: { email },
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
// User already exists - just add them to the org directly
|
||||||
|
const existingRole = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: existingUser.id,
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingRole) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
isNew: false,
|
||||||
|
message: 'User is already a member of this organization.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Add user to org with the specified roles
|
||||||
|
await this.receptionRef.roleManager.modifyRoleForUserAtOrg({
|
||||||
|
action: 'create',
|
||||||
|
userId: existingUser.id,
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
roles: requestArg.roles,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
isNew: false,
|
||||||
|
message: 'Existing user has been added to the organization.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if invitation already exists for this email
|
||||||
|
let invitation = await this.CUserInvitation.getInstance({
|
||||||
|
data: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
let isNew = false;
|
||||||
|
if (invitation) {
|
||||||
|
// Add org to existing invitation
|
||||||
|
await invitation.addOrganization(requestArg.organizationId, user.id, requestArg.roles);
|
||||||
|
} else {
|
||||||
|
// Create new invitation
|
||||||
|
invitation = await UserInvitation.createNewInvitation(
|
||||||
|
email,
|
||||||
|
requestArg.organizationId,
|
||||||
|
user.id,
|
||||||
|
requestArg.roles
|
||||||
|
);
|
||||||
|
isNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send invitation email
|
||||||
|
await this.sendInvitationEmail(invitation, requestArg.organizationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
invitation: await invitation.createSavableObject(),
|
||||||
|
isNew,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get org invitations
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetOrgInvitations>(
|
||||||
|
'getOrgInvitations',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
const allInvitations = await this.CUserInvitation.getInstances({});
|
||||||
|
const orgInvitations = allInvitations.filter(inv =>
|
||||||
|
inv.data.status === 'pending' &&
|
||||||
|
!inv.isExpired() &&
|
||||||
|
inv.data.organizationRefs.some(ref => ref.organizationId === requestArg.organizationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitations: await Promise.all(orgInvitations.map(inv => inv.createSavableObject())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get org members
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetOrgMembers>(
|
||||||
|
'getOrgMembers',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsMemberOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
const roles = await this.receptionRef.roleManager.CRole.getInstances({
|
||||||
|
data: { organizationId: requestArg.organizationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const members: Array<{
|
||||||
|
user: plugins.idpInterfaces.data.IUser;
|
||||||
|
role: plugins.idpInterfaces.data.IRole;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
const memberUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
id: role.data.userId,
|
||||||
|
});
|
||||||
|
if (memberUser) {
|
||||||
|
members.push({
|
||||||
|
user: await memberUser.createSavableObject(),
|
||||||
|
role: await role.createSavableObject(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { members };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cancel invitation
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_CancelInvitation>(
|
||||||
|
'cancelInvitation',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
const invitation = await this.CUserInvitation.getInstance({ id: requestArg.invitationId });
|
||||||
|
if (!invitation) {
|
||||||
|
return { success: false, message: 'Invitation not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await invitation.removeOrganization(requestArg.organizationId);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resend invitation
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ResendInvitation>(
|
||||||
|
'resendInvitation',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
const invitation = await this.CUserInvitation.getInstance({ id: requestArg.invitationId });
|
||||||
|
if (!invitation) {
|
||||||
|
return { success: false, message: 'Invitation not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await invitation.regenerateToken();
|
||||||
|
await this.sendInvitationEmail(invitation, requestArg.organizationId);
|
||||||
|
|
||||||
|
return { success: true, message: 'Invitation resent.' };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove member
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_RemoveMember>(
|
||||||
|
'removeMember',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
// Cannot remove yourself if you're the only owner
|
||||||
|
const role = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: requestArg.userId,
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return { success: false, message: 'Member not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if trying to remove an owner
|
||||||
|
if (role.data.roles.includes('owner')) {
|
||||||
|
// Count owners
|
||||||
|
const allRoles = await this.receptionRef.roleManager.CRole.getInstances({
|
||||||
|
data: { organizationId: requestArg.organizationId },
|
||||||
|
});
|
||||||
|
const ownerCount = allRoles.filter(r => r.data.roles.includes('owner')).length;
|
||||||
|
if (ownerCount <= 1) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Cannot remove the last owner. Transfer ownership first.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await role.delete();
|
||||||
|
|
||||||
|
// Remove org from user's connectedOrgs
|
||||||
|
const memberUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
id: requestArg.userId,
|
||||||
|
});
|
||||||
|
if (memberUser && memberUser.data.connectedOrgs) {
|
||||||
|
memberUser.data.connectedOrgs = memberUser.data.connectedOrgs.filter(
|
||||||
|
orgId => orgId !== requestArg.organizationId
|
||||||
|
);
|
||||||
|
await memberUser.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update member roles
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_UpdateMemberRoles>(
|
||||||
|
'updateMemberRoles',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
|
||||||
|
|
||||||
|
const role = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: requestArg.userId,
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return { success: false, message: 'Member not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If removing owner role, check we're not removing the last owner
|
||||||
|
if (role.data.roles.includes('owner') && !requestArg.roles.includes('owner')) {
|
||||||
|
const allRoles = await this.receptionRef.roleManager.CRole.getInstances({
|
||||||
|
data: { organizationId: requestArg.organizationId },
|
||||||
|
});
|
||||||
|
const ownerCount = allRoles.filter(r => r.data.roles.includes('owner')).length;
|
||||||
|
if (ownerCount <= 1) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Cannot remove owner role from the last owner.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
role.data.roles = requestArg.roles;
|
||||||
|
await role.save();
|
||||||
|
|
||||||
|
return { success: true, role: await role.createSavableObject() };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transfer ownership
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_TransferOwnership>(
|
||||||
|
'transferOwnership',
|
||||||
|
async (requestArg) => {
|
||||||
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
||||||
|
|
||||||
|
// Verify current user is an owner
|
||||||
|
const currentUserRole = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!currentUserRole || !currentUserRole.data.roles.includes('owner')) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
|
'Only owners can transfer ownership.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new owner's role
|
||||||
|
const newOwnerRole = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: requestArg.newOwnerId,
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!newOwnerRole) {
|
||||||
|
return { success: false, message: 'New owner must be a member of the organization.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add owner role to new owner
|
||||||
|
if (!newOwnerRole.data.roles.includes('owner')) {
|
||||||
|
newOwnerRole.data.roles.push('owner');
|
||||||
|
await newOwnerRole.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove owner role from current user (but keep other roles)
|
||||||
|
currentUserRole.data.roles = currentUserRole.data.roles.filter(r => r !== 'owner');
|
||||||
|
if (currentUserRole.data.roles.length === 0) {
|
||||||
|
currentUserRole.data.roles = ['admin']; // Demote to admin
|
||||||
|
}
|
||||||
|
await currentUserRole.save();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get invitation by token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetInvitationByToken>(
|
||||||
|
'getInvitationByToken',
|
||||||
|
async (requestArg) => {
|
||||||
|
const invitation = await this.CUserInvitation.getInstance({
|
||||||
|
data: { token: requestArg.token },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invitation) {
|
||||||
|
return { isExpired: true, requiresRegistration: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitation.isExpired()) {
|
||||||
|
return { isExpired: true, requiresRegistration: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get organization names
|
||||||
|
const organizations: Array<{ id: string; name: string }> = [];
|
||||||
|
for (const ref of invitation.data.organizationRefs) {
|
||||||
|
const org = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
||||||
|
id: ref.organizationId,
|
||||||
|
});
|
||||||
|
if (org) {
|
||||||
|
organizations.push({ id: org.id, name: org.data.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user with this email exists
|
||||||
|
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
data: { email: invitation.data.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitation: await invitation.createSavableObject(),
|
||||||
|
organizations,
|
||||||
|
isExpired: false,
|
||||||
|
requiresRegistration: !existingUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Accept invitation
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_AcceptInvitation>(
|
||||||
|
'acceptInvitation',
|
||||||
|
async (requestArg) => {
|
||||||
|
const invitation = await this.CUserInvitation.getInstance({
|
||||||
|
data: { token: requestArg.token },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invitation) {
|
||||||
|
return { success: false, message: 'Invalid invitation token.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitation.isExpired()) {
|
||||||
|
return { success: false, message: 'This invitation has expired.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
id: requestArg.userId,
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
return { success: false, message: 'User not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create roles for each organization
|
||||||
|
const organizations: plugins.idpInterfaces.data.IOrganization[] = [];
|
||||||
|
const roles: plugins.idpInterfaces.data.IRole[] = [];
|
||||||
|
|
||||||
|
for (const ref of invitation.data.organizationRefs) {
|
||||||
|
// Check if role already exists
|
||||||
|
let role = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
organizationId: ref.organizationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
role = await this.receptionRef.roleManager.modifyRoleForUserAtOrg({
|
||||||
|
action: 'create',
|
||||||
|
userId: user.id,
|
||||||
|
organizationId: ref.organizationId,
|
||||||
|
roles: ref.roles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
roles.push(await role.createSavableObject());
|
||||||
|
|
||||||
|
const org = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
||||||
|
id: ref.organizationId,
|
||||||
|
});
|
||||||
|
if (org) {
|
||||||
|
// Add role to org's roleIds if not already there
|
||||||
|
if (!org.data.roleIds.includes(role.id)) {
|
||||||
|
org.data.roleIds.push(role.id);
|
||||||
|
await org.save();
|
||||||
|
}
|
||||||
|
organizations.push(await org.createSavableObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user's connectedOrgs
|
||||||
|
if (!user.data.connectedOrgs) {
|
||||||
|
user.data.connectedOrgs = [];
|
||||||
|
}
|
||||||
|
if (!user.data.connectedOrgs.includes(ref.organizationId)) {
|
||||||
|
user.data.connectedOrgs.push(ref.organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
await invitation.accept(user.id);
|
||||||
|
|
||||||
|
return { success: true, organizations, roles };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find invitation by email
|
||||||
|
*/
|
||||||
|
public async getInvitationByEmail(email: string): Promise<UserInvitation | null> {
|
||||||
|
return this.CUserInvitation.getInstance({
|
||||||
|
data: { email: email.toLowerCase().trim() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending invitations for an email (for registration flow)
|
||||||
|
*/
|
||||||
|
public async getPendingInvitationsForEmail(email: string): Promise<UserInvitation | null> {
|
||||||
|
const invitation = await this.getInvitationByEmail(email);
|
||||||
|
if (invitation && invitation.data.status === 'pending' && !invitation.isExpired()) {
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired invitations
|
||||||
|
*/
|
||||||
|
public async cleanupExpiredInvitations(): Promise<number> {
|
||||||
|
const allInvitations = await this.CUserInvitation.getInstances({
|
||||||
|
data: { status: 'pending' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let cleanedCount = 0;
|
||||||
|
for (const invitation of allInvitations) {
|
||||||
|
if (invitation.isExpired()) {
|
||||||
|
invitation.data.status = 'expired';
|
||||||
|
await invitation.save();
|
||||||
|
cleanedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send invitation email
|
||||||
|
*/
|
||||||
|
private async sendInvitationEmail(
|
||||||
|
invitation: UserInvitation,
|
||||||
|
organizationId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const org = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
||||||
|
id: organizationId,
|
||||||
|
});
|
||||||
|
const orgName = org?.data.name || 'an organization';
|
||||||
|
|
||||||
|
await this.receptionRef.receptionMailer.sendInvitationEmail(
|
||||||
|
invitation.data.email,
|
||||||
|
orgName,
|
||||||
|
invitation.data.token,
|
||||||
|
this.receptionRef.options.baseUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify user is admin/owner of organization
|
||||||
|
*/
|
||||||
|
private async verifyUserIsAdminOfOrg(userId: string, organizationId: string): Promise<void> {
|
||||||
|
const role = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: { userId, organizationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Not a member of this organization.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAdminRole = role.data.roles.some(r =>
|
||||||
|
['owner', 'admin'].includes(r)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasAdminRole) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
|
'You do not have permission to perform this action.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify user is member of organization
|
||||||
|
*/
|
||||||
|
private async verifyUserIsMemberOfOrg(userId: string, organizationId: string): Promise<void> {
|
||||||
|
const role = await this.receptionRef.roleManager.CRole.getInstance({
|
||||||
|
data: { userId, organizationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Not a member of this organization.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,6 +180,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/users', async () => {
|
||||||
|
viewcontainer.classList.add('changing');
|
||||||
|
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||||
|
console.log('We are viewing the users page');
|
||||||
|
await cleanupViews();
|
||||||
|
viewcontainer.append(new views.UsersView());
|
||||||
|
viewcontainer.classList.remove('changing');
|
||||||
|
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||||
|
});
|
||||||
|
|
||||||
this.subrouter.on('/admin', async () => {
|
this.subrouter.on('/admin', async () => {
|
||||||
viewcontainer.classList.add('changing');
|
viewcontainer.classList.add('changing');
|
||||||
await this.domtools.convenience.smartdelay.delayFor(300);
|
await this.domtools.convenience.smartdelay.delayFor(300);
|
||||||
|
|||||||
@@ -279,8 +279,8 @@ export class LeleAccountNavigation extends DeesElement {
|
|||||||
Apps
|
Apps
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="navigationOption"
|
class="navigationOption ${this.isActive('users') ? 'active' : ''}"
|
||||||
@click=${async () => {}}
|
@click=${() => this.navigateToOrgPage('users')}
|
||||||
>
|
>
|
||||||
<dees-icon .icon=${'lucide:users'}></dees-icon>
|
<dees-icon .icon=${'lucide:users'}></dees-icon>
|
||||||
Users
|
Users
|
||||||
|
|||||||
@@ -617,8 +617,7 @@ export class AdminView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>(
|
||||||
'/typedrequest',
|
|
||||||
'getGlobalAppStats'
|
'getGlobalAppStats'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -644,8 +643,7 @@ export class AdminView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_CreateGlobalApp>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateGlobalApp>(
|
||||||
'/typedrequest',
|
|
||||||
'createGlobalApp'
|
'createGlobalApp'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -682,8 +680,7 @@ export class AdminView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_UpdateGlobalApp>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateGlobalApp>(
|
||||||
'/typedrequest',
|
|
||||||
'updateGlobalApp'
|
'updateGlobalApp'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -717,8 +714,7 @@ export class AdminView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_RegenerateAppCredentials>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RegenerateAppCredentials>(
|
||||||
'/typedrequest',
|
|
||||||
'regenerateAppCredentials'
|
'regenerateAppCredentials'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -739,8 +735,7 @@ export class AdminView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_DeleteGlobalApp>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteGlobalApp>(
|
||||||
'/typedrequest',
|
|
||||||
'deleteGlobalApp'
|
'deleteGlobalApp'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -374,8 +374,7 @@ export class AppsView extends DeesElement {
|
|||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
// Fetch global apps
|
// Fetch global apps
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalApps>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalApps>(
|
||||||
'/typedrequest',
|
|
||||||
'getGlobalApps'
|
'getGlobalApps'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -384,8 +383,7 @@ export class AppsView extends DeesElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch connections for this organization
|
// Fetch connections for this organization
|
||||||
const connectionsRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
const connectionsRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
||||||
'/typedrequest',
|
|
||||||
'getAppConnections'
|
'getAppConnections'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -424,8 +422,7 @@ export class AppsView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_ToggleAppConnection>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ToggleAppConnection>(
|
||||||
'/typedrequest',
|
|
||||||
'toggleAppConnection'
|
'toggleAppConnection'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export class BaseView extends DeesElement {
|
|||||||
<div class="org-list">
|
<div class="org-list">
|
||||||
${this.organizations.map((org) => {
|
${this.organizations.map((org) => {
|
||||||
const roleObj = this.roles.find(r => r.data.organizationId === org.id);
|
const roleObj = this.roles.find(r => r.data.organizationId === org.id);
|
||||||
const roleName = roleObj?.data.role || 'member';
|
const roleName = roleObj?.data.roles?.[0] || 'member';
|
||||||
const roleClass = roleName === 'owner' ? 'owner' :
|
const roleClass = roleName === 'owner' ? 'owner' :
|
||||||
roleName === 'admin' ? 'admin' : '';
|
roleName === 'admin' ? 'admin' : '';
|
||||||
const roleDisplay = roleName.charAt(0).toUpperCase() + roleName.slice(1);
|
const roleDisplay = roleName.charAt(0).toUpperCase() + roleName.slice(1);
|
||||||
@@ -754,8 +754,7 @@ export class BaseView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>(
|
||||||
'/typedrequest',
|
|
||||||
'getUserSessions'
|
'getUserSessions'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -772,8 +771,7 @@ export class BaseView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetUserActivity>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserActivity>(
|
||||||
'/typedrequest',
|
|
||||||
'getUserActivity'
|
'getUserActivity'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -794,8 +792,7 @@ export class BaseView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const typedRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>(
|
const typedRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>(
|
||||||
'/typedrequest',
|
|
||||||
'revokeSession'
|
'revokeSession'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './orgsetup.js';
|
|||||||
export * from './orgview.js';
|
export * from './orgview.js';
|
||||||
export * from './paddlesetup.js';
|
export * from './paddlesetup.js';
|
||||||
export * from './subscriptions.js';
|
export * from './subscriptions.js';
|
||||||
|
export * from './usersview.js';
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ export class OrgView extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleName = this.userRole?.data.role || 'member';
|
const roleName = this.userRole?.data.roles?.[0] || 'member';
|
||||||
const roleClass = roleName === 'owner' ? 'owner' : roleName === 'admin' ? 'admin' : '';
|
const roleClass = roleName === 'owner' ? 'owner' : roleName === 'admin' ? 'admin' : '';
|
||||||
const roleDisplay = roleName.charAt(0).toUpperCase() + roleName.slice(1);
|
const roleDisplay = roleName.charAt(0).toUpperCase() + roleName.slice(1);
|
||||||
|
|
||||||
@@ -472,8 +472,7 @@ export class OrgView extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const jwt = await idpState.idpClient.getJwt();
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
const connectionsRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
const connectionsRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
||||||
'/typedrequest',
|
|
||||||
'getAppConnections'
|
'getAppConnections'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,836 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
DeesElement,
|
||||||
|
html,
|
||||||
|
cssManager,
|
||||||
|
css,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} 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-users': UsersView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMemberDisplay {
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
isOwner: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IInvitationDisplay {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
invitedAt: number;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('lele-accountview-users')
|
||||||
|
export class UsersView extends DeesElement {
|
||||||
|
@state()
|
||||||
|
accessor members: IMemberDisplay[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor invitations: IInvitationDisplay[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor loading: boolean = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor activeTab: 'members' | 'pending' | 'invite' = 'members';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor organizationId: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor organizationName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor inviteEmail: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor inviteRoles: string[] = ['viewer'];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor isAdmin: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor currentUserId: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor submitting: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor actionMessage: { type: 'success' | 'error'; text: string } | null = null;
|
||||||
|
|
||||||
|
private static readonly AVAILABLE_ROLES = ['owner', 'admin', 'editor', 'viewer', 'guest'];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card:hover {
|
||||||
|
border-color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-email {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-roles {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.owner {
|
||||||
|
background: rgba(234, 179, 8, 0.2);
|
||||||
|
color: #eab308;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.admin {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.editor {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.viewer {
|
||||||
|
background: rgba(148, 163, 184, 0.2);
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge.guest {
|
||||||
|
background: rgba(168, 162, 158, 0.2);
|
||||||
|
color: #a8a29e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
border-color: var(--foreground);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.danger:hover {
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invitation-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invitation-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invitation-email {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invitation-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-form {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--foreground);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-option {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-option:hover {
|
||||||
|
border-color: var(--foreground);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-option.selected {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.you-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #3b82f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<h1>Users</h1>
|
||||||
|
<p>Manage members and invitations for ${this.organizationName || 'your organization'}.</p>
|
||||||
|
|
||||||
|
${this.actionMessage ? html`
|
||||||
|
<div class="message ${this.actionMessage.type}">${this.actionMessage.text}</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
class="tab ${this.activeTab === 'members' ? 'active' : ''}"
|
||||||
|
@click=${() => this.activeTab = 'members'}
|
||||||
|
>
|
||||||
|
Members (${this.members.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab ${this.activeTab === 'pending' ? 'active' : ''}"
|
||||||
|
@click=${() => this.activeTab = 'pending'}
|
||||||
|
>
|
||||||
|
Pending (${this.invitations.length})
|
||||||
|
</button>
|
||||||
|
${this.isAdmin ? html`
|
||||||
|
<button
|
||||||
|
class="tab ${this.activeTab === 'invite' ? 'active' : ''}"
|
||||||
|
@click=${() => this.activeTab = 'invite'}
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.renderTabContent()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTabContent() {
|
||||||
|
if (this.loading) {
|
||||||
|
return html`
|
||||||
|
<div class="loading">
|
||||||
|
<span>Loading users...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.activeTab) {
|
||||||
|
case 'members':
|
||||||
|
return this.renderMembers();
|
||||||
|
case 'pending':
|
||||||
|
return this.renderPendingInvitations();
|
||||||
|
case 'invite':
|
||||||
|
return this.renderInviteForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMembers() {
|
||||||
|
if (this.members.length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<dees-icon .icon=${'lucide:users'}></dees-icon>
|
||||||
|
<h2>No Members</h2>
|
||||||
|
<p>This organization has no members yet.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="member-list">
|
||||||
|
${this.members.map(member => html`
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-avatar">
|
||||||
|
${member.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div class="member-details">
|
||||||
|
<span class="member-name">
|
||||||
|
${member.name}
|
||||||
|
${member.userId === this.currentUserId ? html`<span class="you-badge">You</span>` : ''}
|
||||||
|
</span>
|
||||||
|
<span class="member-email">${member.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-roles">
|
||||||
|
${member.roles.map(role => html`
|
||||||
|
<span class="role-badge ${role}">${role}</span>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
${this.isAdmin && member.userId !== this.currentUserId ? html`
|
||||||
|
<div class="member-actions">
|
||||||
|
<button
|
||||||
|
class="action-button danger"
|
||||||
|
@click=${() => this.handleRemoveMember(member.userId, member.name)}
|
||||||
|
?disabled=${this.submitting || member.isOwner}
|
||||||
|
title=${member.isOwner ? 'Cannot remove owner' : 'Remove member'}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPendingInvitations() {
|
||||||
|
if (this.invitations.length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<dees-icon .icon=${'lucide:mail'}></dees-icon>
|
||||||
|
<h2>No Pending Invitations</h2>
|
||||||
|
<p>There are no pending invitations for this organization.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="member-list">
|
||||||
|
${this.invitations.map(inv => html`
|
||||||
|
<div class="invitation-card">
|
||||||
|
<div class="invitation-info">
|
||||||
|
<span class="invitation-email">${inv.email}</span>
|
||||||
|
<span class="invitation-meta">
|
||||||
|
Invited ${this.formatDate(inv.invitedAt)} · Expires ${this.formatDate(inv.expiresAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="member-roles">
|
||||||
|
${inv.roles.map(role => html`
|
||||||
|
<span class="role-badge ${role}">${role}</span>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
${this.isAdmin ? html`
|
||||||
|
<div class="member-actions">
|
||||||
|
<button
|
||||||
|
class="action-button"
|
||||||
|
@click=${() => this.handleResendInvitation(inv.id)}
|
||||||
|
?disabled=${this.submitting}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-button danger"
|
||||||
|
@click=${() => this.handleCancelInvitation(inv.id, inv.email)}
|
||||||
|
?disabled=${this.submitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderInviteForm(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="invite-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email Address</label>
|
||||||
|
<dees-input-text
|
||||||
|
.label=${''}
|
||||||
|
.placeholder=${'Enter email address'}
|
||||||
|
.value=${this.inviteEmail}
|
||||||
|
?disabled=${this.submitting}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Role</label>
|
||||||
|
<div class="role-selector">
|
||||||
|
${UsersView.AVAILABLE_ROLES.filter(r => r !== 'owner').map(role => html`
|
||||||
|
<button
|
||||||
|
class="role-option ${this.inviteRoles.includes(role) ? 'selected' : ''}"
|
||||||
|
@click=${() => this.toggleRole(role)}
|
||||||
|
?disabled=${this.submitting}
|
||||||
|
>
|
||||||
|
${role}
|
||||||
|
</button>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dees-button
|
||||||
|
.text=${'Send Invitation'}
|
||||||
|
.status=${this.submitting ? 'pending' : 'normal'}
|
||||||
|
@click=${() => this.handleSendInvitation()}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async firstUpdated() {
|
||||||
|
// Subscribe to email input changes
|
||||||
|
await this.updateComplete;
|
||||||
|
const emailInput = this.shadowRoot?.querySelector('dees-input-text') as any;
|
||||||
|
if (emailInput) {
|
||||||
|
emailInput.changeSubject?.subscribe((element: any) => {
|
||||||
|
this.inviteEmail = element.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the organization from 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;
|
||||||
|
this.organizationName = selectedOrg.data.name;
|
||||||
|
this.currentUserId = currentState.user?.id || '';
|
||||||
|
|
||||||
|
// Check if current user is admin
|
||||||
|
const currentUserRole = currentState.roles.find(
|
||||||
|
r => r.data.organizationId === this.organizationId && r.data.userId === this.currentUserId
|
||||||
|
);
|
||||||
|
this.isAdmin = currentUserRole?.data?.roles?.some(r => ['owner', 'admin'].includes(r)) ?? false;
|
||||||
|
|
||||||
|
// Get JWT from IdpState
|
||||||
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
|
// Fetch members
|
||||||
|
const membersRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgMembers>(
|
||||||
|
'getOrgMembers'
|
||||||
|
);
|
||||||
|
|
||||||
|
const membersResponse = await membersRequest.fire({
|
||||||
|
jwt,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.members = membersResponse.members.map(m => ({
|
||||||
|
userId: m.user.id,
|
||||||
|
name: m.user.data.name || m.user.data.username || 'Unknown',
|
||||||
|
email: m.user.data.email,
|
||||||
|
roles: m.role.data.roles || [],
|
||||||
|
isOwner: m.role.data.roles?.includes('owner') ?? false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Fetch invitations if admin
|
||||||
|
if (this.isAdmin) {
|
||||||
|
const invitationsRequest = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgInvitations>(
|
||||||
|
'getOrgInvitations'
|
||||||
|
);
|
||||||
|
|
||||||
|
const invitationsResponse = await invitationsRequest.fire({
|
||||||
|
jwt,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.invitations = invitationsResponse.invitations.map(inv => {
|
||||||
|
const orgRef = inv.data.organizationRefs.find(ref => ref.organizationId === this.organizationId);
|
||||||
|
return {
|
||||||
|
id: inv.id,
|
||||||
|
email: inv.data.email,
|
||||||
|
roles: orgRef?.roles || [],
|
||||||
|
invitedAt: orgRef?.invitedAt || inv.data.createdAt,
|
||||||
|
expiresAt: inv.data.expiresAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading users:', error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleRole(role: string) {
|
||||||
|
if (this.inviteRoles.includes(role)) {
|
||||||
|
this.inviteRoles = this.inviteRoles.filter(r => r !== role);
|
||||||
|
} else {
|
||||||
|
this.inviteRoles = [...this.inviteRoles, role];
|
||||||
|
}
|
||||||
|
// Ensure at least one role is selected
|
||||||
|
if (this.inviteRoles.length === 0) {
|
||||||
|
this.inviteRoles = ['viewer'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSendInvitation() {
|
||||||
|
if (!this.inviteEmail.trim()) {
|
||||||
|
this.showMessage('error', 'Please enter an email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inviteRoles.length === 0) {
|
||||||
|
this.showMessage('error', 'Please select at least one role.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true;
|
||||||
|
this.actionMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
|
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateInvitation>(
|
||||||
|
'createInvitation'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
jwt,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
email: this.inviteEmail.trim(),
|
||||||
|
roles: this.inviteRoles,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.showMessage('success', response.message || 'Invitation sent successfully!');
|
||||||
|
this.inviteEmail = '';
|
||||||
|
this.inviteRoles = ['viewer'];
|
||||||
|
await this.loadData();
|
||||||
|
this.activeTab = 'pending';
|
||||||
|
} else {
|
||||||
|
this.showMessage('error', response.message || 'Failed to send invitation.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending invitation:', error);
|
||||||
|
this.showMessage('error', 'Failed to send invitation. Please try again.');
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleResendInvitation(invitationId: string) {
|
||||||
|
this.submitting = true;
|
||||||
|
this.actionMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
|
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ResendInvitation>(
|
||||||
|
'resendInvitation'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
jwt,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
invitationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.showMessage('success', 'Invitation resent successfully!');
|
||||||
|
await this.loadData();
|
||||||
|
} else {
|
||||||
|
this.showMessage('error', response.message || 'Failed to resend invitation.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resending invitation:', error);
|
||||||
|
this.showMessage('error', 'Failed to resend invitation. Please try again.');
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCancelInvitation(invitationId: string, email: string) {
|
||||||
|
if (!confirm(`Cancel invitation for ${email}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true;
|
||||||
|
this.actionMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
|
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CancelInvitation>(
|
||||||
|
'cancelInvitation'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
jwt,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
invitationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.showMessage('success', 'Invitation cancelled.');
|
||||||
|
await this.loadData();
|
||||||
|
} else {
|
||||||
|
this.showMessage('error', response.message || 'Failed to cancel invitation.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cancelling invitation:', error);
|
||||||
|
this.showMessage('error', 'Failed to cancel invitation. Please try again.');
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRemoveMember(userId: string, name: string) {
|
||||||
|
if (!confirm(`Remove ${name} from this organization?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true;
|
||||||
|
this.actionMessage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
|
const jwt = await idpState.idpClient.getJwt();
|
||||||
|
|
||||||
|
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RemoveMember>(
|
||||||
|
'removeMember'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
jwt,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.showMessage('success', `${name} has been removed from the organization.`);
|
||||||
|
await this.loadData();
|
||||||
|
} else {
|
||||||
|
this.showMessage('error', response.message || 'Failed to remove member.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing member:', error);
|
||||||
|
this.showMessage('error', 'Failed to remove member. Please try again.');
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showMessage(type: 'success' | 'error', text: string) {
|
||||||
|
this.actionMessage = { type, text };
|
||||||
|
// Auto-hide after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.actionMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(timestamp: number): string {
|
||||||
|
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -174,13 +174,11 @@ export class IdpLoginPrompt extends DeesElement {
|
|||||||
const idpState = await IdpState.getSingletonInstance();
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const loginForm: DeesForm = this.shadowRoot.querySelector('#loginForm');
|
const loginForm: DeesForm = this.shadowRoot.querySelector('#loginForm');
|
||||||
const loginRequestWithUsernameAndPassword =
|
const loginRequestWithUsernameAndPassword =
|
||||||
new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
|
idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
|
||||||
'/typedrequest',
|
|
||||||
'loginWithEmailOrUsernameAndPassword'
|
'loginWithEmailOrUsernameAndPassword'
|
||||||
);
|
);
|
||||||
const loginRequestWithEmail =
|
const loginRequestWithEmail =
|
||||||
new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmail>(
|
idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmail>(
|
||||||
'/typedrequest',
|
|
||||||
'loginWithEmail'
|
'loginWithEmail'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -170,9 +170,9 @@ export class IdpRegistrationPrompt extends DeesElement {
|
|||||||
private register = async (valueArg: { emailAddress: string }) => {
|
private register = async (valueArg: { emailAddress: string }) => {
|
||||||
const registrationForm: DeesForm = this.shadowRoot.querySelector('#registrationForm');
|
const registrationForm: DeesForm = this.shadowRoot.querySelector('#registrationForm');
|
||||||
registrationForm.setStatus('pending', 'registering...');
|
registrationForm.setStatus('pending', 'registering...');
|
||||||
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
const firstSignupRequest =
|
const firstSignupRequest =
|
||||||
new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_FirstRegistration>(
|
idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_FirstRegistration>(
|
||||||
'/typedrequest',
|
|
||||||
'firstRegistrationRequest'
|
'firstRegistrationRequest'
|
||||||
);
|
);
|
||||||
const response = await firstSignupRequest
|
const response = await firstSignupRequest
|
||||||
@@ -209,8 +209,8 @@ export class IdpRegistrationPrompt extends DeesElement {
|
|||||||
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
|
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
|
||||||
// a refreshToken binds directly to a session.
|
// a refreshToken binds directly to a session.
|
||||||
// the refresh token is used on a continuous basis to get fresh and short-lived jwts
|
// the refresh token is used on a continuous basis to get fresh and short-lived jwts
|
||||||
const refreshJwt = new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
const idpState = await IdpState.getSingletonInstance();
|
||||||
'/typedrequest',
|
const refreshJwt = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
|
||||||
'refreshJwt'
|
'refreshJwt'
|
||||||
);
|
);
|
||||||
const responseJwt = await refreshJwt.fire({
|
const responseJwt = await refreshJwt.fire({
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ const run = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// const serviceWorker = await serviceworker.getServiceworkerClient();
|
await serviceworker.getServiceworkerClient();
|
||||||
|
|
||||||
const mainTemplate = html`
|
const mainTemplate = html`
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class IdpState {
|
|||||||
}>
|
}>
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
this.idpClient.enableTypedSocket();
|
await this.idpClient.enableTypedSocket();
|
||||||
const domtoolsInstance = await domtools.DomTools.setupDomTools();
|
const domtoolsInstance = await domtools.DomTools.setupDomTools();
|
||||||
this.domtools = domtoolsInstance;
|
this.domtools = domtoolsInstance;
|
||||||
const state = new plugins.deesDomtools.plugins.smartstate.Smartstate<'main'>();
|
const state = new plugins.deesDomtools.plugins.smartstate.Smartstate<'main'>();
|
||||||
|
|||||||
Reference in New Issue
Block a user