create-forgeon 0.3.16 → 0.3.17

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.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/src/cli/add-options.test.mjs +5 -2
  3. package/src/cli/options.test.mjs +1 -0
  4. package/src/cli/prompt-select.test.mjs +1 -0
  5. package/src/core/docs.test.mjs +1 -0
  6. package/src/core/scaffold.test.mjs +1 -0
  7. package/src/core/validate.test.mjs +1 -0
  8. package/src/modules/accounts.mjs +416 -0
  9. package/src/modules/dependencies.test.mjs +71 -29
  10. package/src/modules/executor.mjs +3 -2
  11. package/src/modules/executor.test.mjs +512 -477
  12. package/src/modules/files-access.mjs +9 -7
  13. package/src/modules/files-image.mjs +9 -7
  14. package/src/modules/files-local.mjs +15 -6
  15. package/src/modules/files-quotas.mjs +8 -6
  16. package/src/modules/files-s3.mjs +17 -6
  17. package/src/modules/files.mjs +21 -21
  18. package/src/modules/idempotency.test.mjs +13 -7
  19. package/src/modules/probes.test.mjs +4 -2
  20. package/src/modules/queue.mjs +9 -6
  21. package/src/modules/rate-limit.mjs +14 -10
  22. package/src/modules/rbac.mjs +12 -11
  23. package/src/modules/registry.mjs +22 -35
  24. package/src/modules/scheduler.mjs +9 -6
  25. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  26. package/src/modules/shared/patch-utils.mjs +29 -1
  27. package/src/modules/sync-integrations.mjs +102 -422
  28. package/src/modules/sync-integrations.test.mjs +32 -111
  29. package/src/run-add-module.test.mjs +1 -0
  30. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -325
  31. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  32. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  33. package/templates/module-fragments/accounts/20_scope.md +29 -0
  34. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  35. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  36. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  37. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  38. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  39. package/templates/module-fragments/swagger/20_scope.md +2 -1
  40. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  41. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  42. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  43. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  44. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  45. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  46. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  47. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  48. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  49. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  50. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  51. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  52. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  53. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  54. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  57. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  58. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  59. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  61. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  62. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  69. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  70. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  71. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  75. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  76. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  77. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  78. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  79. package/templates/module-presets/files/packages/files/package.json +1 -2
  80. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  81. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  82. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  83. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  84. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  85. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  86. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  87. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  88. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  89. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  90. package/src/modules/jwt-auth.mjs +0 -271
  91. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  92. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  93. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  94. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  95. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  96. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  97. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  98. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  99. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  100. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  101. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  102. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  103. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  104. /package/templates/module-presets/{jwt-auth/packages/auth-api/src/jwt-auth.guard.ts → accounts/packages/accounts-api/src/access-token.guard.ts} +0 -0
  105. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  106. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  107. /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
