@smi-digital/create-smi-app 2.9.2 → 2.10.0

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/dist/index.js CHANGED
@@ -505,10 +505,10 @@ async function createApps(projectRoot, targets) {
505
505
  );
506
506
  }
507
507
  await runStep(
508
- "Adding Postgres driver (pg) for production",
508
+ "Adding Postgres + email-provider dependencies for production",
509
509
  async () => runCommandQuiet(
510
510
  "npm",
511
- ["install", "pg"],
511
+ ["install", "pg", "@strapi/provider-email-nodemailer"],
512
512
  join4(projectRoot, target.directory)
513
513
  )
514
514
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smi-digital/create-smi-app",
3
- "version": "2.9.2",
3
+ "version": "2.10.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,20 @@
1
+ export default ({ env }) => ({
2
+ email: {
3
+ config: {
4
+ provider: 'nodemailer',
5
+ providerOptions: {
6
+ host: env('SMTP_HOST', 'smtp.example.com'),
7
+ port: env.int('SMTP_PORT', 587),
8
+ auth: {
9
+ user: env('SMTP_USER'),
10
+ pass: env('SMTP_PASSWORD'),
11
+ },
12
+ secure: false, // true for 465, false for other ports
13
+ },
14
+ settings: {
15
+ defaultFrom: env('SMTP_DEFAULT_FROM'),
16
+ defaultReplyTo: env('SMTP_DEFAULT_REPLY_TO'),
17
+ },
18
+ },
19
+ },
20
+ });
@@ -0,0 +1,15 @@
1
+ import type { Core } from '@strapi/strapi';
2
+
3
+ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
4
+ host: env('HOST', '0.0.0.0'),
5
+ port: env.int('PORT', 1337),
6
+ // Public URL of the CMS backend itself. Used to build absolute links such as
7
+ // the admin password-reset email; without it Strapi falls back to host:port
8
+ // (e.g. http://0.0.0.0:1337). Set via PUBLIC_URL in production.backend.env.
9
+ url: env('PUBLIC_URL', undefined),
10
+ app: {
11
+ keys: env.array('APP_KEYS'),
12
+ },
13
+ });
14
+
15
+ export default config;
@@ -0,0 +1,223 @@
1
+ import type { Core } from '@strapi/strapi';
2
+
3
+ export type EmailLocale = 'de' | 'en';
4
+
5
+ export interface ResetPasswordTemplateVars {
6
+ USER: { username: string; email: string };
7
+ URL: string;
8
+ TOKEN: string;
9
+ }
10
+
11
+ export interface EmailConfirmationTemplateVars {
12
+ USER: { username: string; email: string };
13
+ URL: string;
14
+ CODE: string;
15
+ }
16
+
17
+ // ─── Reset Password ────────────────────────────────────────────────────────────
18
+
19
+ const resetPasswordSubject: Record<EmailLocale, string> = {
20
+ de: 'Passwort zurücksetzen – __PROJECT_DOMAIN__',
21
+ en: 'Reset your password – __PROJECT_DOMAIN__',
22
+ };
23
+
24
+ const resetPasswordHtml: Record<EmailLocale, (v: ResetPasswordTemplateVars) => string> = {
25
+ de: (v) => `
26
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
27
+ <h2>Hallo ${v.USER.username},</h2>
28
+ <p>wir haben eine Anfrage zum Zurücksetzen deines Passworts erhalten.</p>
29
+ <p>Klicke auf den folgenden Link, um dein Passwort zurückzusetzen:</p>
30
+ <p>
31
+ <a href="${v.URL}?code=${v.TOKEN}"
32
+ style="display:inline-block;padding:12px 24px;background:#1C135D;color:#fff;text-decoration:none;border-radius:4px">
33
+ Passwort zurücksetzen
34
+ </a>
35
+ </p>
36
+ <p style="color:#888;font-size:13px">
37
+ Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.
38
+ </p>
39
+ </div>`,
40
+
41
+ en: (v) => `
42
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
43
+ <h2>Hi ${v.USER.username},</h2>
44
+ <p>We received a request to reset your password.</p>
45
+ <p>Click the link below to choose a new password:</p>
46
+ <p>
47
+ <a href="${v.URL}?code=${v.TOKEN}"
48
+ style="display:inline-block;padding:12px 24px;background:#1C135D;color:#fff;text-decoration:none;border-radius:4px">
49
+ Reset password
50
+ </a>
51
+ </p>
52
+ <p style="color:#888;font-size:13px">
53
+ If you did not request this, you can safely ignore this email.
54
+ </p>
55
+ </div>`,
56
+ };
57
+
58
+ const resetPasswordText: Record<EmailLocale, (v: ResetPasswordTemplateVars) => string> = {
59
+ de: (v) =>
60
+ `Hallo ${v.USER.username},\n\nKlicke auf diesen Link, um dein Passwort zurückzusetzen:\n${v.URL}?code=${v.TOKEN}\n\nFalls du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail.`,
61
+ en: (v) =>
62
+ `Hi ${v.USER.username},\n\nClick this link to reset your password:\n${v.URL}?code=${v.TOKEN}\n\nIf you did not request this, ignore this email.`,
63
+ };
64
+
65
+ // ─── Email Confirmation ────────────────────────────────────────────────────────
66
+
67
+ const emailConfirmationSubject: Record<EmailLocale, string> = {
68
+ de: 'E-Mail-Adresse bestätigen – __PROJECT_DOMAIN__',
69
+ en: 'Confirm your email address – __PROJECT_DOMAIN__',
70
+ };
71
+
72
+ const emailConfirmationHtml: Record<EmailLocale, (v: EmailConfirmationTemplateVars) => string> = {
73
+ de: (v) => `
74
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
75
+ <h2>Hallo ${v.USER.username},</h2>
76
+ <p>Bitte bestätige deine E-Mail-Adresse, indem du auf den folgenden Link klickst:</p>
77
+ <p>
78
+ <a href="${v.URL}?confirmation=${v.CODE}"
79
+ style="display:inline-block;padding:12px 24px;background:#1C135D;color:#fff;text-decoration:none;border-radius:4px">
80
+ E-Mail bestätigen
81
+ </a>
82
+ </p>
83
+ <p style="color:#888;font-size:13px">
84
+ Falls du kein Konto erstellt hast, kannst du diese E-Mail ignorieren.
85
+ </p>
86
+ </div>`,
87
+
88
+ en: (v) => `
89
+ <div style="font-family:sans-serif;max-width:600px;margin:0 auto">
90
+ <h2>Hi ${v.USER.username},</h2>
91
+ <p>Please confirm your email address by clicking the link below:</p>
92
+ <p>
93
+ <a href="${v.URL}?confirmation=${v.CODE}"
94
+ style="display:inline-block;padding:12px 24px;background:#1C135D;color:#fff;text-decoration:none;border-radius:4px">
95
+ Confirm email
96
+ </a>
97
+ </p>
98
+ <p style="color:#888;font-size:13px">
99
+ If you did not create an account, you can safely ignore this email.
100
+ </p>
101
+ </div>`,
102
+ };
103
+
104
+ const emailConfirmationText: Record<EmailLocale, (v: EmailConfirmationTemplateVars) => string> = {
105
+ de: (v) =>
106
+ `Hallo ${v.USER.username},\n\nBitte bestätige deine E-Mail-Adresse:\n${v.URL}?confirmation=${v.CODE}\n\nFalls du kein Konto erstellt hast, ignoriere diese E-Mail.`,
107
+ en: (v) =>
108
+ `Hi ${v.USER.username},\n\nPlease confirm your email address:\n${v.URL}?confirmation=${v.CODE}\n\nIf you did not create an account, ignore this email.`,
109
+ };
110
+
111
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
112
+
113
+ function resolveLocale(raw: unknown): EmailLocale {
114
+ if (raw === 'en') return 'en';
115
+ return 'de';
116
+ }
117
+
118
+ // ─── Extension ────────────────────────────────────────────────────────────────
119
+
120
+ export default (plugin: any): any => {
121
+ const originalBootstrap = plugin.bootstrap;
122
+
123
+ plugin.bootstrap = async ({ strapi }: { strapi: Core.Strapi }): Promise<void> => {
124
+ // Run the original bootstrap first
125
+ if (originalBootstrap) {
126
+ await originalBootstrap({ strapi });
127
+ }
128
+
129
+ const authController = strapi
130
+ .plugin('users-permissions')
131
+ .controller('auth') as Record<string, Function>;
132
+
133
+ // ── Override: forgotPassword ──────────────────────────────────────────
134
+ authController.forgotPassword = async (ctx: any): Promise<void> => {
135
+ const locale = resolveLocale(ctx.request.body?.locale ?? ctx.query?.locale);
136
+ const email: string = (ctx.request.body?.email as string)?.toLowerCase();
137
+
138
+ if (!email) {
139
+ return ctx.badRequest('email is required');
140
+ }
141
+
142
+ const user = await strapi.db
143
+ .query('plugin::users-permissions.user')
144
+ .findOne({ where: { email } });
145
+
146
+ if (!user || user.blocked) {
147
+ ctx.send({ ok: true });
148
+ return;
149
+ }
150
+
151
+ const crypto = await import('crypto');
152
+ const resetPasswordToken = crypto.randomBytes(64).toString('hex');
153
+
154
+ const baseUrl = (process.env.PROD_URL ?? '').replace(/\/+$/, '');
155
+ const resetUrl = `${baseUrl}/reset-password`;
156
+ const vars: ResetPasswordTemplateVars = {
157
+ USER: { username: user.username, email: user.email },
158
+ URL: resetUrl,
159
+ TOKEN: resetPasswordToken,
160
+ };
161
+
162
+ await strapi.db
163
+ .query('plugin::users-permissions.user')
164
+ .update({ where: { id: user.id }, data: { resetPasswordToken } });
165
+
166
+ await strapi.plugin('email').provider.send({
167
+ to: user.email,
168
+ from: process.env.SMTP_DEFAULT_FROM,
169
+ subject: resetPasswordSubject[locale],
170
+ html: resetPasswordHtml[locale](vars),
171
+ text: resetPasswordText[locale](vars),
172
+ });
173
+
174
+ ctx.send({ ok: true });
175
+ };
176
+
177
+ // ── Override: sendEmailConfirmation ───────────────────────────────────
178
+ authController.sendEmailConfirmation = async (ctx: any): Promise<void> => {
179
+ const locale = resolveLocale(ctx.request.body?.locale ?? ctx.query?.locale);
180
+ const email: string = (ctx.request.body?.email as string)?.toLowerCase();
181
+
182
+ if (!email) {
183
+ return ctx.badRequest('email is required');
184
+ }
185
+
186
+ const user = await strapi.db
187
+ .query('plugin::users-permissions.user')
188
+ .findOne({ where: { email } });
189
+
190
+ if (!user) {
191
+ return ctx.badRequest('No user found with this email');
192
+ }
193
+
194
+ if (user.confirmed) {
195
+ return ctx.badRequest('This email is already confirmed');
196
+ }
197
+
198
+ const serverUrl: string = ((strapi.config.get('server.url') as string) ?? '').replace(
199
+ /\/+$/,
200
+ '',
201
+ );
202
+ const confirmUrl = `${serverUrl}/api/auth/email-confirmation`;
203
+
204
+ const vars: EmailConfirmationTemplateVars = {
205
+ USER: { username: user.username, email: user.email },
206
+ URL: confirmUrl,
207
+ CODE: user.confirmationToken ?? '',
208
+ };
209
+
210
+ await strapi.plugin('email').provider.send({
211
+ to: user.email,
212
+ from: process.env.SMTP_DEFAULT_FROM,
213
+ subject: emailConfirmationSubject[locale],
214
+ html: emailConfirmationHtml[locale](vars),
215
+ text: emailConfirmationText[locale](vars),
216
+ });
217
+
218
+ ctx.send({ email, sent: true });
219
+ };
220
+ };
221
+
222
+ return plugin;
223
+ };
@@ -49,6 +49,18 @@
49
49
  }
