create-forgeon 0.3.23 → 0.3.25
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 +14 -12
- package/package.json +1 -1
- package/src/modules/accounts.mjs +3 -1
- package/src/modules/communications.mjs +233 -0
- package/src/modules/dependencies.test.mjs +22 -11
- package/src/modules/executor.mjs +4 -0
- package/src/modules/executor.test.mjs +93 -2
- package/src/modules/registry.mjs +24 -8
- package/src/modules/shared/probes.mjs +3 -1
- package/src/run-add-module.test.mjs +4 -0
- package/templates/base/apps/web/src/App.tsx +75 -11
- package/templates/base/apps/web/src/probes.ts +11 -1
- package/templates/base/apps/web/src/styles.css +25 -0
- package/templates/module-fragments/accounts/20_scope.md +3 -2
- package/templates/module-fragments/accounts/90_status_implemented.md +2 -2
- package/templates/module-fragments/accounts/90_status_planned.md +1 -1
- package/templates/module-fragments/communications/00_title.md +1 -0
- package/templates/module-fragments/communications/10_overview.md +6 -0
- package/templates/module-fragments/communications/20_scope.md +23 -0
- package/templates/module-fragments/communications/90_status_implemented.md +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +60 -20
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +1 -7
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +1 -1
- package/templates/module-presets/communications/packages/communications/package.json +23 -0
- package/templates/module-presets/communications/packages/communications/src/communications-config.loader.ts +59 -0
- package/templates/module-presets/communications/packages/communications/src/communications-config.module.ts +11 -0
- package/templates/module-presets/communications/packages/communications/src/communications-config.service.ts +60 -0
- package/templates/module-presets/communications/packages/communications/src/communications-env.schema.ts +40 -0
- package/templates/module-presets/communications/packages/communications/src/communications.constants.ts +3 -0
- package/templates/module-presets/communications/packages/communications/src/communications.probe.controller.ts +18 -0
- package/templates/module-presets/communications/packages/communications/src/communications.service.ts +104 -0
- package/templates/module-presets/communications/packages/communications/src/communications.types.ts +55 -0
- package/templates/module-presets/communications/packages/communications/src/dto/send-communications-probe.dto.ts +6 -0
- package/templates/module-presets/communications/packages/communications/src/email/email-channel.service.ts +90 -0
- package/templates/module-presets/communications/packages/communications/src/email/email-provider.port.ts +16 -0
- package/templates/module-presets/communications/packages/communications/src/email/providers/gmail-smtp-email.provider.ts +104 -0
- package/templates/module-presets/communications/packages/communications/src/forgeon-communications.module.ts +65 -0
- package/templates/module-presets/communications/packages/communications/src/index.ts +21 -0
- package/templates/module-presets/communications/packages/communications/src/push/providers/stub-push.provider.ts +16 -0
- package/templates/module-presets/communications/packages/communications/src/push/push-channel.service.ts +56 -0
- package/templates/module-presets/communications/packages/communications/src/push/push-provider.port.ts +14 -0
- package/templates/module-presets/communications/packages/communications/src/sms/providers/stub-sms.provider.ts +16 -0
- package/templates/module-presets/communications/packages/communications/src/sms/sms-channel.service.ts +56 -0
- package/templates/module-presets/communications/packages/communications/src/sms/sms-provider.port.ts +14 -0
- package/templates/module-presets/communications/packages/communications/src/template-loader.service.ts +98 -0
- package/templates/module-presets/communications/packages/communications/src/template-renderer.service.ts +30 -0
- package/templates/module-presets/communications/packages/communications/tsconfig.json +9 -0
- package/templates/module-presets/communications/resources/communications/email/communications_probe.html +9 -0
- package/templates/module-presets/communications/resources/communications/email/communications_probe.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/email/email_verification_code.html +8 -0
- package/templates/module-presets/communications/resources/communications/email/email_verification_code.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/email/password_reset.html +8 -0
- package/templates/module-presets/communications/resources/communications/email/password_reset.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/email/welcome_email.html +8 -0
- package/templates/module-presets/communications/resources/communications/email/welcome_email.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/push/login_alert.json +4 -0
- package/templates/module-presets/communications/resources/communications/sms/phone_verification.txt +1 -0
- package/templates/module-presets/i18n/apps/web/src/App.tsx +72 -8
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +0 -13
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { COMMUNICATIONS_SMS_PROVIDER } from '../communications.constants';
|
|
3
|
+
import { TemplateLoaderService } from '../template-loader.service';
|
|
4
|
+
import { TemplateRendererService } from '../template-renderer.service';
|
|
5
|
+
import type { CommunicationMessageInput, CommunicationResult } from '../communications.types';
|
|
6
|
+
import type { SmsProvider } from './sms-provider.port';
|
|
7
|
+
|
|
8
|
+
const SMS_ERROR_CODES = {
|
|
9
|
+
missingRecipient: 'COMMUNICATIONS_SMS_RECIPIENT_REQUIRED',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class SmsChannelService {
|
|
14
|
+
private readonly logger = new Logger(SmsChannelService.name);
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly templateLoader: TemplateLoaderService,
|
|
18
|
+
private readonly templateRenderer: TemplateRendererService,
|
|
19
|
+
@Inject(COMMUNICATIONS_SMS_PROVIDER)
|
|
20
|
+
private readonly smsProvider: SmsProvider,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async send(input: CommunicationMessageInput): Promise<CommunicationResult> {
|
|
24
|
+
const recipient = input.recipient.phone?.trim();
|
|
25
|
+
if (!recipient) {
|
|
26
|
+
throw new BadRequestException({
|
|
27
|
+
message: 'SMS channel requires a phone recipient',
|
|
28
|
+
details: {
|
|
29
|
+
code: SMS_ERROR_CODES.missingRecipient,
|
|
30
|
+
channel: 'sms',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const template = await this.templateLoader.loadChannelTemplate('sms', input.kind, input.locale);
|
|
36
|
+
const text = this.templateRenderer.render(template, {
|
|
37
|
+
...(input.payload ?? {}),
|
|
38
|
+
...(input.metadata ?? {}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const response = await this.smsProvider.send({ phone: recipient, text });
|
|
42
|
+
this.logger.warn(`communications.sms kind=${input.kind} provider=${this.smsProvider.providerId} phone=${recipient}`);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
kind: input.kind,
|
|
46
|
+
channel: 'sms',
|
|
47
|
+
provider: this.smsProvider.providerId,
|
|
48
|
+
status: response.status,
|
|
49
|
+
messageId: response.messageId,
|
|
50
|
+
recipient,
|
|
51
|
+
metadata: {
|
|
52
|
+
locale: input.locale ?? null,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
package/templates/module-presets/communications/packages/communications/src/sms/sms-provider.port.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SmsProviderSendInput {
|
|
2
|
+
phone: string;
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SmsProviderSendResult {
|
|
7
|
+
status: 'stub' | 'sent';
|
|
8
|
+
messageId: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SmsProvider {
|
|
12
|
+
readonly providerId: string;
|
|
13
|
+
send(input: SmsProviderSendInput): Promise<SmsProviderSendResult>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { promises as fsPromises } from 'node:fs';
|
|
4
|
+
import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common';
|
|
5
|
+
import type { CommunicationChannel } from './communications.types';
|
|
6
|
+
import { CommunicationsConfigService } from './communications-config.service';
|
|
7
|
+
|
|
8
|
+
type TemplateVariant = 'body' | 'subject';
|
|
9
|
+
|
|
10
|
+
const CHANNEL_EXTENSIONS: Record<CommunicationChannel, string> = {
|
|
11
|
+
email: '.html',
|
|
12
|
+
sms: '.txt',
|
|
13
|
+
push: '.json',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SUBJECT_EXTENSION = '.subject.txt';
|
|
17
|
+
const TEMPLATE_ERROR_CODES = {
|
|
18
|
+
invalidKey: 'COMMUNICATIONS_TEMPLATE_INVALID_KEY',
|
|
19
|
+
missing: 'COMMUNICATIONS_TEMPLATE_MISSING',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class TemplateLoaderService {
|
|
24
|
+
constructor(private readonly configService: CommunicationsConfigService) {}
|
|
25
|
+
|
|
26
|
+
async loadChannelTemplate(
|
|
27
|
+
channel: CommunicationChannel,
|
|
28
|
+
kind: string,
|
|
29
|
+
locale?: string,
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
const relativePath = this.resolveTemplateRelativePath(channel, kind, 'body', locale);
|
|
32
|
+
return this.readTemplate(relativePath, channel, kind);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async loadOptionalEmailSubjectTemplate(kind: string, locale?: string): Promise<string | null> {
|
|
36
|
+
const relativePath = this.resolveTemplateRelativePath('email', kind, 'subject', locale);
|
|
37
|
+
const absolutePath = path.join(this.configService.templatesRoot, relativePath);
|
|
38
|
+
if (!fs.existsSync(absolutePath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return fsPromises.readFile(absolutePath, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private resolveTemplateRelativePath(
|
|
46
|
+
channel: CommunicationChannel,
|
|
47
|
+
kind: string,
|
|
48
|
+
variant: TemplateVariant,
|
|
49
|
+
locale?: string,
|
|
50
|
+
): string {
|
|
51
|
+
const safeKind = this.sanitizeSegment(kind, 'kind');
|
|
52
|
+
const safeLocale = locale ? this.sanitizeSegment(locale, 'locale') : null;
|
|
53
|
+
const extension = variant === 'subject' ? SUBJECT_EXTENSION : CHANNEL_EXTENSIONS[channel];
|
|
54
|
+
|
|
55
|
+
if (safeLocale) {
|
|
56
|
+
const localizedPath = path.join(channel, `${safeKind}.${safeLocale}${extension}`);
|
|
57
|
+
const absoluteLocalizedPath = path.join(this.configService.templatesRoot, localizedPath);
|
|
58
|
+
if (fs.existsSync(absoluteLocalizedPath)) {
|
|
59
|
+
return localizedPath;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return path.join(channel, `${safeKind}${extension}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async readTemplate(relativePath: string, channel: CommunicationChannel, kind: string): Promise<string> {
|
|
67
|
+
const absolutePath = path.join(this.configService.templatesRoot, relativePath);
|
|
68
|
+
if (!fs.existsSync(absolutePath)) {
|
|
69
|
+
throw new InternalServerErrorException({
|
|
70
|
+
message: 'Communication template was not found',
|
|
71
|
+
details: {
|
|
72
|
+
code: TEMPLATE_ERROR_CODES.missing,
|
|
73
|
+
channel,
|
|
74
|
+
kind,
|
|
75
|
+
relativePath,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return fsPromises.readFile(absolutePath, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private sanitizeSegment(value: string, field: 'kind' | 'locale'): string {
|
|
84
|
+
const normalized = value.trim();
|
|
85
|
+
if (!/^[a-z0-9._-]+$/i.test(normalized)) {
|
|
86
|
+
throw new BadRequestException({
|
|
87
|
+
message: `Communication ${field} contains unsupported characters`,
|
|
88
|
+
details: {
|
|
89
|
+
code: TEMPLATE_ERROR_CODES.invalidKey,
|
|
90
|
+
field,
|
|
91
|
+
value,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { CommunicationPayloadValue } from './communications.types';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class TemplateRendererService {
|
|
6
|
+
render(template: string, values: Record<string, CommunicationPayloadValue> = {}): string {
|
|
7
|
+
let output = template;
|
|
8
|
+
|
|
9
|
+
for (const [rawKey, rawValue] of Object.entries(values)) {
|
|
10
|
+
const replacement = this.stringifyValue(rawValue);
|
|
11
|
+
const normalizedKeys = new Set([rawKey, rawKey.toUpperCase()]);
|
|
12
|
+
|
|
13
|
+
for (const key of normalizedKeys) {
|
|
14
|
+
output = output.split(`$${key}$`).join(replacement);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return output;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private stringifyValue(value: CommunicationPayloadValue): string {
|
|
22
|
+
if (value instanceof Date) {
|
|
23
|
+
return value.toISOString();
|
|
24
|
+
}
|
|
25
|
+
if (value === null || value === undefined) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #111827;">
|
|
3
|
+
<h1>Communications Probe</h1>
|
|
4
|
+
<p>This is a test message from the Forgeon communications module.</p>
|
|
5
|
+
<p>Recipient: <strong>$EMAIL$</strong></p>
|
|
6
|
+
<p>Probe id: <strong>$PROBE_ID$</strong></p>
|
|
7
|
+
<p>Generated at: <strong>$DATE$</strong></p>
|
|
8
|
+
</body>
|
|
9
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Communications probe
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #111827;">
|
|
3
|
+
<h1>Email Verification</h1>
|
|
4
|
+
<p>Hello $NAME$,</p>
|
|
5
|
+
<p>Your verification code is <strong>$CODE$</strong>.</p>
|
|
6
|
+
<p>If you did not request this, you can safely ignore this message.</p>
|
|
7
|
+
</body>
|
|
8
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Verify your email address
|
package/templates/module-presets/communications/resources/communications/email/password_reset.html
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #111827;">
|
|
3
|
+
<h1>Password Reset</h1>
|
|
4
|
+
<p>Hello $NAME$,</p>
|
|
5
|
+
<p>Use this reset token to continue: <strong>$TOKEN$</strong>.</p>
|
|
6
|
+
<p>If you did not request a password reset, please ignore this email.</p>
|
|
7
|
+
</body>
|
|
8
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reset your password
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Welcome to Forgeon
|
package/templates/module-presets/communications/resources/communications/sms/phone_verification.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Phone verification code: $CODE$
|
|
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
3
3
|
import i18n from './i18n';
|
|
4
4
|
import * as i18nWeb from '@forgeon/i18n-web';
|
|
5
5
|
import type { I18nLocale } from '@forgeon/i18n-web';
|
|
6
|
-
import { probeDefinitions, type ProbeDefinition, type ProbeResult } from './probes';
|
|
6
|
+
import { probeDefinitions, type ProbeDefinition, type ProbeInputDefinition, type ProbeResult } from './probes';
|
|
7
7
|
import './styles.css';
|
|
8
8
|
|
|
9
9
|
type ProbeState = {
|
|
@@ -12,17 +12,42 @@ type ProbeState = {
|
|
|
12
12
|
loading: boolean;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
type ProbeInputState = Record<string, string>;
|
|
16
|
+
|
|
15
17
|
const emptyProbeState: ProbeState = {
|
|
16
18
|
result: null,
|
|
17
19
|
error: null,
|
|
18
20
|
loading: false,
|
|
19
21
|
};
|
|
20
22
|
|
|
23
|
+
function resolveBodyTemplate(value: unknown, inputs: ProbeInputState): unknown {
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const match = value.match(/^\$INPUT\.([a-zA-Z0-9_-]+)\$$/);
|
|
26
|
+
if (match) {
|
|
27
|
+
return inputs[match[1]] ?? '';
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map((item) => resolveBodyTemplate(item, inputs));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (value && typeof value === 'object') {
|
|
37
|
+
return Object.fromEntries(
|
|
38
|
+
Object.entries(value).map(([key, nestedValue]) => [key, resolveBodyTemplate(nestedValue, inputs)]),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
export default function App() {
|
|
22
46
|
const { t } = useTranslation(['ui']);
|
|
23
47
|
const { I18N_LOCALES, getInitialLocale, persistLocale, toLangQuery } = i18nWeb;
|
|
24
48
|
const [locale, setLocale] = useState<I18nLocale>(getInitialLocale);
|
|
25
49
|
const [probeState, setProbeState] = useState<Record<string, ProbeState>>({});
|
|
50
|
+
const [probeInputs, setProbeInputs] = useState<Record<string, ProbeInputState>>({});
|
|
26
51
|
|
|
27
52
|
const changeLocale = (nextLocale: I18nLocale) => {
|
|
28
53
|
setLocale(nextLocale);
|
|
@@ -30,15 +55,39 @@ export default function App() {
|
|
|
30
55
|
void i18n.changeLanguage(nextLocale);
|
|
31
56
|
};
|
|
32
57
|
|
|
58
|
+
const getProbeInputValue = (probeId: string, input: ProbeInputDefinition): string => {
|
|
59
|
+
return probeInputs[probeId]?.[input.id] ?? input.defaultValue ?? '';
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const updateProbeInput = (probeId: string, inputId: string, value: string) => {
|
|
63
|
+
setProbeInputs((current) => ({
|
|
64
|
+
...current,
|
|
65
|
+
[probeId]: {
|
|
66
|
+
...(current[probeId] ?? {}),
|
|
67
|
+
[inputId]: value,
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
};
|
|
71
|
+
|
|
33
72
|
const requestProbe = async (probe: ProbeDefinition): Promise<ProbeResult> => {
|
|
34
|
-
const
|
|
35
|
-
|
|
73
|
+
const method = probe.request?.method ?? 'GET';
|
|
74
|
+
const headers: Record<string, string> = {
|
|
75
|
+
...(probe.request?.headers ?? {}),
|
|
76
|
+
'Accept-Language': locale,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const requestInit: RequestInit = {
|
|
80
|
+
method,
|
|
36
81
|
cache: 'no-store',
|
|
37
|
-
headers
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
headers,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (method !== 'GET' && probe.request?.body !== undefined) {
|
|
86
|
+
headers['Content-Type'] = 'application/json';
|
|
87
|
+
requestInit.body = JSON.stringify(resolveBodyTemplate(probe.request.body, probeInputs[probe.id] ?? {}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const response = await fetch(`/api${probe.path}${toLangQuery(locale)}`, requestInit);
|
|
42
91
|
|
|
43
92
|
let body: unknown = null;
|
|
44
93
|
try {
|
|
@@ -113,6 +162,21 @@ export default function App() {
|
|
|
113
162
|
{current.loading ? 'Running...' : probe.buttonLabel}
|
|
114
163
|
</button>
|
|
115
164
|
</div>
|
|
165
|
+
{probe.inputs?.length ? (
|
|
166
|
+
<div className="probe-inputs">
|
|
167
|
+
{probe.inputs.map((input) => (
|
|
168
|
+
<label key={`${probe.id}-${input.id}`} className="probe-input">
|
|
169
|
+
<span>{input.label}</span>
|
|
170
|
+
<input
|
|
171
|
+
type={input.type ?? 'text'}
|
|
172
|
+
value={getProbeInputValue(probe.id, input)}
|
|
173
|
+
placeholder={input.placeholder}
|
|
174
|
+
onChange={(event) => updateProbeInput(probe.id, input.id, event.target.value)}
|
|
175
|
+
/>
|
|
176
|
+
</label>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
116
180
|
<div className="probe-output">
|
|
117
181
|
<h3>{probe.resultTitle}</h3>
|
|
118
182
|
{current.error ? <p className="error">{current.error}</p> : null}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export const ACCOUNTS_EMAIL_PORT = 'FORGEON_ACCOUNTS_EMAIL_PORT';
|
|
2
|
-
|
|
3
|
-
export interface AccountsEmailPort {
|
|
4
|
-
sendVerificationEmail(input: { email: string; token: string; userId: string }): Promise<void>;
|
|
5
|
-
sendPasswordResetEmail(input: { email: string; token: string; userId: string }): Promise<void>;
|
|
6
|
-
sendWelcomeEmail(input: { email: string; userId: string }): Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class StubAccountsEmailAdapter implements AccountsEmailPort {
|
|
10
|
-
async sendVerificationEmail(): Promise<void> {}
|
|
11
|
-
async sendPasswordResetEmail(): Promise<void> {}
|
|
12
|
-
async sendWelcomeEmail(): Promise<void> {}
|
|
13
|
-
}
|