@@ -0,0 +1,318 @@
1
+ import crypto from 'node:crypto';
2
+ import {
3
+ BadRequestException,
4
+ ConflictException,
5
+ Inject,
6
+ Injectable,
7
+ UnauthorizedException,
8
+ } from '@nestjs/common';
9
+ import type {
10
+ AuthSessionResponse,
11
+ RegisterRequest,
12
+ UserRecordDto,
13
+ } from '@forgeon/accounts-contracts';
14
+ import { ACCOUNTS_EMAIL_PORT, type AccountsEmailPort } from './accounts-email.port';
15
+ import {
16
+ ACCOUNTS_PERSISTENCE_PORT,
17
+ type AccountsPersistencePort,
18
+ } from './accounts-persistence.port';
19
+ import { AuthJwtService } from './auth-jwt.service';
20
+ import { AuthPasswordService } from './auth-password.service';
21
+ import type { AuthRefreshTokenPayload } from './auth.types';
22
+ import { UsersService } from './users.service';
23
+ import { toUserRecordDto } from './users.types';
24
+
25
+ const AUTH_ERROR_CODES = {
26
+ invalidCredentials: 'AUTH_INVALID_CREDENTIALS',
27
+ refreshInvalid: 'AUTH_REFRESH_INVALID',
28
+ tokenExpired: 'AUTH_TOKEN_EXPIRED',
29
+ emailTaken: 'AUTH_EMAIL_TAKEN',
30
+ accountDisabled: 'AUTH_ACCOUNT_DISABLED',
31
+ } as const;
32
+
33
+ @Injectable()
34
+ export class AuthCoreService {
35
+ constructor(
36
+ @Inject(ACCOUNTS_PERSISTENCE_PORT)
37
+ private readonly persistence: AccountsPersistencePort,
38
+ @Inject(ACCOUNTS_EMAIL_PORT)
39
+ private readonly emailPort: AccountsEmailPort,
40
+ private readonly authJwtService: AuthJwtService,
41
+ private readonly authPasswordService: AuthPasswordService,
42
+ private readonly usersService: UsersService,
43
+ ) {}
44
+
45
+ async registerWithPassword(input: RegisterRequest): Promise<AuthSessionResponse> {
46
+ const email = input.email.trim().toLowerCase();
47
+ const existing = await this.persistence.findPasswordAccountByEmail(email);
48
+ if (existing) {
49
+ throw new ConflictException({
50
+ message: 'Email is already registered',
51
+ details: { code: AUTH_ERROR_CODES.emailTaken },
52
+ });
53
+ }
54
+
55
+ const passwordHash = await this.authPasswordService.hash(input.password);
56
+ const account = await this.persistence.createPasswordAccount({
57
+ email,
58
+ passwordHash,
59
+ status: 'active',
60
+ userData: this.usersService.resolveUserData(input.user),
61
+ profile: {
62
+ name: this.readNullableString(input.profile, 'name'),
63
+ avatar: this.readNullableString(input.profile, 'avatar'),
64
+ data: this.usersService.resolveProfileData(this.readNestedObject(input.profile, 'data')),
65
+ },
66
+ settings: {
67
+ theme: this.readNullableString(input.settings, 'theme'),
68
+ locale: this.readNullableString(input.settings, 'locale'),
69
+ data: this.usersService.resolveSettingsData(this.readNestedObject(input.settings, 'data')),
70
+ },
71
+ });
72
+
73
+ const verificationToken = this.createStubToken('verify', account.id);
74
+ await Promise.all([
75
+ this.emailPort.sendVerificationEmail({
76
+ email,
77
+ token: verificationToken,
78
+ userId: account.id,
79
+ }),
80
+ this.emailPort.sendWelcomeEmail({
81
+ email,
82
+ userId: account.id,
83
+ }),
84
+ ]);
85
+
86
+ return this.issueSession(toUserRecordDto(account));
87
+ }
88
+
89
+ async loginWithPassword(emailInput: string, password: string): Promise<AuthSessionResponse> {
90
+ const email = emailInput.trim().toLowerCase();
91
+ const account = await this.persistence.findPasswordAccountByEmail(email);
92
+ if (!account?.passwordHash) {
93
+ throw this.invalidCredentialsError();
94
+ }
95
+
96
+ this.assertAccountActive(account);
97
+
98
+ const matched = await this.authPasswordService.verify(password, account.passwordHash);
99
+ if (!matched) {
100
+ throw this.invalidCredentialsError();
101
+ }
102
+
103
+ return this.issueSession(toUserRecordDto(account));
104
+ }
105
+
106
+ async refreshTokens(refreshToken: string): Promise<AuthSessionResponse> {
107
+ let payload: AuthRefreshTokenPayload;
108
+ try {
109
+ payload = await this.authJwtService.verifyRefreshToken(refreshToken);
110
+ } catch (error) {
111
+ const code =
112
+ error instanceof Error && error.name === 'TokenExpiredError'
113
+ ? AUTH_ERROR_CODES.tokenExpired
114
+ : AUTH_ERROR_CODES.refreshInvalid;
115
+
116
+ throw new UnauthorizedException({
117
+ message: 'Refresh token is invalid or expired',
118
+ details: { code },
119
+ });
120
+ }
121
+
122
+ const record = await this.persistence.findRefreshTokenById(payload.jti);
123
+ if (!record || record.revokedAt || record.userId !== payload.sub || record.expiresAt <= new Date()) {
124
+ throw new UnauthorizedException({
125
+ message: 'Refresh token is invalid or expired',
126
+ details: { code: AUTH_ERROR_CODES.refreshInvalid },
127
+ });
128
+ }
129
+
130
+ const matched = await this.authPasswordService.verify(refreshToken, record.tokenHash);
131
+ if (!matched) {
132
+ await this.persistence.revokeRefreshToken(record.id, new Date());
133
+ throw new UnauthorizedException({
134
+ message: 'Refresh token is invalid or expired',
135
+ details: { code: AUTH_ERROR_CODES.refreshInvalid },
136
+ });
137
+ }
138
+
139
+ const account = await this.persistence.findAccountByUserId(payload.sub);
140
+ if (!account) {
141
+ throw new UnauthorizedException({
142
+ message: 'Refresh token is invalid or expired',
143
+ details: { code: AUTH_ERROR_CODES.refreshInvalid },
144
+ });
145
+ }
146
+
147
+ this.assertAccountActive(account);
148
+ await this.persistence.revokeRefreshToken(record.id, new Date());
149
+ return this.issueSession(toUserRecordDto(account));
150
+ }
151
+
152
+ async logout(userId: string, refreshToken: string): Promise<void> {
153
+ const payload = await this.authJwtService.verifyRefreshToken(refreshToken);
154
+ if (payload.sub !== userId) {
155
+ throw new UnauthorizedException({
156
+ message: 'Refresh token does not belong to the current user',
157
+ details: { code: AUTH_ERROR_CODES.refreshInvalid },
158
+ });
159
+ }
160
+
161
+ await this.persistence.revokeRefreshToken(payload.jti, new Date());
162
+ }
163
+
164
+ async changePassword(userId: string, newPassword: string): Promise<void> {
165
+ const passwordHash = await this.authPasswordService.hash(newPassword);
166
+ await this.persistence.updatePassword(userId, passwordHash);
167
+ await this.persistence.revokeRefreshTokensForUser(userId, new Date());
168
+ }
169
+
170
+ async requestPasswordReset(emailInput: string) {
171
+ const email = emailInput.trim().toLowerCase();
172
+ const account = await this.persistence.findPasswordAccountByEmail(email);
173
+ if (account) {
174
+ await this.emailPort.sendPasswordResetEmail({
175
+ email,
176
+ token: this.createStubToken('reset', account.id),
177
+ userId: account.id,
178
+ });
179
+ }
180
+
181
+ return {
182
+ status: 'accepted',
183
+ delivery: 'stub',
184
+ };
185
+ }
186
+
187
+ async resetPassword(token: string, newPassword: string) {
188
+ if (token.trim().length < 8) {
189
+ throw new BadRequestException('Reset token is invalid');
190
+ }
191
+
192
+ return {
193
+ status: 'accepted',
194
+ delivery: 'stub',
195
+ nextAction: 'emails-module',
196
+ passwordLength: newPassword.length,
197
+ };
198
+ }
199
+
200
+ async verifyEmail(token: string) {
201
+ if (token.trim().length < 8) {
202
+ throw new BadRequestException('Verification token is invalid');
203
+ }
204
+
205
+ return {
206
+ status: 'accepted',
207
+ delivery: 'stub',
208
+ nextAction: 'emails-module',
209
+ };
210
+ }
211
+
212
+ async me(userId: string): Promise<{ user: UserRecordDto }> {
213
+ const user = await this.usersService.getByIdOrThrow(userId);
214
+ return { user };
215
+ }
216
+
217
+ getProbeStatus() {
218
+ return {
219
+ status: 'ok',
220
+ feature: 'accounts',
221
+ storage: 'db-adapter',
222
+ emailDelivery: 'stub',
223
+ selfServiceRoutes: [
224
+ '/api/users/:id',
225
+ '/api/users/:id/profile',
226
+ '/api/users/:id/settings',
227
+ ],
228
+ };
229
+ }
230
+
231
+ private async issueSession(user: UserRecordDto): Promise<AuthSessionResponse> {
232
+ const refreshId = crypto.randomUUID();
233
+ const [accessToken, refreshToken] = await Promise.all([
234
+ this.authJwtService.signAccessToken({
235
+ sub: user.id,
236
+ email: user.email ?? undefined,
237
+ type: 'access',
238
+ }),
239
+ this.authJwtService.signRefreshToken({
240
+ sub: user.id,
241
+ email: user.email ?? undefined,
242
+ jti: refreshId,
243
+ type: 'refresh',
244
+ }),
245
+ ]);
246
+
247
+ const tokenHash = await this.authPasswordService.hash(refreshToken);
248
+ await this.persistence.createRefreshToken({
249
+ id: refreshId,
250
+ userId: user.id,
251
+ tokenHash,
252
+ expiresAt: this.toExpiresAt(this.authJwtService.refreshTtl),
253
+ });
254
+
255
+ return {
256
+ accessToken,
257
+ refreshToken,
258
+ tokenType: 'Bearer',
259
+ accessTtl: this.authJwtService.accessTtl,
260
+ refreshTtl: this.authJwtService.refreshTtl,
261
+ user,
262
+ };
263
+ }
264
+
265
+ private assertAccountActive(user: { status: string; deletedAt: Date | null }) {
266
+ if (user.deletedAt || user.status !== 'active') {
267
+ throw new UnauthorizedException({
268
+ message: 'Account is not active',
269
+ details: { code: AUTH_ERROR_CODES.accountDisabled },
270
+ });
271
+ }
272
+ }
273
+
274
+ private invalidCredentialsError() {
275
+ return new UnauthorizedException({
276
+ message: 'Invalid credentials',
277
+ details: { code: AUTH_ERROR_CODES.invalidCredentials },
278
+ });
279
+ }
280
+
281
+ private createStubToken(kind: string, userId: string): string {
282
+ return `stub-${kind}-${userId}-${Date.now()}`;
283
+ }
284
+
285
+ private readNullableString(input: unknown, key: string): string | null {
286
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
287
+ return null;
288
+ }
289
+ const value = (input as Record<string, unknown>)[key];
290
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
291
+ }
292
+
293
+ private readNestedObject(input: unknown, key: string): Record<string, unknown> | null {
294
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
295
+ return null;
296
+ }
297
+ const value = (input as Record<string, unknown>)[key];
298
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
299
+ return null;
300
+ }
301
+ return value as Record<string, unknown>;
302
+ }
303
+
304
+ private toExpiresAt(ttl: string): Date {
305
+ const now = Date.now();
306
+ const match = ttl.trim().match(/^(\d+)([smhd])$/i);
307
+ if (!match) {
308
+ const seconds = Number.parseInt(ttl, 10);
309
+ return new Date(now + (Number.isFinite(seconds) ? seconds : 7 * 24 * 60 * 60) * 1000);
310
+ }
311
+
312
+ const amount = Number.parseInt(match[1], 10);
313
+ const unit = match[2].toLowerCase();
314
+ const multiplier =
315
+ unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000;
316
+ return new Date(now + amount * multiplier);
317
+ }
318
+ }
@@ -6,9 +6,9 @@ export const authEnvSchema = z
6
6
  JWT_ACCESS_EXPIRES_IN: z.string().trim().min(2).default('15m'),
