diff --git a/packages/auth/src/TwoFactor.ts b/packages/auth/src/TwoFactor.ts index aa99b68c..e886cd11 100644 --- a/packages/auth/src/TwoFactor.ts +++ b/packages/auth/src/TwoFactor.ts @@ -1,30 +1,25 @@ import { Encryption, Hash, env, getModel } from '@arkstack/common' -import type { IssuedSmsCode, SmsCodePurpose, TwoFactorMethod, TwoFactorSetup, TwoFactorStatus } from './types/TwoFactor' +import type { IssuedSmsCode, SmsCodePurpose, TwoFactorMethod, TwoFactorSetup, TwoFactorStatus, TwoFactorUser } from './types/TwoFactor' import { Secret } from 'otpauth' import type { User } from '@app/models/User' import type { UserTwoFactor } from '@app/models/UserTwoFactor' import { randomBytes } from 'node:crypto' -type TwoFactorUser = User & { - phone?: string | null -} - - export class TwoFactor { static smsCodeTtlMinutes: number = Number(env('TWO_FACTOR_SMS_TTL_MINUTES', 10)) || 10 - private static async getModel () { - return await getModel('UserTwoFactor') + private static async getModel() { + return getModel('UserTwoFactor') } - private static async getRecord (userId: User['id']) { + private static async getRecord(userId: User['id']): Promise { const Model = await this.getModel() - return await Model.query().where({ userId }).first() + return await Model.query().where({ userId }).first() as never } - private static async upsert ( + private static async upsert( userId: User['id'], attributes: Partial { const left = randomBytes(3).toString('hex').slice(0, 4).toUpperCase() const right = randomBytes(3).toString('hex').slice(0, 4).toUpperCase() return `${left}-${right}` }) - - // return Array.from({ length: 8 }, () => { - // const left = Math.random().toString(36).slice(2, 6).toUpperCase() - // const right = Math.random().toString(36).slice(2, 6).toUpperCase() - - // return `${left}-${right}` - // }) } /** @@ -215,7 +204,7 @@ export class TwoFactor { * @param codes An array of recovery codes to hash. * @returns An array of hashed recovery codes. */ - static async hashBackupCodes (codes: string[]) { + static async hashBackupCodes(codes: string[]) { return await Promise.all(codes.map(async code => await Hash.make(code))) } @@ -225,7 +214,7 @@ export class TwoFactor { * @param userId The ID of the user. * @returns An array of recovery-code hashes. */ - static async readRecoveryCodeHashes (userId: User['id']) { + static async readRecoveryCodeHashes(userId: User['id']) { const record = await this.getRecord(userId) return record?.recoveryCodeHashes ?? [] @@ -237,7 +226,7 @@ export class TwoFactor { * @param userId * @param hashes */ - static async writeRecoveryCodeHashes (userId: User['id'], hashes: string[]) { + static async writeRecoveryCodeHashes(userId: User['id'], hashes: string[]) { await this.upsert(userId, { recoveryCodeHashes: hashes }) } @@ -248,7 +237,7 @@ export class TwoFactor { * @param recoveryCode The recovery code to consume. * @returns True if the recovery code was valid and consumed, false otherwise. */ - static async consumeRecoveryCode (userId: User['id'], recoveryCode: string) { + static async consumeRecoveryCode(userId: User['id'], recoveryCode: string) { const hashes = await this.readRecoveryCodeHashes(userId) for (const [index, hash] of hashes.entries()) { @@ -271,7 +260,7 @@ export class TwoFactor { * @param userId The ID of the user. * @returns An object containing the 2FA status and recovery codes remaining. */ - static async readStatus (userId: User['id']): Promise { + static async readStatus(userId: User['id']): Promise { const record = await this.getRecord(userId) const enabledAt = record?.enabledAt?.toISOString() ?? null const recoveryCodes = record?.recoveryCodeHashes ?? [] @@ -284,7 +273,7 @@ export class TwoFactor { } } - static createSmsCode () { + static createSmsCode() { return Math.floor(100000 + Math.random() * 900000).toString() } @@ -294,7 +283,7 @@ export class TwoFactor { * @param user * @param purpose */ - static async issueSmsCode (user: User, purpose: SmsCodePurpose): Promise { + static async issueSmsCode(user: User, purpose: SmsCodePurpose): Promise { if (!(user as TwoFactorUser).phone) { throw new Error('A phone number is required to issue a two-factor SMS code.') } @@ -316,7 +305,7 @@ export class TwoFactor { } } - static async clearSmsCode (userId: User['id']) { + static async clearSmsCode(userId: User['id']) { await this.upsert(userId, { smsCodeHash: null, smsCodeExpiresAt: null, @@ -332,7 +321,7 @@ export class TwoFactor { * @param purpose * @returns */ - static async verifySmsCode (userId: User['id'], code: string, purpose: SmsCodePurpose) { + static async verifySmsCode(userId: User['id'], code: string, purpose: SmsCodePurpose) { const record = await this.getRecord(userId) if (!record?.smsCodeHash || !record.smsCodeExpiresAt || record.smsCodePurpose !== purpose) { diff --git a/packages/auth/src/types/TwoFactor.ts b/packages/auth/src/types/TwoFactor.ts index dcac1ffb..3446f6c0 100644 --- a/packages/auth/src/types/TwoFactor.ts +++ b/packages/auth/src/types/TwoFactor.ts @@ -1,3 +1,4 @@ +import type { User } from '@app/models/User' export type TwoFactorMethod = 'authenticator' | 'sms' export type SmsCodePurpose = 'setup' | 'login' @@ -19,3 +20,7 @@ export type IssuedSmsCode = { expiresAt: Date purpose: SmsCodePurpose } + +export type TwoFactorUser = User & { + phone?: string | null +} \ No newline at end of file diff --git a/packages/common/src/utils/helpers.ts b/packages/common/src/utils/helpers.ts index c3f41e0f..b9449607 100644 --- a/packages/common/src/utils/helpers.ts +++ b/packages/common/src/utils/helpers.ts @@ -1,10 +1,7 @@ -import type { Model, ModelStatic } from 'arkormx' -import { importFile, resolveRuntimeModule } from '../system' -import path from 'node:path' -import { Arkstack } from '@arkstack/contract' +import type { Model, ModelStatic, RegisteredModelClass, RegisteredModelName, RelatedModelClass } from 'arkormx' +import { getModel as getArkormxModel } from 'arkormx' import { RequestException } from '../Exceptions/RequestException' -import { createRequire } from 'node:module' import { PaginationOptions } from '../types' export type AbstractModelConstructor = @@ -17,11 +14,6 @@ export type ModelConstructor = export interface ModelRegistry { } -type ModelName = Extract -type ModelModule = Record & { - default?: unknown; -} - /** * Checks and asserts if target is a class * @@ -106,64 +98,20 @@ export const resolvePagination = ( } /** - * Import an application model by name. + * Synchronously resolve an application model by name. * - * Apps can augment `ModelRegistry` to make `getModel('User')` return `typeof User`. - * Without a registry entry, pass the class type explicitly: `getModel('User')`. - * - * @param modelName + * Registered models are returned first. If a model has not been registered yet, + * ArkORM loads it from the configured models paths, registers it, and returns + * the matching constructor. + * + * @param modelName + * @alias {@link getArkormxModel} + * @returns */ -export async function getModel( - modelName: TName -): Promise -export async function getModel( - modelName: string -): Promise -export async function getModel(modelName: string) { - const resolveModelExport = (module: ModelModule | unknown, modelName: string) => { - if (!isModelModule(module)) { - return module - } - - return module.default ?? module[modelName] ?? module - } - - const isModelModule = (value: unknown): value is ModelModule => ( - typeof value === 'object' && value !== null - ) - - const { getUserConfig } = await import('arkormx') - const modelPath = getUserConfig().paths?.models || './src/models' - const sourcePath = path.join( - path.isAbsolute(modelPath) ? modelPath : path.join(Arkstack.rootDir(), modelPath), - modelName - ) - // In production the source tree is absent; resolve to the build output. - const modulePath = resolveRuntimeModule(sourcePath) - const module = await importFile(modulePath) - const exportName = path.basename(modelName, path.extname(modelName)) - const model = resolveModelExport(module, exportName) - - if (typeof model !== 'function') { - throw new Error(`Model "${modelName}" not found`) - } - - return model -} - -const isModelModule = (value: unknown): value is ModelModule => ( - typeof value === 'object' && value !== null -) - -const resolveModelExport = ( - module: ModelModule | unknown, - modelName: string -) => { - if (!isModelModule(module)) { - return module - } - - return module.default ?? module[modelName] ?? module +export function getModel(modelName: TName): RegisteredModelClass +export function getModel(modelName: string): TModel; +export function getModel(modelName: string): TModel { + return getArkormxModel(modelName) } /** @@ -173,57 +121,13 @@ const resolveModelExport = ( * Without a registry entry, pass the class type explicitly: `getModel('User')`. * * @param modelName + * @alias {@link getArkormxModel} + * @deprecated 0.17.27 - Use {@link getModel} or {@link getArkormxModel} */ -export function getModelSync( - modelName: TName -): ModelRegistry[TName] -export function getModelSync< - TModel extends AbstractModelConstructor = ModelConstructor ->( - modelName: string -): TModel -export function getModelSync(modelName: string) { - const require = createRequire(import.meta.url) - - const { Arkorm, getUserConfig } = require('arkormx') as typeof import('arkormx') - - const exportName = path.basename( - modelName, - path.extname(modelName) - ) - - /* - * Prefer models that have already been loaded and registered. - */ - const registeredModel = Arkorm - .getRegisteredModels() - .find((model) => model.name === exportName) - - if (registeredModel) { - return registeredModel - } - - const modelPath = getUserConfig().paths?.models || './src/models' - - const sourcePath = path.join( - path.isAbsolute(modelPath) - ? modelPath - : path.join(Arkstack.rootDir(), modelPath), - modelName - ) - - /* - * In production, resolve the corresponding build output. - */ - const modulePath = resolveRuntimeModule(sourcePath) - const module = require(modulePath) as ModelModule | unknown - const model = resolveModelExport(module, exportName) - - if (typeof model !== 'function') { - throw new Error(`Model "${modelName}" not found`) - } - - return model +export function getModelSync(modelName: TName): RegisteredModelClass +export function getModelSync(modelName: string): TModel; +export function getModelSync(modelName: string): TModel { + return getArkormxModel(modelName) } export const initializeGlobalContext = async (