@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 +2 -2
- package/package.json +1 -1
- package/templates/integrations/strapi-astro/backend/config/plugins.ts.template +20 -0
- package/templates/integrations/strapi-astro/backend/config/server.ts.template +15 -0
- package/templates/integrations/strapi-astro/backend/src/extensions/users-permissions/strapi-server.ts.template +223 -0
- package/templates/integrations/strapi-astro/integration.config.json +12 -0
- package/templates/integrations/strapi-astro/production.backend.env.template +19 -0
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
|
|
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
|
@@ -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
|