-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.module.ts
More file actions
133 lines (127 loc) · 5.52 KB
/
Copy pathauth.module.ts
File metadata and controls
133 lines (127 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/**
* @file auth.module.ts
* @description NestJS module that wires `BymaxAuthModule.registerAsync` with all
* five required implementation bindings: user repository, platform user repository,
* Redis client, email provider, and auth hooks.
*
* Design notes:
* - `chooseEmailProviderClass` is evaluated once at module decoration time (before
* the DI container is available), reading `process.env.EMAIL_PROVIDER` directly.
* This is an accepted exception to the "no direct process.env" rule because NestJS
* `@Module()` metadata must be synchronous — see AGENTS.md §Critical Rules.
* - `controllers.mfa` and `controllers.oauth` are synchronous flags on `registerAsync`
* (not inside `useFactory`) because the module is built before `useFactory` resolves.
* - `BYMAX_AUTH_REDIS_CLIENT` must be in `extraProviders` — the library's `registerAsync`
* validates this eagerly and does not check `imports`. The global `RedisModule` provides
* a separate client instance for other feature modules (e.g. NotificationsModule).
*
* @layer auth
* @see docs/guidelines/nest-auth-guidelines.md
*/
import { Module } from '@nestjs/common';
import type { Type } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
BymaxAuthModule,
BYMAX_AUTH_EMAIL_PROVIDER,
BYMAX_AUTH_HOOKS,
BYMAX_AUTH_PLATFORM_USER_REPOSITORY,
BYMAX_AUTH_REDIS_CLIENT,
BYMAX_AUTH_USER_REPOSITORY,
} from '@bymax-one/nest-auth';
import type { BymaxAuthModuleOptions, IEmailProvider } from '@bymax-one/nest-auth';
import { Redis } from 'ioredis';
import { PrismaModule } from '../prisma/prisma.module.js';
import type { Env } from '../config/env.schema.js';
import { buildAuthOptions } from './auth.config.js';
import { AppAuthHooks } from './app-auth.hooks.js';
import { MailpitEmailProvider } from './mailpit-email.provider.js';
import { PrismaUserRepository } from './prisma-user.repository.js';
import { PrismaPlatformUserRepository } from './prisma-platform-user.repository.js';
import { ResendEmailProvider } from './resend-email.provider.js';
/**
* Returns the email provider class based on `EMAIL_PROVIDER` env var.
*
* Evaluated once at module decoration time (synchronous, before DI initialises).
* `process.env` is the only source available at this stage — this is the accepted
* exception documented in AGENTS.md §Critical Rules §7.
*
* @returns `ResendEmailProvider` when `EMAIL_PROVIDER=resend`; otherwise `MailpitEmailProvider`.
*/
function chooseEmailProviderClass(): Type<IEmailProvider> {
return (process.env['EMAIL_PROVIDER'] ?? 'mailpit').toLowerCase() === 'resend'
? ResendEmailProvider
: MailpitEmailProvider;
}
/**
* Returns `true` iff both Google OAuth env vars are set at process startup.
*
* Evaluated synchronously on module decoration so the `controllers.oauth` flag
* is known when NestJS builds the `DynamicModule` — before `useFactory` resolves.
*
* @returns Whether Google OAuth should be enabled.
*/
function isGoogleOAuthConfigured(): boolean {
return (
typeof process.env['OAUTH_GOOGLE_CLIENT_ID'] === 'string' &&
process.env['OAUTH_GOOGLE_CLIENT_ID'].length > 0 &&
typeof process.env['OAUTH_GOOGLE_CLIENT_SECRET'] === 'string' &&
process.env['OAUTH_GOOGLE_CLIENT_SECRET'].length > 0
);
}
const EmailProviderClass = chooseEmailProviderClass();
/**
* Application auth module that registers `BymaxAuthModule` with all four
* app-owned implementation classes bound to their library injection tokens.
*
* Re-exports `BymaxAuthModule` so downstream feature modules can consume
* library guards and decorators without re-importing the library directly.
*
* @public
*/
@Module({
imports: [
BymaxAuthModule.registerAsync({
imports: [ConfigModule, PrismaModule],
// Cast required because AuthModuleAsyncOptions.useFactory is typed as
// (...args: unknown[]) => ... but we need a typed ConfigService parameter.
// This is the standard NestJS async-options pattern: inject ensures the
// correct type is provided at runtime; the cast satisfies TS contravariance.
useFactory: ((...args: unknown[]) => {
const config = args[0] as ConfigService<Env, true>;
return buildAuthOptions(config);
}) satisfies (...args: unknown[]) => BymaxAuthModuleOptions,
inject: [ConfigService],
controllers: {
mfa: true,
oauth: isGoogleOAuthConfigured(),
// FCM #13 — Session management: mounts /api/auth/sessions/* routes.
sessions: true,
// FCM #22 — Platform admin context: mounts /api/auth/platform/* routes.
platform: true,
// FCM #21 — Invitation flow: mounts /api/auth/invitations routes.
invitations: true,
},
extraProviders: [
{
provide: BYMAX_AUTH_REDIS_CLIENT,
inject: [ConfigService],
useFactory: (config: ConfigService<Env, true>): Redis => {
const url = config.getOrThrow<string>('REDIS_URL');
return new Redis(url, {
lazyConnect: true,
maxRetriesPerRequest: null,
retryStrategy: (times: number) => Math.min(times * 200, 2_000),
});
},
},
{ provide: BYMAX_AUTH_USER_REPOSITORY, useClass: PrismaUserRepository },
{ provide: BYMAX_AUTH_PLATFORM_USER_REPOSITORY, useClass: PrismaPlatformUserRepository },
{ provide: BYMAX_AUTH_EMAIL_PROVIDER, useClass: EmailProviderClass },
{ provide: BYMAX_AUTH_HOOKS, useClass: AppAuthHooks },
],
}),
],
exports: [BymaxAuthModule],
})
export class AuthModule {}