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 { 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 { const doc = await IdpSdkAccountDoc.findByEmailNormalized(this.normalizeEmail(emailArg)); return doc?.toAccount() || null; } public async getAccountById(idArg: string): Promise { const doc = await IdpSdkAccountDoc.findById(idArg); return doc?.toAccount() || null; } public async listAccounts(): Promise { const docs = await IdpSdkAccountDoc.getInstances({}); return docs.map((docArg) => docArg.toAccount()); } public async hasActiveAdminAccount(): Promise { const admins = await IdpSdkAccountDoc.findAdmins(); return admins.length > 0; } public async verifyLocalPassword(accountArg: IIdpSdkAccount, passwordArg: string): Promise { 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 { 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'))]; } }