create-forgeon 0.3.24 → 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/communications.mjs +4 -3
- package/src/modules/dependencies.test.mjs +37 -14
- package/src/modules/executor.mjs +2 -0
- package/src/modules/executor.test.mjs +105 -16
- package/src/modules/registry.mjs +24 -8
- 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-fragments/communications/20_scope.md +5 -2
- 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 +29 -5
- 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/communications/packages/communications/src/communications-config.loader.ts +2 -1
- package/templates/module-presets/communications/packages/communications/src/communications-env.schema.ts +21 -5
- package/templates/module-presets/communications/packages/communications/src/email/providers/gmail-smtp-email.provider.ts +52 -12
- package/templates/module-presets/communications/packages/communications/src/forgeon-communications.module.ts +2 -1
|
@@ -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
|
+
}
|
|
@@ -31,12 +31,13 @@ export const communicationsConfig = registerAs(
|
|
|
31
31
|
COMMUNICATIONS_CONFIG_NAMESPACE,
|
|
32
32
|
(): CommunicationsConfigValues => {
|
|
33
33
|
const env = parseCommunicationsEnv(process.env as unknown as Record<string, unknown>);
|
|
34
|
+
const derivedFrom = env.COMMUNICATIONS_EMAIL_FROM || env.COMMUNICATIONS_EMAIL_SMTP_USER;
|
|
34
35
|
|
|
35
36
|
return {
|
|
36
37
|
templatesRoot: path.resolve(process.cwd(), env.COMMUNICATIONS_TEMPLATES_ROOT),
|
|
37
38
|
email: {
|
|
38
39
|
provider: env.COMMUNICATIONS_EMAIL_PROVIDER,
|
|
39
|
-
from:
|
|
40
|
+
from: derivedFrom,
|
|
40
41
|
replyTo: env.COMMUNICATIONS_EMAIL_REPLY_TO || null,
|
|
41
42
|
subjectPrefix: env.COMMUNICATIONS_EMAIL_SUBJECT_PREFIX || null,
|
|
42
43
|
smtp: {
|
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
function normalizeEnvBoolean(value: unknown): unknown {
|
|
4
|
+
if (typeof value === 'string') {
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (['false', '0', 'no', 'off', ''].includes(normalized)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const envBoolean = z.preprocess(normalizeEnvBoolean, z.boolean());
|
|
2
18
|
|
|
3
19
|
export const communicationsEnvSchema = z
|
|
4
20
|
.object({
|
|
5
21
|
COMMUNICATIONS_TEMPLATES_ROOT: z.string().trim().min(1).default('resources/communications'),
|
|
6
22
|
COMMUNICATIONS_EMAIL_PROVIDER: z.enum(['gmail-smtp']).default('gmail-smtp'),
|
|
7
|
-
COMMUNICATIONS_EMAIL_FROM: z.string().trim().
|
|
23
|
+
COMMUNICATIONS_EMAIL_FROM: z.string().trim().default(''),
|
|
8
24
|
COMMUNICATIONS_EMAIL_REPLY_TO: z.string().trim().default(''),
|
|
9
25
|
COMMUNICATIONS_EMAIL_SUBJECT_PREFIX: z.string().trim().default('[Forgeon]'),
|
|
10
26
|
COMMUNICATIONS_EMAIL_SMTP_HOST: z.string().trim().min(1).default('smtp.gmail.com'),
|
|
11
27
|
COMMUNICATIONS_EMAIL_SMTP_PORT: z.coerce.number().int().min(1).max(65535).default(587),
|
|
12
|
-
COMMUNICATIONS_EMAIL_SMTP_SECURE:
|
|
28
|
+
COMMUNICATIONS_EMAIL_SMTP_SECURE: envBoolean.default(false),
|
|
13
29
|
COMMUNICATIONS_EMAIL_SMTP_USER: z.string().trim().default(''),
|
|
14
30
|
COMMUNICATIONS_EMAIL_SMTP_PASS: z.string().trim().default(''),
|
|
15
31
|
COMMUNICATIONS_SMS_PROVIDER: z.enum(['stub']).default('stub'),
|
|
16
|
-
COMMUNICATIONS_PUSH_PROVIDER: z.enum(['stub']).default('stub')
|
|
32
|
+
COMMUNICATIONS_PUSH_PROVIDER: z.enum(['stub']).default('stub'),
|
|
17
33
|
})
|
|
18
34
|
.passthrough();
|
|
19
35
|
|
|
@@ -21,4 +37,4 @@ export type CommunicationsEnv = z.infer<typeof communicationsEnvSchema>;
|
|
|
21
37
|
|
|
22
38
|
export function parseCommunicationsEnv(input: Record<string, unknown>): CommunicationsEnv {
|
|
23
39
|
return communicationsEnvSchema.parse(input);
|
|
24
|
-
}
|
|
40
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { EmailProvider, EmailProviderSendInput, EmailProviderSendResult } f
|
|
|
6
6
|
|
|
7
7
|
const EMAIL_ERROR_CODES = {
|
|
8
8
|
providerNotConfigured: 'COMMUNICATIONS_EMAIL_PROVIDER_NOT_CONFIGURED',
|
|
9
|
+
providerSendFailed: 'COMMUNICATIONS_EMAIL_PROVIDER_SEND_FAILED',
|
|
9
10
|
} as const;
|
|
10
11
|
|
|
11
12
|
@Injectable()
|
|
@@ -30,20 +31,33 @@ export class GmailSmtpEmailProvider implements EmailProvider {
|
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
try {
|
|
35
|
+
const response = await this.getTransporter().sendMail({
|
|
36
|
+
from: this.configService.emailFrom,
|
|
37
|
+
to: input.to,
|
|
38
|
+
subject: input.subject,
|
|
39
|
+
html: input.html,
|
|
40
|
+
replyTo: input.replyTo ?? this.configService.emailReplyTo ?? undefined,
|
|
41
|
+
});
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
this.logger.log(`email.sent provider=${this.providerId} to=${input.to} messageId=${response.messageId ?? 'n/a'}`);
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
return {
|
|
46
|
+
status: 'sent',
|
|
47
|
+
messageId: response.messageId ?? null,
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const details = this.extractErrorDetails(error);
|
|
51
|
+
this.logger.error(`email.failed provider=${this.providerId} to=${input.to} details=${JSON.stringify(details)}`);
|
|
52
|
+
throw new ServiceUnavailableException({
|
|
53
|
+
message: 'Email delivery failed',
|
|
54
|
+
details: {
|
|
55
|
+
code: EMAIL_ERROR_CODES.providerSendFailed,
|
|
56
|
+
provider: this.providerId,
|
|
57
|
+
...details,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
private getTransporter(): Transporter {
|
|
@@ -61,4 +75,30 @@ export class GmailSmtpEmailProvider implements EmailProvider {
|
|
|
61
75
|
|
|
62
76
|
return this.transporter;
|
|
63
77
|
}
|
|
78
|
+
|
|
79
|
+
private extractErrorDetails(error: unknown): Record<string, unknown> {
|
|
80
|
+
if (!error || typeof error !== 'object') {
|
|
81
|
+
return { raw: String(error) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const candidate = error as {
|
|
85
|
+
code?: unknown;
|
|
86
|
+
command?: unknown;
|
|
87
|
+
response?: unknown;
|
|
88
|
+
responseCode?: unknown;
|
|
89
|
+
errno?: unknown;
|
|
90
|
+
syscall?: unknown;
|
|
91
|
+
message?: unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
message: typeof candidate.message === 'string' ? candidate.message : String(error),
|
|
96
|
+
code: candidate.code ?? null,
|
|
97
|
+
command: candidate.command ?? null,
|
|
98
|
+
responseCode: candidate.responseCode ?? null,
|
|
99
|
+
response: candidate.response ?? null,
|
|
100
|
+
errno: candidate.errno ?? null,
|
|
101
|
+
syscall: candidate.syscall ?? null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
64
104
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DynamicModule, Module, ModuleMetadata } from '@nestjs/common';
|
|
1
|
+
import { DynamicModule, Global, Module, ModuleMetadata } from '@nestjs/common';
|
|
2
2
|
import {
|
|
3
3
|
COMMUNICATIONS_EMAIL_PROVIDER,
|
|
4
4
|
COMMUNICATIONS_PUSH_PROVIDER,
|
|
@@ -20,6 +20,7 @@ export interface ForgeonCommunicationsModuleOptions {
|
|
|
20
20
|
imports?: ModuleMetadata['imports'];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
@Global()
|
|
23
24
|
@Module({})
|
|
24
25
|
export class ForgeonCommunicationsModule {
|
|
25
26
|
static register(options: ForgeonCommunicationsModuleOptions = {}): DynamicModule {
|