7
7
  JWT_REFRESH_SECRET: z.string().trim().min(16).default('forgeon-refresh-secret-change-me'),
8
8
  JWT_REFRESH_EXPIRES_IN: z.string().trim().min(2).default('7d'),
9
- AUTH_BCRYPT_ROUNDS: z.coerce.number().int().min(4).max(15).default(10),
10
- AUTH_DEMO_EMAIL: z.string().trim().email().default('demo@forgeon.local'),
11
- AUTH_DEMO_PASSWORD: z.string().min(8).default('forgeon-demo-password'),
9
+ AUTH_ARGON2_MEMORY_COST: z.coerce.number().int().min(1024).default(19456),
10
+ AUTH_ARGON2_TIME_COST: z.coerce.number().int().min(1).default(2),
11
+ AUTH_ARGON2_PARALLELISM: z.coerce.number().int().min(1).default(1)
12
12
  })
13
13
  .passthrough();
14
14
 
@@ -16,4 +16,4 @@ export type AuthEnv = z.infer<typeof authEnvSchema>;
16
16
 
17
17
  export function parseAuthEnv(input: Record<string, unknown>): AuthEnv {
18
18
  return authEnvSchema.parse(input);
19
- }
19
+ }
@@ -0,0 +1,58 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { JwtService } from '@nestjs/jwt';
3
+ import type { JwtSignOptions } from '@nestjs/jwt';
4
+ import type { AuthAccessClaims, AuthRefreshClaims } from '@forgeon/accounts-contracts';
5
+ import { AuthConfigService } from './auth-config.service';
6
+ import type { AuthAccessTokenPayload, AuthRefreshTokenPayload } from './auth.types';
7
+
8
+ type JwtExpiresIn = NonNullable<JwtSignOptions['expiresIn']>;
9
+
10
+ @Injectable()
11
+ export class AuthJwtService {
12
+ constructor(
13
+ private readonly jwtService: JwtService,
14
+ private readonly authConfigService: AuthConfigService,
15
+ ) {}
16
+
17
+ signAccessToken(payload: AuthAccessClaims): Promise<string> {
18
+ return this.jwtService.signAsync(payload, {
19
+ secret: this.authConfigService.accessSecret,
20
+ expiresIn: this.toJwtExpiresIn(this.authConfigService.accessExpiresIn),
21
+ });
22
+ }
23
+
24
+ signRefreshToken(payload: AuthRefreshClaims): Promise<string> {
25
+ return this.jwtService.signAsync(payload, {
26
+ secret: this.authConfigService.refreshSecret,
27
+ expiresIn: this.toJwtExpiresIn(this.authConfigService.refreshExpiresIn),
28
+ });
29
+ }
30
+
31
+ verifyAccessToken(token: string): Promise<AuthAccessTokenPayload> {
32
+ return this.jwtService.verifyAsync<AuthAccessTokenPayload>(token, {
33
+ secret: this.authConfigService.accessSecret,
34
+ });
35
+ }
36
+
37
+ verifyRefreshToken(token: string): Promise<AuthRefreshTokenPayload> {
38
+ return this.jwtService.verifyAsync<AuthRefreshTokenPayload>(token, {
39
+ secret: this.authConfigService.refreshSecret,
40
+ });
41
+ }
42
+
43
+ get accessTtl(): string {
44
+ return this.authConfigService.accessExpiresIn;
45
+ }
46
+
47
+ get refreshTtl(): string {
48
+ return this.authConfigService.refreshExpiresIn;
49
+ }
50
+
51
+ private toJwtExpiresIn(value: string): JwtExpiresIn {
52
+ const trimmed = value.trim();
53
+ if (/^\d+$/.test(trimmed)) {
54
+ return Number(trimmed);
55
+ }
56
+ return trimmed as JwtExpiresIn;
57
+ }
58
+ }
@@ -0,0 +1,21 @@
1
+ import argon2 from 'argon2';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { AuthConfigService } from './auth-config.service';
4
+
5
+ @Injectable()
6
+ export class AuthPasswordService {
7
+ constructor(private readonly authConfigService: AuthConfigService) {}
8
+
9
+ hash(password: string): Promise<string> {
10
+ return argon2.hash(password, {
11
+ type: argon2.argon2id,
12
+ memoryCost: this.authConfigService.argon2MemoryCost,
13
+ timeCost: this.authConfigService.argon2TimeCost,
14
+ parallelism: this.authConfigService.argon2Parallelism,
15
+ });
16
+ }
17
+
18
+ verify(password: string, hash: string): Promise<boolean> {
19
+ return argon2.verify(hash, password);
20
+ }
21
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Get,
5
+ Post,
6
+ Req,
7
+ UseGuards,
8
+ } from '@nestjs/common';
9
+ import { AuthService } from './auth.service';
10
+ import {
11
+ ChangePasswordDto,
12
+ ConfirmPasswordResetDto,
13
+ LoginDto,
14
+ RefreshDto,
15
+ RegisterDto,
16
+ RequestPasswordResetDto,
17
+ VerifyEmailDto,
18
+ } from './dto';
19
+ import { JwtAuthGuard } from './access-token.guard';
20
+ import type { AuthAccessTokenPayload } from './auth.types';
21
+
22
+ type RequestWithUser = { user?: AuthAccessTokenPayload };
23
+
24
+ @Controller('auth')
25
+ export class AuthController {
26
+ constructor(private readonly authService: AuthService) {}
27
+
28
+ @Post('register')
29
+ register(@Body() body: RegisterDto) {
30
+ return this.authService.register(body);
31
+ }
32
+
33
+ @Post('login')
34
+ login(@Body() body: LoginDto) {
35
+ return this.authService.login(body);
36
+ }
37
+
38
+ @Post('refresh')
39
+ refresh(@Body() body: RefreshDto) {
40
+ return this.authService.refresh(body);
41
+ }
42
+
43
+ @UseGuards(JwtAuthGuard)
44
+ @Post('logout')
45
+ async logout(@Body() body: RefreshDto, @Req() request: RequestWithUser) {
46
+ const user = this.getRequestUser(request);
47
+ await this.authService.logout(user.sub, body.refreshToken);
48
+ return { status: 'ok' };
49
+ }
50
+
51
+ @UseGuards(JwtAuthGuard)
52
+ @Get('me')
53
+ me(@Req() request: RequestWithUser) {
54
+ const user = this.getRequestUser(request);
55
+ return this.authService.me(user.sub);
56
+ }
57
+
58
+ @UseGuards(JwtAuthGuard)
59
+ @Post('change-password')
60
+ async changePassword(@Body() body: ChangePasswordDto, @Req() request: RequestWithUser) {
61
+ const user = this.getRequestUser(request);
62
+ await this.authService.changePassword(user.sub, body.newPassword);
63
+ return { status: 'ok' };
64
+ }
65
+
66
+ @Post('verify-email')
67
+ verifyEmail(@Body() body: VerifyEmailDto) {
68
+ return this.authService.verifyEmail(body.token);
69
+ }
70
+
71
+ @Post('password-reset/request')
72
+ requestPasswordReset(@Body() body: RequestPasswordResetDto) {
73
+ return this.authService.requestPasswordReset(body.email);
74
+ }
75
+
76
+ @Post('password-reset/confirm')
77
+ confirmPasswordReset(@Body() body: ConfirmPasswordResetDto) {
78
+ return this.authService.resetPassword(body.token, body.newPassword);
79
+ }
80
+
81
+ private getRequestUser(request: RequestWithUser): AuthAccessTokenPayload {
82
+ const user = request.user;
83
+ if (!user?.sub) {
84
+ return {
85
+ sub: 'unknown',
86
+ email: 'unknown@invalid.local',
87
+ type: 'access',
88
+ };
89
+ }
90
+ return user;
91
+ }
92
+ }
93
+
@@ -0,0 +1,48 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import type { RegisterRequest } from '@forgeon/accounts-contracts';
3
+ import { AuthCoreService } from './auth-core.service';
4
+
5
+ @Injectable()
6
+ export class AuthService {
7
+ constructor(private readonly authCoreService: AuthCoreService) {}
8
+
9
+ register(input: RegisterRequest) {
10
+ return this.authCoreService.registerWithPassword(input);
11
+ }
12
+
13
+ login(input: { email: string; password: string }) {
14
+ return this.authCoreService.loginWithPassword(input.email, input.password);
15
+ }
16
+
17
+ refresh(input: { refreshToken: string }) {
18
+ return this.authCoreService.refreshTokens(input.refreshToken);
19
+ }
20
+
21
+ logout(userId: string, refreshToken: string) {
22
+ return this.authCoreService.logout(userId, refreshToken);
23
+ }
24
+
25
+ changePassword(userId: string, newPassword: string) {
26
+ return this.authCoreService.changePassword(userId, newPassword);
27
+ }
28
+
29
+ requestPasswordReset(email: string) {
30
+ return this.authCoreService.requestPasswordReset(email);
31
+ }
32
+
33
+ resetPassword(token: string, newPassword: string) {
34
+ return this.authCoreService.resetPassword(token, newPassword);
35
+ }
36
+
37
+ verifyEmail(token: string) {
38
+ return this.authCoreService.verifyEmail(token);
39
+ }
40
+
41
+ me(userId: string) {
42
+ return this.authCoreService.me(userId);
43
+ }
44
+
45
+ getProbeStatus() {
46
+ return this.authCoreService.getProbeStatus();
47
+ }
48
+ }
@@ -0,0 +1,17 @@
1
+ import type { AuthAccessClaims, AuthRefreshClaims, IdentityProvider } from '@forgeon/accounts-contracts';
2
+
3
+ export interface AuthAccessTokenPayload extends AuthAccessClaims {
4
+ iat?: number;
5
+ exp?: number;
6
+ }
7
+
8
+ export interface AuthRefreshTokenPayload extends AuthRefreshClaims {
9
+ iat?: number;
10
+ exp?: number;
11
+ }
12
+
13
+ export interface AuthProfile {
14
+ provider: IdentityProvider;
15
+ providerId: string;
16
+ email?: string;
17
+ }
@@ -0,0 +1,13 @@
1
+ import type { ChangePasswordRequest } from '@forgeon/accounts-contracts';
2
+ import { IsOptional, IsString, MinLength } from 'class-validator';
3
+
4
+ export class ChangePasswordDto implements ChangePasswordRequest {
5
+ @IsOptional()
6
+ @IsString()
7
+ @MinLength(8)
8
+ currentPassword?: string;
9
+
10
+ @IsString()
11
+ @MinLength(8)
12
+ newPassword!: string;
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { ConfirmPasswordResetRequest } from '@forgeon/accounts-contracts';
2
+ import { IsString, MinLength } from 'class-validator';
3
+
4
+ export class ConfirmPasswordResetDto implements ConfirmPasswordResetRequest {
5
+ @IsString()
6
+ @MinLength(8)
7
+ token!: string;
8
+
9
+ @IsString()
10
+ @MinLength(8)
11
+ newPassword!: string;
12
+ }
@@ -0,0 +1,10 @@
1
+ export * from './change-password.dto';
2
+ export * from './confirm-password-reset.dto';
3
+ export * from './login.dto';
4
+ export * from './refresh.dto';
5
+ export * from './register.dto';
6
+ export * from './request-password-reset.dto';
7
+ export * from './update-user-profile.dto';
8
+ export * from './update-user-settings.dto';
9
+ export * from './update-user.dto';
10
+ export * from './verify-email.dto';
@@ -1,4 +1,4 @@
1
- import type { LoginRequest } from '@forgeon/auth-contracts';
1
+ import type { LoginRequest } from '@forgeon/accounts-contracts';
2
2
  import { IsEmail, IsString, MinLength } from 'class-validator';
3
3
 
4
4
  export class LoginDto implements LoginRequest {
@@ -1,4 +1,4 @@
1
- import type { RefreshRequest } from '@forgeon/auth-contracts';
1
+ import type { RefreshRequest } from '@forgeon/accounts-contracts';
2
2
  import { IsString, MinLength } from 'class-validator';
3
3
 
4
4
  export class RefreshDto implements RefreshRequest {
@@ -0,0 +1,23 @@
1
+ import type { RegisterRequest } from '@forgeon/accounts-contracts';
2
+ import { IsEmail, IsObject, IsOptional, IsString, MinLength } from 'class-validator';
3
+
4
+ export class RegisterDto implements RegisterRequest {
5
+ @IsEmail()
6
+ email!: string;
7
+
8
+ @IsString()
9
+ @MinLength(8)
10
+ password!: string;
11
+
12
+ @IsOptional()
13
+ @IsObject()
14
+ user?: Record<string, unknown>;
15
+
16
+ @IsOptional()
17
+ @IsObject()
18
+ profile?: Record<string, unknown>;
19
+
20
+ @IsOptional()
21
+ @IsObject()
22
+ settings?: Record<string, unknown>;
23
+ }
@@ -0,0 +1,7 @@
1
+ import type { RequestPasswordResetRequest } from '@forgeon/accounts-contracts';
2
+ import { IsEmail } from 'class-validator';
3
+
4
+ export class RequestPasswordResetDto implements RequestPasswordResetRequest {
5
+ @IsEmail()
6
+ email!: string;
7
+ }
@@ -0,0 +1,16 @@
1
+ import type { UpdateUserProfileRequest } from '@forgeon/accounts-contracts';
2
+ import { IsObject, IsOptional, IsString } from 'class-validator';
3
+
4
+ export class UpdateUserProfileDto implements UpdateUserProfileRequest {
5
+ @IsOptional()
6
+ @IsString()
7
+ name?: string | null;
8
+
9
+ @IsOptional()
10
+ @IsString()
11
+ avatar?: string | null;
12
+
13
+ @IsOptional()
14
+ @IsObject()
15
+ data?: Record<string, unknown>;
16
+ }