50
50
  ],
51
51
  "cdFiles": [
52
+ {
53
+ "template": "backend/config/plugins.ts.template",
54
+ "target": "backend/config/plugins.ts"
55
+ },
56
+ {
57
+ "template": "backend/config/server.ts.template",
58
+ "target": "backend/config/server.ts"
59
+ },
60
+ {
61
+ "template": "backend/src/extensions/users-permissions/strapi-server.ts.template",
62
+ "target": "backend/src/extensions/users-permissions/strapi-server.ts"
63
+ },
52
64
  {
53
65
  "template": "docker-compose.yml.template",
54
66
  "target": "docker-compose.yml"
@@ -28,6 +28,25 @@ DATABASE_USERNAME=__DB_NAME__
28
28
  DATABASE_PASSWORD=__DB_PASSWORD__
29
29
  DATABASE_SSL=false
30
30
 
31
+ # Public URLs.
32
+ # PROD_URL → the public site (frontend). Used to build the end-user password
33
+ # reset link (<PROD_URL>/reset-password).
34
+ # PUBLIC_URL → the CMS backend itself. Used for absolute backend links such as
35
+ # the admin password-reset email and the email-confirmation link.
36
+ PROD_URL=https://__PROJECT_DOMAIN__
37
+ PUBLIC_URL=https://__BACKEND_DOMAIN__
38
+
39
+ # SMTP / contact form (FILL IN before relying on email — password reset,
40
+ # email confirmation, contact flow).
41
+ SMTP_HOST=
42
+ SMTP_PORT=587
43
+ SMTP_USER=
44
+ SMTP_PASSWORD=
45
+ SMTP_DEFAULT_FROM=
46
+ SMTP_DEFAULT_REPLY_TO=
47
+ CONTACTFLOW_SENDER=
48
+ CONTACTFLOW_RECEIVER=
49
+
31
50
  # Production Settings
32
51
  HOST=0.0.0.0
33
52
  PORT=1337