create-forgeon 0.3.25 → 0.3.26
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/package.json +1 -1
- package/src/modules/accounts-communications.mjs +146 -0
- package/src/modules/accounts.mjs +17 -5
- package/src/modules/dependencies.test.mjs +31 -5
- package/src/modules/executor.mjs +2 -0
- package/src/modules/executor.test.mjs +18 -17
- package/src/modules/registry.mjs +18 -2
- package/templates/module-fragments/accounts/20_scope.md +4 -5
- package/templates/module-fragments/accounts/90_status_implemented.md +2 -2
- package/templates/module-fragments/accounts-communications/00_title.md +1 -0
- package/templates/module-fragments/accounts-communications/10_overview.md +3 -0
- package/templates/module-fragments/accounts-communications/20_scope.md +24 -0
- package/templates/module-fragments/accounts-communications/90_status_implemented.md +3 -0
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +22 -1
- package/templates/module-presets/accounts/packages/accounts-api/package.json +0 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +122 -117
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-pending-operations.ts +9 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +2 -21
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.handlers.ts +45 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +19 -18
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.store.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +30 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +2 -0
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +37 -1
- package/templates/module-presets/accounts-communications/packages/accounts-communications/package.json +22 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/auth-communications.controller.ts +69 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/auth-communications.service.ts +221 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/confirmed-change-password.handler.ts +16 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/confirm-change-email.dto.ts +8 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/confirm-change-password.dto.ts +8 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/index.ts +6 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/request-change-email.dto.ts +7 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/index.ts +5 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/pending-verification-register.handler.ts +13 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/tsconfig.json +9 -0
package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
2
|
DynamicModule,
|
|
3
3
|
Module,
|
|
4
4
|
ModuleMetadata,
|
|
5
5
|
Provider,
|
|
6
|
+
Type,
|
|
6
7
|
} from '@nestjs/common';
|
|
7
8
|
import { JwtModule } from '@nestjs/jwt';
|
|
8
9
|
import { PassportModule } from '@nestjs/passport';
|
|
@@ -11,14 +12,22 @@ import {
|
|
|
11
12
|
ACCOUNTS_AUTHZ_CLAIMS_RESOLVER,
|
|
12
13
|
NoopAccountsAuthzClaimsResolver,
|
|
13
14
|
} from './accounts-rbac.port';
|
|
15
|
+
import { JwtAuthGuard } from './access-token.guard';
|
|
14
16
|
import { AuthConfigModule } from './auth-config.module';
|
|
15
17
|
import { AuthController } from './auth.controller';
|
|
16
18
|
import { AuthCoreService } from './auth-core.service';
|
|
19
|
+
import {
|
|
20
|
+
CHANGE_PASSWORD_HANDLER,
|
|
21
|
+
DefaultChangePasswordHandler,
|
|
22
|
+
DefaultRegisterHandler,
|
|
23
|
+
REGISTER_HANDLER,
|
|
24
|
+
type ChangePasswordHandler,
|
|
25
|
+
type RegisterHandler,
|
|
26
|
+
} from './auth.handlers';
|
|
17
27
|
import { AuthJwtService } from './auth-jwt.service';
|
|
18
28
|
import { AuthPasswordService } from './auth-password.service';
|
|
19
29
|
import { AuthService } from './auth.service';
|
|
20
30
|
import { AuthStore } from './auth.store';
|
|
21
|
-
import { JwtAuthGuard } from './access-token.guard';
|
|
22
31
|
import { JwtStrategy } from './jwt.strategy';
|
|
23
32
|
import { OwnerAccessGuard } from './owner-access.guard';
|
|
24
33
|
import { UsersController } from './users.controller';
|
|
@@ -29,7 +38,12 @@ import { UsersStore } from './users.store';
|
|
|
29
38
|
export interface ForgeonAccountsModuleOptions {
|
|
30
39
|
imports?: ModuleMetadata['imports'];
|
|
31
40
|
providers?: Provider[];
|
|
41
|
+
controllers?: Type<unknown>[];
|
|
32
42
|
users?: UsersModuleOptions;
|
|
43
|
+
handlers?: {
|
|
44
|
+
register?: Type<RegisterHandler>;
|
|
45
|
+
changePassword?: Type<ChangePasswordHandler>;
|
|
46
|
+
};
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
@Module({})
|
|
@@ -44,7 +58,7 @@ export class ForgeonAccountsModule {
|
|
|
44
58
|
JwtModule.register({}),
|
|
45
59
|
...(options.imports ?? []),
|
|
46
60
|
],
|
|
47
|
-
controllers: [AuthController, UsersController],
|
|
61
|
+
controllers: [AuthController, UsersController, ...(options.controllers ?? [])],
|
|
48
62
|
providers: [
|
|
49
63
|
{
|
|
50
64
|
provide: USERS_MODULE_OPTIONS,
|
|
@@ -57,6 +71,16 @@ export class ForgeonAccountsModule {
|
|
|
57
71
|
AuthStore,
|
|
58
72
|
UsersStore,
|
|
59
73
|
AuthCoreService,
|
|
74
|
+
DefaultRegisterHandler,
|
|
75
|
+
DefaultChangePasswordHandler,
|
|
76
|
+
{
|
|
77
|
+
provide: REGISTER_HANDLER,
|
|
78
|
+
useExisting: options.handlers?.register ?? DefaultRegisterHandler,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
provide: CHANGE_PASSWORD_HANDLER,
|
|
82
|
+
useExisting: options.handlers?.changePassword ?? DefaultChangePasswordHandler,
|
|
83
|
+
},
|
|
60
84
|
AuthJwtService,
|
|
61
85
|
AuthPasswordService,
|
|
62
86
|
AuthService,
|
|
@@ -69,6 +93,8 @@ export class ForgeonAccountsModule {
|
|
|
69
93
|
exports: [
|
|
70
94
|
AuthConfigModule,
|
|
71
95
|
AuthCoreService,
|
|
96
|
+
REGISTER_HANDLER,
|
|
97
|
+
CHANGE_PASSWORD_HANDLER,
|
|
72
98
|
AuthJwtService,
|
|
73
99
|
AuthPasswordService,
|
|
74
100
|
AuthService,
|
|
@@ -79,4 +105,4 @@ export class ForgeonAccountsModule {
|
|
|
79
105
|
],
|
|
80
106
|
};
|
|
81
107
|
}
|
|
82
|
-
}
|
|
108
|
+
}
|
|
@@ -5,8 +5,10 @@ export * from './auth-config.service';
|
|
|
5
5
|
export * from './auth.controller';
|
|
6
6
|
export * from './auth-core.service';
|
|
7
7
|
export * from './auth-env.schema';
|
|
8
|
+
export * from './auth.handlers';
|
|
8
9
|
export * from './auth-jwt.service';
|
|
9
10
|
export * from './auth-password.service';
|
|
11
|
+
export * from './auth-pending-operations';
|
|
10
12
|
export * from './auth.service';
|
|
11
13
|
export * from './auth.store';
|
|
12
14
|
export * from './auth.types';
|
|
@@ -5,9 +5,12 @@ export const AUTH_API_ROUTES = {
|
|
|
5
5
|
logout: '/api/auth/logout',
|
|
6
6
|
me: '/api/auth/me',
|
|
7
7
|
changePassword: '/api/auth/change-password',
|
|
8
|
+
changePasswordConfirm: '/api/auth/change-password/confirm',
|
|
8
9
|
verifyEmail: '/api/auth/verify-email',
|
|
9
10
|
passwordResetRequest: '/api/auth/password-reset/request',
|
|
10
11
|
passwordResetConfirm: '/api/auth/password-reset/confirm',
|
|
12
|
+
changeEmailRequest: '/api/auth/change-email/request',
|
|
13
|
+
changeEmailConfirm: '/api/auth/change-email/confirm',
|
|
11
14
|
} as const;
|
|
12
15
|
|
|
13
16
|
export const USERS_API_ROUTES = {
|
|
@@ -99,6 +102,18 @@ export interface VerifyEmailRequest {
|
|
|
99
102
|
token: string;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
export interface ConfirmChangePasswordRequest {
|
|
106
|
+
token: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface RequestChangeEmailRequest {
|
|
110
|
+
email: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ConfirmChangeEmailRequest {
|
|
114
|
+
token: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
102
117
|
export interface UpdateUserRequest {
|
|
103
118
|
data?: JsonObject;
|
|
104
119
|
}
|
|
@@ -116,4 +131,25 @@ export interface TokenPair {
|
|
|
116
131
|
|
|
117
132
|
export interface AuthSessionResponse extends TokenPair {
|
|
118
133
|
user: UserRecordDto;
|
|
119
|
-
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface PendingVerificationResponse {
|
|
137
|
+
status: 'pending_verification';
|
|
138
|
+
message: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface CompletedChangePasswordResponse {
|
|
142
|
+
status: 'completed';
|
|
143
|
+
message?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface PendingConfirmationChangePasswordResponse {
|
|
147
|
+
status: 'pending_confirmation';
|
|
148
|
+
message: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type RegisterResult = AuthSessionResponse | PendingVerificationResponse;
|
|
152
|
+
export type VerifyEmailResult = AuthSessionResponse;
|
|
153
|
+
export type ChangePasswordResult =
|
|
154
|
+
| CompletedChangePasswordResponse
|
|
155
|
+
| PendingConfirmationChangePasswordResponse;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeon/accounts-communications",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@forgeon/accounts-api": "workspace:*",
|
|
12
|
+
"@forgeon/accounts-contracts": "workspace:*",
|
|
13
|
+
"@forgeon/communications": "workspace:*",
|
|
14
|
+
"@nestjs/common": "^11.0.1",
|
|
15
|
+
"class-transformer": "^0.5.1",
|
|
16
|
+
"class-validator": "^0.14.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.10.7",
|
|
20
|
+
"typescript": "^5.7.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Post,
|
|
5
|
+
Req,
|
|
6
|
+
UseGuards,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { JwtAuthGuard } from '@forgeon/accounts-api';
|
|
9
|
+
import type { AuthAccessTokenPayload } from '@forgeon/accounts-api';
|
|
10
|
+
import { AuthCommunicationsService } from './auth-communications.service';
|
|
11
|
+
import {
|
|
12
|
+
ConfirmChangeEmailDto,
|
|
13
|
+
ConfirmChangePasswordDto,
|
|
14
|
+
ConfirmPasswordResetDto,
|
|
15
|
+
RequestChangeEmailDto,
|
|
16
|
+
RequestPasswordResetDto,
|
|
17
|
+
VerifyEmailDto,
|
|
18
|
+
} from './dto';
|
|
19
|
+
|
|
20
|
+
type RequestWithUser = { user?: AuthAccessTokenPayload };
|
|
21
|
+
|
|
22
|
+
@Controller('auth')
|
|
23
|
+
export class AuthCommunicationsController {
|
|
24
|
+
constructor(private readonly authCommunicationsService: AuthCommunicationsService) {}
|
|
25
|
+
|
|
26
|
+
@Post('verify-email')
|
|
27
|
+
verifyEmail(@Body() body: VerifyEmailDto) {
|
|
28
|
+
return this.authCommunicationsService.verifyEmail(body.token);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Post('password-reset/request')
|
|
32
|
+
requestPasswordReset(@Body() body: RequestPasswordResetDto) {
|
|
33
|
+
return this.authCommunicationsService.requestPasswordReset(body.email);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Post('password-reset/confirm')
|
|
37
|
+
confirmPasswordReset(@Body() body: ConfirmPasswordResetDto) {
|
|
38
|
+
return this.authCommunicationsService.confirmPasswordReset(body.token, body.newPassword);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Post('change-password/confirm')
|
|
42
|
+
confirmChangePassword(@Body() body: ConfirmChangePasswordDto) {
|
|
43
|
+
return this.authCommunicationsService.confirmChangePassword(body.token);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@UseGuards(JwtAuthGuard)
|
|
47
|
+
@Post('change-email/request')
|
|
48
|
+
requestChangeEmail(@Body() body: RequestChangeEmailDto, @Req() request: RequestWithUser) {
|
|
49
|
+
const user = this.getRequestUser(request);
|
|
50
|
+
return this.authCommunicationsService.requestChangeEmail(user.sub, body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Post('change-email/confirm')
|
|
54
|
+
confirmChangeEmail(@Body() body: ConfirmChangeEmailDto) {
|
|
55
|
+
return this.authCommunicationsService.confirmChangeEmail(body.token);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private getRequestUser(request: RequestWithUser): AuthAccessTokenPayload {
|
|
59
|
+
const user = request.user;
|
|
60
|
+
if (!user?.sub) {
|
|
61
|
+
return {
|
|
62
|
+
sub: 'unknown',
|
|
63
|
+
email: 'unknown@invalid.local',
|
|
64
|
+
type: 'access',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return user;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type {
|
|
3
|
+
ChangePasswordRequest,
|
|
4
|
+
ChangePasswordResult,
|
|
5
|
+
PendingVerificationResponse,
|
|
6
|
+
RegisterRequest,
|
|
7
|
+
RequestChangeEmailRequest,
|
|
8
|
+
VerifyEmailResult,
|
|
9
|
+
} from '@forgeon/accounts-contracts';
|
|
10
|
+
import {
|
|
11
|
+
AUTH_PENDING_OPERATION_TYPES,
|
|
12
|
+
AuthCoreService,
|
|
13
|
+
AuthPasswordService,
|
|
14
|
+
} from '@forgeon/accounts-api';
|
|
15
|
+
import { CommunicationsService } from '@forgeon/communications';
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class AuthCommunicationsService {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly authCoreService: AuthCoreService,
|
|
21
|
+
private readonly authPasswordService: AuthPasswordService,
|
|
22
|
+
private readonly communicationsService: CommunicationsService,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async registerWithPendingVerification(input: RegisterRequest): Promise<PendingVerificationResponse> {
|
|
26
|
+
const account = await this.authCoreService.createPasswordAccount(input, {
|
|
27
|
+
status: 'pending_verification',
|
|
28
|
+
emailVerifiedAt: null,
|
|
29
|
+
});
|
|
30
|
+
const operation = await this.authCoreService.issuePendingOperation({
|
|
31
|
+
userId: account.id,
|
|
32
|
+
type: AUTH_PENDING_OPERATION_TYPES.emailVerification,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await this.communicationsService.send({
|
|
36
|
+
kind: 'email_verification_code',
|
|
37
|
+
channels: ['email'],
|
|
38
|
+
recipient: { email: account.providerId },
|
|
39
|
+
payload: {
|
|
40
|
+
NAME: account.profile?.name ?? 'there',
|
|
41
|
+
TOKEN: operation.token,
|
|
42
|
+
},
|
|
43
|
+
locale: account.settings?.locale ?? undefined,
|
|
44
|
+
metadata: {
|
|
45
|
+
USER_ID: account.id,
|
|
46
|
+
SOURCE: 'accounts-communications.register',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
status: 'pending_verification',
|
|
52
|
+
message: 'Verification email sent',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async verifyEmail(token: string): Promise<VerifyEmailResult> {
|
|
57
|
+
const operation = await this.authCoreService.readPendingOperation(
|
|
58
|
+
token,
|
|
59
|
+
AUTH_PENDING_OPERATION_TYPES.emailVerification,
|
|
60
|
+
);
|
|
61
|
+
const account = await this.authCoreService.markEmailVerified(operation.userId);
|
|
62
|
+
await this.authCoreService.consumePendingOperation(operation.id);
|
|
63
|
+
await this.sendWelcomeEmailPlaceholder(account.providerId);
|
|
64
|
+
return this.authCoreService.issueSessionForAccount(account);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async requestPasswordReset(email: string) {
|
|
68
|
+
const account = await this.authCoreService.findPasswordAccountByEmail(email);
|
|
69
|
+
if (account) {
|
|
70
|
+
const operation = await this.authCoreService.issuePendingOperation({
|
|
71
|
+
userId: account.id,
|
|
72
|
+
type: AUTH_PENDING_OPERATION_TYPES.passwordReset,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await this.communicationsService.send({
|
|
76
|
+
kind: 'password_reset',
|
|
77
|
+
channels: ['email'],
|
|
78
|
+
recipient: { email: account.providerId },
|
|
79
|
+
payload: {
|
|
80
|
+
NAME: account.profile?.name ?? 'there',
|
|
81
|
+
TOKEN: operation.token,
|
|
82
|
+
},
|
|
83
|
+
locale: account.settings?.locale ?? undefined,
|
|
84
|
+
metadata: {
|
|
85
|
+
USER_ID: account.id,
|
|
86
|
+
SOURCE: 'accounts-communications.password-reset',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
status: 'accepted',
|
|
93
|
+
message: 'If the account exists, a reset email was sent',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async confirmPasswordReset(token: string, newPassword: string) {
|
|
98
|
+
const operation = await this.authCoreService.readPendingOperation(
|
|
99
|
+
token,
|
|
100
|
+
AUTH_PENDING_OPERATION_TYPES.passwordReset,
|
|
101
|
+
);
|
|
102
|
+
const passwordHash = await this.authPasswordService.hash(newPassword);
|
|
103
|
+
await this.authCoreService.applyPasswordHash(operation.userId, passwordHash);
|
|
104
|
+
await this.authCoreService.consumePendingOperation(operation.id);
|
|
105
|
+
return {
|
|
106
|
+
status: 'completed',
|
|
107
|
+
message: 'Password reset confirmed',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async startConfirmedChangePassword(
|
|
112
|
+
userId: string,
|
|
113
|
+
input: ChangePasswordRequest,
|
|
114
|
+
): Promise<ChangePasswordResult> {
|
|
115
|
+
const account = await this.authCoreService.findAccountByUserIdOrThrow(userId);
|
|
116
|
+
const passwordHash = await this.authPasswordService.hash(input.newPassword);
|
|
117
|
+
const operation = await this.authCoreService.issuePendingOperation({
|
|
118
|
+
userId,
|
|
119
|
+
type: AUTH_PENDING_OPERATION_TYPES.passwordChange,
|
|
120
|
+
metadata: {
|
|
121
|
+
passwordHash,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await this.communicationsService.send({
|
|
126
|
+
kind: 'password_change_confirmation',
|
|
127
|
+
channels: ['email'],
|
|
128
|
+
recipient: { email: account.providerId },
|
|
129
|
+
payload: {
|
|
130
|
+
NAME: account.profile?.name ?? 'there',
|
|
131
|
+
TOKEN: operation.token,
|
|
132
|
+
},
|
|
133
|
+
locale: account.settings?.locale ?? undefined,
|
|
134
|
+
metadata: {
|
|
135
|
+
USER_ID: account.id,
|
|
136
|
+
SOURCE: 'accounts-communications.change-password',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
status: 'pending_confirmation',
|
|
142
|
+
message: 'Password change confirmation sent',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async confirmChangePassword(token: string) {
|
|
147
|
+
const operation = await this.authCoreService.readPendingOperation(
|
|
148
|
+
token,
|
|
149
|
+
AUTH_PENDING_OPERATION_TYPES.passwordChange,
|
|
150
|
+
);
|
|
151
|
+
const passwordHash = this.readMetadataString(operation.metadata, 'passwordHash');
|
|
152
|
+
await this.authCoreService.applyPasswordHash(operation.userId, passwordHash);
|
|
153
|
+
await this.authCoreService.consumePendingOperation(operation.id);
|
|
154
|
+
return {
|
|
155
|
+
status: 'completed',
|
|
156
|
+
message: 'Password changed successfully',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async requestChangeEmail(userId: string, input: RequestChangeEmailRequest) {
|
|
161
|
+
const account = await this.authCoreService.findAccountByUserIdOrThrow(userId);
|
|
162
|
+
const nextEmail = input.email.trim().toLowerCase();
|
|
163
|
+
const operation = await this.authCoreService.issuePendingOperation({
|
|
164
|
+
userId,
|
|
165
|
+
type: AUTH_PENDING_OPERATION_TYPES.emailChange,
|
|
166
|
+
metadata: {
|
|
167
|
+
email: nextEmail,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await this.communicationsService.send({
|
|
172
|
+
kind: 'email_change_confirmation',
|
|
173
|
+
channels: ['email'],
|
|
174
|
+
recipient: { email: nextEmail },
|
|
175
|
+
payload: {
|
|
176
|
+
NAME: account.profile?.name ?? 'there',
|
|
177
|
+
TOKEN: operation.token,
|
|
178
|
+
},
|
|
179
|
+
locale: account.settings?.locale ?? undefined,
|
|
180
|
+
metadata: {
|
|
181
|
+
USER_ID: account.id,
|
|
182
|
+
SOURCE: 'accounts-communications.change-email',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
status: 'accepted',
|
|
188
|
+
message: 'Email change confirmation sent',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async confirmChangeEmail(token: string) {
|
|
193
|
+
const operation = await this.authCoreService.readPendingOperation(
|
|
194
|
+
token,
|
|
195
|
+
AUTH_PENDING_OPERATION_TYPES.emailChange,
|
|
196
|
+
);
|
|
197
|
+
const nextEmail = this.readMetadataString(operation.metadata, 'email');
|
|
198
|
+
await this.authCoreService.updatePrimaryEmail(operation.userId, nextEmail);
|
|
199
|
+
await this.authCoreService.consumePendingOperation(operation.id);
|
|
200
|
+
return {
|
|
201
|
+
status: 'completed',
|
|
202
|
+
message: 'Email changed successfully',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async sendWelcomeEmailPlaceholder(_email: string): Promise<void> {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async sendAccountNotificationPlaceholder(_email: string): Promise<void> {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private readMetadataString(metadata: Record<string, unknown> | null, key: string): string {
|
|
215
|
+
const value = metadata?.[key];
|
|
216
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
217
|
+
throw new Error(`Missing pending operation metadata: ${key}`);
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type {
|
|
3
|
+
ChangePasswordRequest,
|
|
4
|
+
ChangePasswordResult,
|
|
5
|
+
} from '@forgeon/accounts-contracts';
|
|
6
|
+
import type { ChangePasswordHandler } from '@forgeon/accounts-api';
|
|
7
|
+
import { AuthCommunicationsService } from './auth-communications.service';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class ConfirmedChangePasswordHandler implements ChangePasswordHandler {
|
|
11
|
+
constructor(private readonly authCommunicationsService: AuthCommunicationsService) {}
|
|
12
|
+
|
|
13
|
+
execute(userId: string, input: ChangePasswordRequest): Promise<ChangePasswordResult> {
|
|
14
|
+
return this.authCommunicationsService.startConfirmedChangePassword(userId, input);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfirmChangePasswordRequest } from '@forgeon/accounts-contracts';
|
|
2
|
+
import { IsString, MinLength } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
export class ConfirmChangePasswordDto implements ConfirmChangePasswordRequest {
|
|
5
|
+
@IsString()
|
|
6
|
+
@MinLength(8)
|
|
7
|
+
token!: string;
|
|
8
|
+
}
|
|
@@ -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,6 @@
|
|
|
1
|
+
export * from './confirm-change-email.dto';
|
|
2
|
+
export * from './confirm-change-password.dto';
|
|
3
|
+
export * from './confirm-password-reset.dto';
|
|
4
|
+
export * from './request-change-email.dto';
|
|
5
|
+
export * from './request-password-reset.dto';
|
|
6
|
+
export * from './verify-email.dto';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { RegisterRequest, RegisterResult } from '@forgeon/accounts-contracts';
|
|
3
|
+
import type { RegisterHandler } from '@forgeon/accounts-api';
|
|
4
|
+
import { AuthCommunicationsService } from './auth-communications.service';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class PendingVerificationRegisterHandler implements RegisterHandler {
|
|
8
|
+
constructor(private readonly authCommunicationsService: AuthCommunicationsService) {}
|
|
9
|
+
|
|
10
|
+
execute(input: RegisterRequest): Promise<RegisterResult> {
|
|
11
|
+
return this.authCommunicationsService.registerWithPendingVerification(input);
|
|
12
|
+
}
|
|
13
|
+
}
|