95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
|
|
import * as plugins from './plugins.js';
|
||
|
|
import { IdpSdkAccountDoc, setAccountDocSmartdataDb } from './classes.account-doc.js';
|
||
|
|
import { PasswordHasher } from './classes.password-hasher.js';
|
||
|
|
import type { ICreateIdpSdkAccountOptions, IIdpSdkAccount, TIdpAccountAuthSource } from './interfaces.js';
|
||
|
|
|
||
|
|
export class SmartdataAccountStore {
|
||
|
|
constructor(private optionsArg: { smartdataDb: plugins.smartdata.SmartdataDb }) {
|
||
|
|
setAccountDocSmartdataDb(optionsArg.smartdataDb);
|
||
|
|
}
|
||
|
|
|
||
|
|
public normalizeEmail(emailArg: string): string {
|
||
|
|
return emailArg.trim().toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
public async createAccount(optionsArg: ICreateIdpSdkAccountOptions): Promise<IIdpSdkAccount> {
|
||
|
|
const emailNormalized = this.normalizeEmail(optionsArg.email);
|
||
|
|
if (!emailNormalized || !emailNormalized.includes('@')) {
|
||
|
|
throw new Error('A valid account email is required');
|
||
|
|
}
|
||
|
|
const existing = await IdpSdkAccountDoc.findByEmailNormalized(emailNormalized);
|
||
|
|
if (existing) {
|
||
|
|
throw new Error(`Account already exists for ${emailNormalized}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const authSources = this.normalizeAuthSources(optionsArg.authSources);
|
||
|
|
if (authSources.length === 0) {
|
||
|
|
throw new Error('At least one auth source is required');
|
||
|
|
}
|
||
|
|
if (authSources.includes('local') && !optionsArg.password) {
|
||
|
|
throw new Error('A local password is required for local auth');
|
||
|
|
}
|
||
|
|
|
||
|
|
const now = Date.now();
|
||
|
|
const doc = new IdpSdkAccountDoc();
|
||
|
|
doc.id = plugins.crypto.randomUUID();
|
||
|
|
doc.email = optionsArg.email.trim();
|
||
|
|
doc.emailNormalized = emailNormalized;
|
||
|
|
doc.name = optionsArg.name.trim() || doc.email;
|
||
|
|
doc.role = optionsArg.role;
|
||
|
|
doc.status = optionsArg.status || 'active';
|
||
|
|
doc.authSources = authSources;
|
||
|
|
doc.passwordHash = optionsArg.password ? await PasswordHasher.hashPassword(optionsArg.password) : undefined;
|
||
|
|
doc.idpSubject = optionsArg.idpSubject;
|
||
|
|
doc.createdAt = now;
|
||
|
|
doc.updatedAt = now;
|
||
|
|
await doc.save();
|
||
|
|
return doc.toAccount();
|
||
|
|
}
|
||
|
|
|
||
|
|
public async getAccountByEmail(emailArg: string): Promise<IIdpSdkAccount | null> {
|
||
|
|
const doc = await IdpSdkAccountDoc.findByEmailNormalized(this.normalizeEmail(emailArg));
|
||
|
|
return doc?.toAccount() || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async getAccountById(idArg: string): Promise<IIdpSdkAccount | null> {
|
||
|
|
const doc = await IdpSdkAccountDoc.findById(idArg);
|
||
|
|
return doc?.toAccount() || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async listAccounts(): Promise<IIdpSdkAccount[]> {
|
||
|
|
const docs = await IdpSdkAccountDoc.getInstances({});
|
||
|
|
return docs.map((docArg) => docArg.toAccount());
|
||
|
|
}
|
||
|
|
|
||
|
|
public async hasActiveAdminAccount(): Promise<boolean> {
|
||
|
|
const admins = await IdpSdkAccountDoc.findAdmins();
|
||
|
|
return admins.length > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async verifyLocalPassword(accountArg: IIdpSdkAccount, passwordArg: string): Promise<boolean> {
|
||
|
|
if (accountArg.status !== 'active' || !accountArg.authSources.includes('local')) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return PasswordHasher.verifyPassword(passwordArg, accountArg.passwordHash);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async updateLoginState(accountIdArg: string, patchArg: { idpSubject?: string }): Promise<IIdpSdkAccount | null> {
|
||
|
|
const doc = await IdpSdkAccountDoc.findById(accountIdArg);
|
||
|
|
if (!doc) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (patchArg.idpSubject !== undefined) {
|
||
|
|
doc.idpSubject = patchArg.idpSubject;
|
||
|
|
}
|
||
|
|
doc.lastLoginAt = Date.now();
|
||
|
|
doc.updatedAt = Date.now();
|
||
|
|
await doc.save();
|
||
|
|
return doc.toAccount();
|
||
|
|
}
|
||
|
|
|
||
|
|
private normalizeAuthSources(authSourcesArg: TIdpAccountAuthSource[]): TIdpAccountAuthSource[] {
|
||
|
|
return [...new Set(authSourcesArg.filter((sourceArg) => sourceArg === 'local' || sourceArg === 'idp.global'))];
|
||
|
|
}
|
||
|
|
}
|