137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
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();
|
|
}
|
|
}
|