alepha 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/api-files/index.browser.js +80 -0
- package/dist/api-files/index.browser.js.map +1 -0
- package/dist/api-files/index.d.ts +175 -175
- package/dist/api-jobs/index.browser.js +56 -0
- package/dist/api-jobs/index.browser.js.map +1 -0
- package/dist/api-jobs/index.d.ts +156 -156
- package/dist/api-notifications/index.browser.js +382 -0
- package/dist/api-notifications/index.browser.js.map +1 -0
- package/dist/api-notifications/index.d.ts +221 -166
- package/dist/api-notifications/index.js +107 -55
- package/dist/api-notifications/index.js.map +1 -1
- package/dist/api-parameters/index.browser.js +29 -0
- package/dist/api-parameters/index.browser.js.map +1 -0
- package/dist/api-users/index.d.ts +16 -3
- package/dist/api-users/index.js +75 -28
- package/dist/api-users/index.js.map +1 -1
- package/dist/api-verifications/index.browser.js +52 -0
- package/dist/api-verifications/index.browser.js.map +1 -0
- package/dist/api-verifications/index.d.ts +120 -98
- package/dist/api-verifications/index.js +1 -1
- package/dist/api-verifications/index.js.map +1 -1
- package/dist/batch/index.js +0 -5
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.js +7 -5
- package/dist/bucket/index.js.map +1 -1
- package/dist/cli/{dist-Dl9Vl7Ur.js → dist-lGnqsKpu.js} +11 -15
- package/dist/cli/dist-lGnqsKpu.js.map +1 -0
- package/dist/cli/index.d.ts +26 -45
- package/dist/cli/index.js +40 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -0
- package/dist/command/index.js +9 -0
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +5 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +221 -219
- package/dist/core/index.js +5 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +5 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/email/index.d.ts +4 -4
- package/dist/email/index.js +5 -0
- package/dist/email/index.js.map +1 -1
- package/dist/orm/index.d.ts +19 -19
- package/dist/orm/index.js +3 -3
- package/dist/orm/index.js.map +1 -1
- package/dist/redis/index.d.ts +10 -10
- package/dist/security/index.d.ts +28 -28
- package/dist/security/index.js +3 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/index.d.ts +9 -9
- package/dist/server-auth/index.d.ts +152 -152
- package/dist/server-cookies/index.js +2 -2
- package/dist/server-cookies/index.js.map +1 -1
- package/dist/server-links/index.d.ts +33 -33
- package/dist/server-security/index.d.ts +9 -9
- package/dist/server-static/index.js +18 -2
- package/dist/server-static/index.js.map +1 -1
- package/package.json +16 -6
- package/src/api-files/index.browser.ts +17 -0
- package/src/api-jobs/index.browser.ts +15 -0
- package/src/api-notifications/controllers/NotificationController.ts +26 -1
- package/src/api-notifications/index.browser.ts +17 -0
- package/src/api-notifications/index.ts +1 -0
- package/src/api-notifications/schemas/notificationQuerySchema.ts +13 -0
- package/src/api-notifications/services/NotificationService.ts +45 -2
- package/src/api-parameters/index.browser.ts +12 -0
- package/src/api-users/atoms/realmAuthSettingsAtom.ts +3 -1
- package/src/api-users/controllers/UserController.ts +21 -1
- package/src/api-users/primitives/$userRealm.ts +33 -10
- package/src/api-users/providers/UserRealmProvider.ts +1 -0
- package/src/api-users/services/SessionService.ts +2 -0
- package/src/api-users/services/UserService.ts +56 -16
- package/src/api-verifications/index.browser.ts +15 -0
- package/src/api-verifications/index.ts +1 -0
- package/src/batch/providers/BatchProvider.ts +0 -7
- package/src/bucket/index.ts +7 -5
- package/src/cli/apps/AlephaCli.ts +27 -1
- package/src/cli/apps/AlephaPackageBuilderCli.ts +3 -0
- package/src/cli/commands/CoreCommands.ts +6 -2
- package/src/cli/commands/ViteCommands.ts +2 -1
- package/src/cli/services/ProjectUtils.ts +40 -75
- package/src/command/helpers/Asker.ts +10 -0
- package/src/core/Alepha.ts +14 -0
- package/src/core/primitives/$module.ts +1 -1
- package/src/email/index.ts +13 -5
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -3
- package/src/server-cookies/providers/ServerCookiesProvider.ts +2 -1
- package/src/server-static/providers/ServerStaticProvider.ts +18 -3
- package/dist/cli/dist-Dl9Vl7Ur.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["notifications"],"sources":["../../src/api-notifications/controllers/NotificationController.ts","../../src/api-notifications/jobs/NotificationJobs.ts","../../src/api-notifications/entities/notifications.ts","../../src/api-notifications/services/NotificationSenderService.ts","../../src/api-notifications/queues/NotificationQueues.ts","../../src/api-notifications/schemas/notificationCreateSchema.ts","../../src/api-notifications/services/NotificationService.ts","../../src/api-notifications/primitives/$notification.ts","../../src/api-notifications/schemas/notificationContactPreferencesSchema.ts","../../src/api-notifications/index.ts"],"sourcesContent":["export class NotificationController {}\n","export class NotificationJobs {\n // - retry (lost, failed) notifications\n // - purge old notifications\n}\n","import { type Static, t } from \"alepha\";\nimport { $entity, pg } from \"alepha/orm\";\n\nexport const notifications = $entity({\n name: \"notifications\",\n schema: t.object({\n id: pg.primaryKey(t.uuid()),\n\n version: pg.version(),\n\n createdAt: pg.createdAt(),\n\n updatedAt: pg.updatedAt(),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n type: t.enum([\"email\", \"sms\"]),\n\n template: t.text(), // e.g. 'resetPassword'\n\n category: t.optional(\n t.text({\n description:\n \"For grouping related notifications (e.g., 'authentication', 'marketing'). Contact can filter notifications by category.\",\n }),\n ),\n\n critical: t.optional(\n t.boolean({\n description:\n \"Prioritize delivery of this notification. Set to true for important system alerts.\",\n }),\n ),\n\n sensitive: t.optional(\n t.boolean({\n description:\n \"Message won't be logged or stored in plain text. Set to true when notification contains passwords or codes.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n contact: t.text(), // e.g. email address or phone number or user ID or whatever\n\n variables: t.optional(t.record(t.text(), t.any())),\n\n scheduledAt: t.optional(\n t.datetime({\n description:\n \"When set, the notification will be sent at or after this date/time.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n sentAt: t.optional(t.datetime()),\n\n error: t.optional(\n t.object({\n at: t.datetime(),\n name: t.text(),\n message: t.text({ size: \"rich\" }),\n }),\n ),\n\n // TODO: retryCount, lastRetryAt, etc.\n }),\n});\n\nexport type NotificationEntity = Static<typeof notifications.schema>;\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { SmsProvider } from \"alepha/sms\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { $notification } from \"../primitives/$notification.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(notificationId: string | NotificationEntity) {\n this.log.trace(\"Sending notification\", {\n notificationId:\n typeof notificationId === \"string\" ? notificationId : notificationId.id,\n });\n\n const notification =\n typeof notificationId === \"string\"\n ? await this.notificationRepository.findById(notificationId)\n : notificationId;\n\n if (notification.sentAt) {\n this.log.debug(\"Notification already sent\", {\n notificationId: notification.id,\n sentAt: notification.sentAt,\n });\n return;\n }\n\n this.log.debug(\"Processing notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n });\n\n try {\n if (notification.type === \"email\") {\n await this.emailProvider.send(this.renderEmail(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"Email notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n if (notification.type === \"sms\") {\n await this.smsProvider.send(this.renderSms(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"SMS notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n } catch (e) {\n this.log.error(\"Failed to send notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n error: e,\n });\n if (e instanceof Error) {\n notification.error = {\n at: this.dateTimeProvider.nowISOString(),\n name: e.name,\n message: e.message,\n };\n }\n } finally {\n await this.notificationRepository.save(notification);\n }\n }\n\n public renderSms(notification: NotificationEntity) {\n this.log.trace(\"Rendering SMS notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const sms = template.options.sms;\n if (!sms) {\n this.log.error(\"Notification template has no SMS defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no sms defined`,\n );\n }\n\n this.log.debug(\"Rendering SMS\", {\n template: notification.template,\n contact,\n });\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return {\n to: contact,\n message,\n };\n }\n\n public renderEmail(notification: NotificationEntity) {\n this.log.trace(\"Rendering email notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const email = template.options.email;\n if (!email) {\n this.log.error(\"Notification template has no email defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no email defined`,\n );\n }\n\n this.log.debug(\"Rendering email\", {\n template: notification.template,\n contact,\n subject: email.subject,\n });\n\n const subject = email.subject;\n\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return {\n to: contact,\n subject,\n body,\n };\n }\n\n protected load(notification: NotificationEntity) {\n const variables = notification.variables || {};\n const contact = notification.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === notification.template);\n\n if (!template) {\n this.log.error(\"Notification template not found\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `No notification template found for ${notification.template}`,\n );\n }\n\n return {\n template,\n variables,\n contact,\n };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $queue } from \"alepha/queue\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\nexport class NotificationQueues {\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly processNotification = $queue({\n description: \"Queue for processing notifications\",\n schema: t.object({\n notificationId: t.string({ format: \"uuid\" }),\n }),\n handler: async (message) => {\n await this.notificationSenderService.send(message.payload.notificationId);\n },\n });\n}\n","import { type Static, t } from \"alepha\";\nimport { notifications } from \"../entities/notifications.ts\";\n\nexport const notificationCreateSchema = t.pick(notifications.schema, [\n \"type\",\n \"contact\",\n \"template\",\n \"variables\",\n]);\n\nexport type NotificationCreate = Static<typeof notificationCreateSchema>;\n","import { $env, $inject, Alepha, type Static, t } from \"alepha\";\nimport { $batch } from \"alepha/batch\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { notifications } from \"../entities/notifications.ts\";\nimport { NotificationQueues } from \"../queues/NotificationQueues.ts\";\nimport {\n type NotificationCreate,\n notificationCreateSchema,\n} from \"../schemas/notificationCreateSchema.ts\";\nimport { NotificationSenderService } from \"./NotificationSenderService.ts\";\n\nexport const notificationServiceEnvSchema = t.object({\n NOTIFICATION_QUEUE: t.optional(\n t.boolean({\n description:\n \"If true, notifications will be queued instead of sent immediately\",\n }),\n ),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof notificationServiceEnvSchema>> {}\n}\n\nexport class NotificationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(notificationServiceEnvSchema);\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly notificationBatch = $batch({\n maxSize: 100,\n maxDuration: [15, \"seconds\"],\n schema: notificationCreateSchema,\n handler: async (notifications: NotificationCreate[]) => {\n this.log.debug(\"Processing notification batch\", {\n size: notifications.length,\n templates: [...new Set(notifications.map((n) => n.template))],\n });\n\n const entities =\n await this.notificationRepository.createMany(notifications);\n\n await this.alepha\n .inject(NotificationQueues)\n .processNotification.push(\n ...entities.map((it) => ({ notificationId: it.id })),\n );\n\n this.log.info(\"Notification batch queued\", {\n count: entities.length,\n ids: entities.map((it) => it.id),\n });\n },\n });\n\n public async findNotificationById(id: string) {\n this.log.trace(\"Finding notification by ID\", { id });\n return this.notificationRepository.findOne({ where: { id } });\n }\n\n /**\n * Create a new notification.\n */\n public async createNotification(entry: NotificationCreate): Promise<void> {\n this.log.trace(\"Creating notification\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n if (\n this.env.NOTIFICATION_QUEUE !== true ||\n this.alepha.isServerless() ||\n this.alepha.isTest()\n ) {\n this.log.debug(\"Sending notification immediately\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n const notification = await this.notificationRepository.create(entry);\n await this.notificationSenderService.send(notification);\n return;\n }\n\n this.log.debug(\"Queuing notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n this.notificationBatch.push(entry).catch((e) => {\n this.log.error(\"Failed to push notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n error: e,\n });\n });\n }\n}\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationService = $inject(NotificationService);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n if (this.options.email) {\n await this.notificationService.createNotification({\n ...options,\n type: \"email\",\n template: this.name,\n });\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { $module } from \"alepha\";\nimport { NotificationController } from \"./controllers/NotificationController.ts\";\nimport { NotificationJobs } from \"./jobs/NotificationJobs.ts\";\nimport { $notification } from \"./primitives/$notification.ts\";\nimport { NotificationQueues } from \"./queues/NotificationQueues.ts\";\nimport { NotificationSenderService } from \"./services/NotificationSenderService.ts\";\nimport {\n NotificationService,\n notificationServiceEnvSchema,\n} from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/NotificationController.ts\";\nexport * from \"./entities/notifications.ts\";\nexport * from \"./jobs/NotificationJobs.ts\";\nexport * from \"./primitives/$notification.ts\";\nexport * from \"./queues/NotificationQueues.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationCreateSchema.ts\";\nexport * from \"./services/NotificationSenderService.ts\";\nexport * from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Provides notification management API endpoints for Alepha applications.\n *\n * This module includes notification sending, retrieval, status tracking,\n * and user notification preferences management.\n *\n * Requires `AlephaSms` module to be loaded for SMS notifications.\n *\n * @module alepha.api.notifications\n */\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n primitives: [$notification],\n services: [\n NotificationController,\n NotificationService,\n NotificationSenderService,\n NotificationQueues,\n NotificationJobs,\n ],\n register: (alepha) => {\n const env = alepha.parseEnv(notificationServiceEnvSchema);\n if (env.NOTIFICATION_QUEUE) {\n alepha.with(NotificationQueues);\n }\n\n alepha\n .with(NotificationController)\n .with(NotificationService)\n .with(NotificationSenderService)\n .with(NotificationJobs);\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["notifications"],"sources":["../../src/api-notifications/entities/notifications.ts","../../src/api-notifications/schemas/notificationQuerySchema.ts","../../src/api-notifications/primitives/$notification.ts","../../src/api-notifications/services/NotificationSenderService.ts","../../src/api-notifications/queues/NotificationQueues.ts","../../src/api-notifications/schemas/notificationCreateSchema.ts","../../src/api-notifications/services/NotificationService.ts","../../src/api-notifications/controllers/NotificationController.ts","../../src/api-notifications/jobs/NotificationJobs.ts","../../src/api-notifications/schemas/notificationContactPreferencesSchema.ts","../../src/api-notifications/index.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, pg } from \"alepha/orm\";\n\nexport const notifications = $entity({\n name: \"notifications\",\n schema: t.object({\n id: pg.primaryKey(t.uuid()),\n\n version: pg.version(),\n\n createdAt: pg.createdAt(),\n\n updatedAt: pg.updatedAt(),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n type: t.enum([\"email\", \"sms\"]),\n\n template: t.text(), // e.g. 'resetPassword'\n\n category: t.optional(\n t.text({\n description:\n \"For grouping related notifications (e.g., 'authentication', 'marketing'). Contact can filter notifications by category.\",\n }),\n ),\n\n critical: t.optional(\n t.boolean({\n description:\n \"Prioritize delivery of this notification. Set to true for important system alerts.\",\n }),\n ),\n\n sensitive: t.optional(\n t.boolean({\n description:\n \"Message won't be logged or stored in plain text. Set to true when notification contains passwords or codes.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n contact: t.text(), // e.g. email address or phone number or user ID or whatever\n\n variables: t.optional(t.record(t.text(), t.any())),\n\n scheduledAt: t.optional(\n t.datetime({\n description:\n \"When set, the notification will be sent at or after this date/time.\",\n }),\n ),\n\n // -----------------------------------------------------------------------------------------------------------------\n\n sentAt: t.optional(t.datetime()),\n\n error: t.optional(\n t.object({\n at: t.datetime(),\n name: t.text(),\n message: t.text({ size: \"rich\" }),\n }),\n ),\n\n // TODO: retryCount, lastRetryAt, etc.\n }),\n});\n\nexport type NotificationEntity = Static<typeof notifications.schema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n type: t.optional(t.enum([\"email\", \"sms\"])),\n template: t.optional(t.string()),\n contact: t.optional(t.string()),\n category: t.optional(t.string()),\n status: t.optional(t.enum([\"pending\", \"sent\", \"failed\"])),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationService = $inject(NotificationService);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n if (this.options.email) {\n await this.notificationService.createNotification({\n ...options,\n type: \"email\",\n template: this.name,\n });\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { SmsProvider } from \"alepha/sms\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { $notification } from \"../primitives/$notification.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(notificationId: string | NotificationEntity) {\n this.log.trace(\"Sending notification\", {\n notificationId:\n typeof notificationId === \"string\" ? notificationId : notificationId.id,\n });\n\n const notification =\n typeof notificationId === \"string\"\n ? await this.notificationRepository.findById(notificationId)\n : notificationId;\n\n if (notification.sentAt) {\n this.log.debug(\"Notification already sent\", {\n notificationId: notification.id,\n sentAt: notification.sentAt,\n });\n return;\n }\n\n this.log.debug(\"Processing notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n });\n\n try {\n if (notification.type === \"email\") {\n await this.emailProvider.send(this.renderEmail(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"Email notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n if (notification.type === \"sms\") {\n await this.smsProvider.send(this.renderSms(notification));\n notification.sentAt = this.dateTimeProvider.nowISOString();\n this.log.info(\"SMS notification sent\", {\n id: notification.id,\n template: notification.template,\n contact: notification.contact,\n });\n }\n } catch (e) {\n this.log.error(\"Failed to send notification\", {\n id: notification.id,\n type: notification.type,\n template: notification.template,\n contact: notification.contact,\n error: e,\n });\n if (e instanceof Error) {\n notification.error = {\n at: this.dateTimeProvider.nowISOString(),\n name: e.name,\n message: e.message,\n };\n }\n } finally {\n await this.notificationRepository.save(notification);\n }\n }\n\n public renderSms(notification: NotificationEntity) {\n this.log.trace(\"Rendering SMS notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const sms = template.options.sms;\n if (!sms) {\n this.log.error(\"Notification template has no SMS defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no sms defined`,\n );\n }\n\n this.log.debug(\"Rendering SMS\", {\n template: notification.template,\n contact,\n });\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return {\n to: contact,\n message,\n };\n }\n\n public renderEmail(notification: NotificationEntity) {\n this.log.trace(\"Rendering email notification\", {\n id: notification.id,\n template: notification.template,\n });\n\n const { variables, contact, template } = this.load(notification);\n\n const email = template.options.email;\n if (!email) {\n this.log.error(\"Notification template has no email defined\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `Notification template ${notification.template} has no email defined`,\n );\n }\n\n this.log.debug(\"Rendering email\", {\n template: notification.template,\n contact,\n subject: email.subject,\n });\n\n const subject = email.subject;\n\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return {\n to: contact,\n subject,\n body,\n };\n }\n\n protected load(notification: NotificationEntity) {\n const variables = notification.variables || {};\n const contact = notification.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === notification.template);\n\n if (!template) {\n this.log.error(\"Notification template not found\", {\n id: notification.id,\n template: notification.template,\n });\n throw new AlephaError(\n `No notification template found for ${notification.template}`,\n );\n }\n\n return {\n template,\n variables,\n contact,\n };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $queue } from \"alepha/queue\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\nexport class NotificationQueues {\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly processNotification = $queue({\n description: \"Queue for processing notifications\",\n schema: t.object({\n notificationId: t.string({ format: \"uuid\" }),\n }),\n handler: async (message) => {\n await this.notificationSenderService.send(message.payload.notificationId);\n },\n });\n}\n","import { type Static, t } from \"alepha\";\nimport { notifications } from \"../entities/notifications.ts\";\n\nexport const notificationCreateSchema = t.pick(notifications.schema, [\n \"type\",\n \"contact\",\n \"template\",\n \"variables\",\n]);\n\nexport type NotificationCreate = Static<typeof notificationCreateSchema>;\n","import { $env, $inject, Alepha, type Static, t } from \"alepha\";\nimport { $batch } from \"alepha/batch\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type NotificationEntity,\n notifications,\n} from \"../entities/notifications.ts\";\nimport { NotificationQueues } from \"../queues/NotificationQueues.ts\";\nimport {\n type NotificationCreate,\n notificationCreateSchema,\n} from \"../schemas/notificationCreateSchema.ts\";\nimport type { NotificationQuery } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationSenderService } from \"./NotificationSenderService.ts\";\n\nexport const notificationServiceEnvSchema = t.object({\n NOTIFICATION_QUEUE: t.optional(\n t.boolean({\n description:\n \"If true, notifications will be queued instead of sent immediately\",\n }),\n ),\n});\n\ndeclare module \"alepha\" {\n interface Env extends Partial<Static<typeof notificationServiceEnvSchema>> {}\n}\n\nexport class NotificationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly env = $env(notificationServiceEnvSchema);\n protected readonly notificationRepository = $repository(notifications);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n\n public readonly notificationBatch = $batch({\n maxSize: 100,\n maxDuration: [15, \"seconds\"],\n schema: notificationCreateSchema,\n handler: async (notifications: NotificationCreate[]) => {\n this.log.debug(\"Processing notification batch\", {\n size: notifications.length,\n templates: [...new Set(notifications.map((n) => n.template))],\n });\n\n const entities =\n await this.notificationRepository.createMany(notifications);\n\n await this.alepha\n .inject(NotificationQueues)\n .processNotification.push(\n ...entities.map((it) => ({ notificationId: it.id })),\n );\n\n this.log.info(\"Notification batch queued\", {\n count: entities.length,\n ids: entities.map((it) => it.id),\n });\n },\n });\n\n public async findNotificationById(id: string) {\n this.log.trace(\"Finding notification by ID\", { id });\n return this.notificationRepository.findOne({ where: { id } });\n }\n\n public async findNotifications(\n q: NotificationQuery = {},\n ): Promise<Page<NotificationEntity>> {\n this.log.trace(\"Finding notifications\", { query: q });\n q.sort ??= \"-createdAt\";\n\n const where = this.notificationRepository.createQueryWhere();\n\n if (q.type) {\n where.type = { eq: q.type };\n }\n\n if (q.template) {\n where.template = { like: `%${q.template}%` };\n }\n\n if (q.contact) {\n where.contact = { like: `%${q.contact}%` };\n }\n\n if (q.category) {\n where.category = { eq: q.category };\n }\n\n if (q.status) {\n if (q.status === \"sent\") {\n where.sentAt = { isNotNull: true };\n where.error = { isNull: true };\n } else if (q.status === \"failed\") {\n where.error = { isNotNull: true };\n } else if (q.status === \"pending\") {\n where.sentAt = { isNull: true };\n where.error = { isNull: true };\n }\n }\n\n return this.notificationRepository.paginate(q, { where }, { count: true });\n }\n\n /**\n * Create a new notification.\n */\n public async createNotification(entry: NotificationCreate): Promise<void> {\n this.log.trace(\"Creating notification\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n if (\n this.env.NOTIFICATION_QUEUE !== true ||\n this.alepha.isServerless() ||\n this.alepha.isTest()\n ) {\n this.log.debug(\"Sending notification immediately\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n const notification = await this.notificationRepository.create(entry);\n await this.notificationSenderService.send(notification);\n return;\n }\n\n this.log.debug(\"Queuing notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n });\n\n this.notificationBatch.push(entry).catch((e) => {\n this.log.error(\"Failed to push notification to batch\", {\n template: entry.template,\n type: entry.type,\n contact: entry.contact,\n error: e,\n });\n });\n }\n}\n","import { $inject } from \"alepha\";\nimport { pg } from \"alepha/orm\";\nimport { $action } from \"alepha/server\";\nimport { notifications } from \"../entities/notifications.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { NotificationService } from \"../services/NotificationService.ts\";\n\nexport class NotificationController {\n protected readonly url = \"/notifications\";\n protected readonly group = \"notifications\";\n protected readonly notificationService = $inject(NotificationService);\n\n /**\n * Find notifications with pagination and filtering.\n */\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n description: \"Find notifications with pagination and filtering\",\n schema: {\n query: notificationQuerySchema,\n response: pg.page(notifications.schema),\n },\n handler: ({ query }) => this.notificationService.findNotifications(query),\n });\n}\n","export class NotificationJobs {\n // - retry (lost, failed) notifications\n // - purge old notifications\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { $module } from \"alepha\";\nimport { NotificationController } from \"./controllers/NotificationController.ts\";\nimport { NotificationJobs } from \"./jobs/NotificationJobs.ts\";\nimport { $notification } from \"./primitives/$notification.ts\";\nimport { NotificationQueues } from \"./queues/NotificationQueues.ts\";\nimport { NotificationSenderService } from \"./services/NotificationSenderService.ts\";\nimport {\n NotificationService,\n notificationServiceEnvSchema,\n} from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/NotificationController.ts\";\nexport * from \"./entities/notifications.ts\";\nexport * from \"./jobs/NotificationJobs.ts\";\nexport * from \"./primitives/$notification.ts\";\nexport * from \"./queues/NotificationQueues.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationCreateSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\nexport * from \"./services/NotificationSenderService.ts\";\nexport * from \"./services/NotificationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Provides notification management API endpoints for Alepha applications.\n *\n * This module includes notification sending, retrieval, status tracking,\n * and user notification preferences management.\n *\n * Requires `AlephaSms` module to be loaded for SMS notifications.\n *\n * @module alepha.api.notifications\n */\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n primitives: [$notification],\n services: [\n NotificationController,\n NotificationService,\n NotificationSenderService,\n NotificationQueues,\n NotificationJobs,\n ],\n register: (alepha) => {\n const env = alepha.parseEnv(notificationServiceEnvSchema);\n if (env.NOTIFICATION_QUEUE) {\n alepha.with(NotificationQueues);\n }\n\n alepha\n .with(NotificationController)\n .with(NotificationService)\n .with(NotificationSenderService)\n .with(NotificationJobs);\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { $module, t } from "alepha";
|
|
2
|
+
import { $entity, pg } from "alepha/orm";
|
|
3
|
+
|
|
4
|
+
//#region src/api-parameters/entities/parameters.ts
|
|
5
|
+
const parameters = $entity({
|
|
6
|
+
name: "parameters",
|
|
7
|
+
schema: t.object({
|
|
8
|
+
id: pg.primaryKey(t.uuid()),
|
|
9
|
+
createdAt: pg.createdAt(),
|
|
10
|
+
updatedAt: pg.updatedAt(),
|
|
11
|
+
name: t.string(),
|
|
12
|
+
content: t.json(),
|
|
13
|
+
tags: t.optional(t.array(t.string())),
|
|
14
|
+
creatorId: t.optional(t.uuid()),
|
|
15
|
+
creatorName: t.optional(t.string()),
|
|
16
|
+
activationDate: t.datetime({ description: "Optional activation date. Default to now. Must be now or later." })
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/api-parameters/index.browser.ts
|
|
22
|
+
const AlephaApiParameters = $module({
|
|
23
|
+
name: "alepha.api.parameters",
|
|
24
|
+
services: []
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { AlephaApiParameters, parameters };
|
|
29
|
+
//# sourceMappingURL=index.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.js","names":[],"sources":["../../src/api-parameters/entities/parameters.ts","../../src/api-parameters/index.browser.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, pg } from \"alepha/orm\";\n\nexport const parameters = $entity({\n name: \"parameters\",\n schema: t.object({\n id: pg.primaryKey(t.uuid()),\n\n createdAt: pg.createdAt(),\n\n updatedAt: pg.updatedAt(),\n\n name: t.string(),\n\n content: t.json(),\n\n tags: t.optional(t.array(t.string())),\n\n creatorId: t.optional(t.uuid()),\n\n creatorName: t.optional(t.string()),\n\n activationDate: t.datetime({\n description:\n \"Optional activation date. Default to now. Must be now or later.\",\n }),\n }),\n});\n\nexport type ParameterEntity = Static<typeof parameters.schema>;\n","import { $module } from \"alepha\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./entities/parameters.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaApiParameters = $module({\n name: \"alepha.api.parameters\",\n services: [],\n});\n"],"mappings":";;;;AAGA,MAAa,aAAa,QAAQ;CAChC,MAAM;CACN,QAAQ,EAAE,OAAO;EACf,IAAI,GAAG,WAAW,EAAE,MAAM,CAAC;EAE3B,WAAW,GAAG,WAAW;EAEzB,WAAW,GAAG,WAAW;EAEzB,MAAM,EAAE,QAAQ;EAEhB,SAAS,EAAE,MAAM;EAEjB,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;EAErC,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC;EAE/B,aAAa,EAAE,SAAS,EAAE,QAAQ,CAAC;EAEnC,gBAAgB,EAAE,SAAS,EACzB,aACE,mEACH,CAAC;EACH,CAAC;CACH,CAAC;;;;ACnBF,MAAa,sBAAsB,QAAQ;CACzC,MAAM;CACN,UAAU,EAAE;CACb,CAAC"}
|
|
@@ -14,6 +14,7 @@ import { CryptoProvider, RealmPrimitive, RealmPrimitiveOptions, UserAccount } fr
|
|
|
14
14
|
import * as alepha_server_links0 from "alepha/server/links";
|
|
15
15
|
import { OAuth2Profile, ServerAuthProvider, WithLinkFn, WithLoginFn } from "alepha/server/auth";
|
|
16
16
|
import { FileSystemProvider } from "alepha/file";
|
|
17
|
+
import { FileController } from "alepha/api/files";
|
|
17
18
|
import * as drizzle_orm0 from "drizzle-orm";
|
|
18
19
|
import { BuildExtraConfigColumns, SQL, SQLWrapper } from "drizzle-orm";
|
|
19
20
|
import * as drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
|
|
@@ -25,7 +26,6 @@ import "alepha/lock";
|
|
|
25
26
|
import "drizzle-orm/postgres-js";
|
|
26
27
|
import "postgres";
|
|
27
28
|
import "drizzle-orm/sqlite-core";
|
|
28
|
-
import { FileController } from "alepha/api/files";
|
|
29
29
|
|
|
30
30
|
//#region src/api-users/atoms/realmAuthSettingsAtom.d.ts
|
|
31
31
|
declare const realmAuthSettingsAtom: alepha23.Atom<alepha23.TObject<{
|
|
@@ -161,7 +161,7 @@ declare class UserRealmProvider {
|
|
|
161
161
|
protected realms: Map<string, UserRealm>;
|
|
162
162
|
avatars: alepha_bucket0.BucketPrimitive;
|
|
163
163
|
protected readonly onConfigure: alepha23.HookPrimitive<"configure">;
|
|
164
|
-
register(userRealmName: string, userRealmOptions?: UserRealmOptions$1):
|
|
164
|
+
register(userRealmName: string, userRealmOptions?: UserRealmOptions$1): UserRealm;
|
|
165
165
|
/**
|
|
166
166
|
* Gets a registered realm by name, auto-creating default if needed.
|
|
167
167
|
*/
|
|
@@ -2220,10 +2220,15 @@ declare class UserService {
|
|
|
2220
2220
|
}>>;
|
|
2221
2221
|
/**
|
|
2222
2222
|
* Request email verification for a user.
|
|
2223
|
+
* @param email - The email address to verify.
|
|
2224
|
+
* @param userRealmName - Optional realm name.
|
|
2225
|
+
* @param method - The verification method: "code" (default) or "link".
|
|
2226
|
+
* @param verifyUrl - Base URL for verification link (required when method is "link").
|
|
2223
2227
|
*/
|
|
2224
|
-
requestEmailVerification(email: string, userRealmName?: string): Promise<boolean>;
|
|
2228
|
+
requestEmailVerification(email: string, userRealmName?: string, method?: "code" | "link", verifyUrl?: string): Promise<boolean>;
|
|
2225
2229
|
/**
|
|
2226
2230
|
* Verify a user's email using a valid verification token.
|
|
2231
|
+
* Supports both code (6-digit) and link (UUID) verification tokens.
|
|
2227
2232
|
*/
|
|
2228
2233
|
verifyEmail(email: string, token: string, userRealmName?: string): Promise<void>;
|
|
2229
2234
|
/**
|
|
@@ -2544,10 +2549,14 @@ declare class UserController {
|
|
|
2544
2549
|
/**
|
|
2545
2550
|
* Request email verification.
|
|
2546
2551
|
* Generates a verification token using verification service and sends an email to the user.
|
|
2552
|
+
* @param method - The verification method: "code" (default) sends a 6-digit code, "link" sends a clickable verification link.
|
|
2553
|
+
* @param verifyUrl - Required when method is "link". The base URL for the verification link. Token and email will be appended as query params.
|
|
2547
2554
|
*/
|
|
2548
2555
|
requestEmailVerification: alepha_server0.ActionPrimitiveFn<{
|
|
2549
2556
|
query: alepha23.TObject<{
|
|
2550
2557
|
userRealmName: alepha23.TOptional<alepha23.TString>;
|
|
2558
|
+
method: alepha23.TOptional<alepha23.TUnsafe<"link" | "code">>;
|
|
2559
|
+
verifyUrl: alepha23.TOptional<alepha23.TString>;
|
|
2551
2560
|
}>;
|
|
2552
2561
|
body: alepha23.TObject<{
|
|
2553
2562
|
email: alepha23.TString;
|
|
@@ -2691,6 +2700,10 @@ interface UserRealmOptions {
|
|
|
2691
2700
|
google?: true;
|
|
2692
2701
|
github?: true;
|
|
2693
2702
|
};
|
|
2703
|
+
modules?: {
|
|
2704
|
+
files?: boolean;
|
|
2705
|
+
audits?: boolean;
|
|
2706
|
+
};
|
|
2694
2707
|
}
|
|
2695
2708
|
//#endregion
|
|
2696
2709
|
//#region src/api-users/schemas/identityResourceSchema.d.ts
|
package/dist/api-users/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { $realm, CryptoProvider, InvalidCredentialsError, SecurityProvider } fro
|
|
|
14
14
|
import { $client } from "alepha/server/links";
|
|
15
15
|
import { $authCredentials, $authGithub, $authGoogle, ServerAuthProvider, authenticationProviderSchema } from "alepha/server/auth";
|
|
16
16
|
import { FileSystemProvider } from "alepha/file";
|
|
17
|
+
import { AlephaApiFiles } from "alepha/api/files";
|
|
17
18
|
|
|
18
19
|
//#region src/api-users/schemas/identityQuerySchema.ts
|
|
19
20
|
const identityQuerySchema = t.extend(pageQuerySchema, {
|
|
@@ -152,6 +153,7 @@ var UserRealmProvider = class {
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
});
|
|
156
|
+
return this.getRealm(userRealmName);
|
|
155
157
|
}
|
|
156
158
|
/**
|
|
157
159
|
* Gets a registered realm by name, auto-creating default if needed.
|
|
@@ -1136,11 +1138,16 @@ var UserService = class {
|
|
|
1136
1138
|
}
|
|
1137
1139
|
/**
|
|
1138
1140
|
* Request email verification for a user.
|
|
1141
|
+
* @param email - The email address to verify.
|
|
1142
|
+
* @param userRealmName - Optional realm name.
|
|
1143
|
+
* @param method - The verification method: "code" (default) or "link".
|
|
1144
|
+
* @param verifyUrl - Base URL for verification link (required when method is "link").
|
|
1139
1145
|
*/
|
|
1140
|
-
async requestEmailVerification(email, userRealmName) {
|
|
1146
|
+
async requestEmailVerification(email, userRealmName, method = "code", verifyUrl) {
|
|
1141
1147
|
this.log.trace("Requesting email verification", {
|
|
1142
1148
|
email,
|
|
1143
|
-
userRealmName
|
|
1149
|
+
userRealmName,
|
|
1150
|
+
method
|
|
1144
1151
|
});
|
|
1145
1152
|
const user = await this.users(userRealmName).findOne({ where: { email: { eq: email } } }).catch(() => void 0);
|
|
1146
1153
|
if (!user) {
|
|
@@ -1156,21 +1163,40 @@ var UserService = class {
|
|
|
1156
1163
|
}
|
|
1157
1164
|
try {
|
|
1158
1165
|
const verification = await this.verificationController.requestVerificationCode({
|
|
1159
|
-
params: { type:
|
|
1166
|
+
params: { type: method },
|
|
1160
1167
|
body: { target: email }
|
|
1161
1168
|
});
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1169
|
+
if (method === "link") {
|
|
1170
|
+
const url = new URL(verifyUrl || "/verify-email", "http://localhost");
|
|
1171
|
+
url.searchParams.set("email", email);
|
|
1172
|
+
url.searchParams.set("token", verification.token);
|
|
1173
|
+
const fullVerifyUrl = verifyUrl ? `${verifyUrl}${url.search}` : url.pathname + url.search;
|
|
1174
|
+
await this.userNotifications.emailVerificationLink.push({
|
|
1175
|
+
contact: email,
|
|
1176
|
+
variables: {
|
|
1177
|
+
email,
|
|
1178
|
+
verifyUrl: fullVerifyUrl,
|
|
1179
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
this.log.debug("Email verification link sent", {
|
|
1165
1183
|
email,
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1184
|
+
userId: user.id
|
|
1185
|
+
});
|
|
1186
|
+
} else {
|
|
1187
|
+
await this.userNotifications.emailVerification.push({
|
|
1188
|
+
contact: email,
|
|
1189
|
+
variables: {
|
|
1190
|
+
email,
|
|
1191
|
+
code: verification.token,
|
|
1192
|
+
expiresInMinutes: Math.floor(verification.codeExpiration / 60)
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
this.log.debug("Email verification code sent", {
|
|
1196
|
+
email,
|
|
1197
|
+
userId: user.id
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1174
1200
|
} catch (error) {
|
|
1175
1201
|
this.log.warn("Failed to send email verification", {
|
|
1176
1202
|
email,
|
|
@@ -1181,20 +1207,25 @@ var UserService = class {
|
|
|
1181
1207
|
}
|
|
1182
1208
|
/**
|
|
1183
1209
|
* Verify a user's email using a valid verification token.
|
|
1210
|
+
* Supports both code (6-digit) and link (UUID) verification tokens.
|
|
1184
1211
|
*/
|
|
1185
1212
|
async verifyEmail(email, token, userRealmName) {
|
|
1186
1213
|
this.log.trace("Verifying email", {
|
|
1187
1214
|
email,
|
|
1188
1215
|
userRealmName
|
|
1189
1216
|
});
|
|
1217
|
+
const type = /^\d{6}$/.test(token) ? "code" : "link";
|
|
1190
1218
|
if ((await this.verificationController.validateVerificationCode({
|
|
1191
|
-
params: { type
|
|
1219
|
+
params: { type },
|
|
1192
1220
|
body: {
|
|
1193
1221
|
target: email,
|
|
1194
1222
|
token
|
|
1195
1223
|
}
|
|
1196
1224
|
}).catch(() => {
|
|
1197
|
-
this.log.warn("Invalid email verification token", {
|
|
1225
|
+
this.log.warn("Invalid email verification token", {
|
|
1226
|
+
email,
|
|
1227
|
+
type
|
|
1228
|
+
});
|
|
1198
1229
|
throw new BadRequestError("Invalid or expired verification token");
|
|
1199
1230
|
})).alreadyVerified) {
|
|
1200
1231
|
this.log.warn("Email verification token already used", { email });
|
|
@@ -1204,7 +1235,8 @@ var UserService = class {
|
|
|
1204
1235
|
await this.users(userRealmName).updateById(user.id, { emailVerified: true });
|
|
1205
1236
|
this.log.info("Email verified", {
|
|
1206
1237
|
email,
|
|
1207
|
-
userId: user.id
|
|
1238
|
+
userId: user.id,
|
|
1239
|
+
type
|
|
1208
1240
|
});
|
|
1209
1241
|
}
|
|
1210
1242
|
/**
|
|
@@ -1544,12 +1576,21 @@ var UserController = class {
|
|
|
1544
1576
|
/**
|
|
1545
1577
|
* Request email verification.
|
|
1546
1578
|
* Generates a verification token using verification service and sends an email to the user.
|
|
1579
|
+
* @param method - The verification method: "code" (default) sends a 6-digit code, "link" sends a clickable verification link.
|
|
1580
|
+
* @param verifyUrl - Required when method is "link". The base URL for the verification link. Token and email will be appended as query params.
|
|
1547
1581
|
*/
|
|
1548
1582
|
requestEmailVerification = $action({
|
|
1549
1583
|
path: "/users/email-verification/request",
|
|
1550
1584
|
group: this.group,
|
|
1551
1585
|
schema: {
|
|
1552
|
-
query: t.object({
|
|
1586
|
+
query: t.object({
|
|
1587
|
+
userRealmName: t.optional(t.string()),
|
|
1588
|
+
method: t.optional(t.enum(["code", "link"], {
|
|
1589
|
+
default: "code",
|
|
1590
|
+
description: "Verification method: \"code\" sends a 6-digit code, \"link\" sends a clickable verification link."
|
|
1591
|
+
})),
|
|
1592
|
+
verifyUrl: t.optional(t.string({ description: "Base URL for verification link. Required when method is \"link\". Token and email will be appended as query params." }))
|
|
1593
|
+
}),
|
|
1553
1594
|
body: t.object({ email: t.email() }),
|
|
1554
1595
|
response: t.object({
|
|
1555
1596
|
success: t.boolean(),
|
|
@@ -1557,10 +1598,11 @@ var UserController = class {
|
|
|
1557
1598
|
})
|
|
1558
1599
|
},
|
|
1559
1600
|
handler: async ({ body, query }) => {
|
|
1560
|
-
|
|
1601
|
+
const method = query.method ?? "code";
|
|
1602
|
+
await this.userService.requestEmailVerification(body.email, query.userRealmName, method, query.verifyUrl);
|
|
1561
1603
|
return {
|
|
1562
1604
|
success: true,
|
|
1563
|
-
message: "If an account exists with this email, a verification code has been sent."
|
|
1605
|
+
message: method === "link" ? "If an account exists with this email, a verification link has been sent." : "If an account exists with this email, a verification code has been sent."
|
|
1564
1606
|
};
|
|
1565
1607
|
}
|
|
1566
1608
|
});
|
|
@@ -1642,7 +1684,7 @@ const realmAuthSettingsAtom = $atom({
|
|
|
1642
1684
|
registrationAllowed: true,
|
|
1643
1685
|
emailEnabled: true,
|
|
1644
1686
|
emailRequired: true,
|
|
1645
|
-
usernameEnabled:
|
|
1687
|
+
usernameEnabled: false,
|
|
1646
1688
|
usernameRequired: false,
|
|
1647
1689
|
phoneEnabled: false,
|
|
1648
1690
|
phoneRequired: false,
|
|
@@ -1909,6 +1951,7 @@ var SessionService = class {
|
|
|
1909
1951
|
realm: realm.name,
|
|
1910
1952
|
username: profile.email.split("@")[0],
|
|
1911
1953
|
email: profile.email,
|
|
1954
|
+
emailVerified: true,
|
|
1912
1955
|
roles: ["user"]
|
|
1913
1956
|
});
|
|
1914
1957
|
if (profile.picture) {
|
|
@@ -1963,7 +2006,9 @@ const $userRealm = (options = {}) => {
|
|
|
1963
2006
|
const securityProvider = alepha.inject(SecurityProvider);
|
|
1964
2007
|
const userRealmProvider = alepha.inject(UserRealmProvider);
|
|
1965
2008
|
const name = options.realm?.name ?? DEFAULT_USER_REALM_NAME;
|
|
1966
|
-
userRealmProvider.register(name, options);
|
|
2009
|
+
const userRealm = userRealmProvider.register(name, options);
|
|
2010
|
+
if (options.modules?.audits) {}
|
|
2011
|
+
if (options.modules?.files) alepha.with(AlephaApiFiles);
|
|
1967
2012
|
const realm = $realm({
|
|
1968
2013
|
...options.realm,
|
|
1969
2014
|
name,
|
|
@@ -2000,12 +2045,14 @@ const $userRealm = (options = {}) => {
|
|
|
2000
2045
|
realm.login = (name$1) => {
|
|
2001
2046
|
return (credentials) => sessionService.login(name$1, credentials.username, credentials.password);
|
|
2002
2047
|
};
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
2048
|
+
const identities$1 = options.identities ?? { credentials: true };
|
|
2049
|
+
if (identities$1) {
|
|
2050
|
+
const auth = {};
|
|
2051
|
+
if (identities$1.credentials) auth.credentials = $authCredentials(realm);
|
|
2052
|
+
else userRealm.settings.registrationAllowed = false;
|
|
2053
|
+
if (identities$1.google) auth.google = $authGoogle(realm);
|
|
2054
|
+
if (identities$1.github) auth.github = $authGithub(realm);
|
|
2055
|
+
alepha.with(() => auth);
|
|
2009
2056
|
}
|
|
2010
2057
|
return realm;
|
|
2011
2058
|
};
|