@smi-digital/create-smi-app 2.9.1 → 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 +16 -14
- package/package.json +1 -1
- package/templates/base/package.json.template +1 -1
- package/templates/integrations/strapi-astro/.gitattributes.template +4 -0
- package/templates/integrations/strapi-astro/.gitvault/clean.sh.template +41 -0
- package/templates/integrations/strapi-astro/.gitvault/merge.sh.template +42 -0
- package/templates/integrations/strapi-astro/.gitvault/textconv.sh.template +10 -0
- 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 +24 -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
|
);
|
|
@@ -702,21 +702,23 @@ async function activateVaultFilter(projectRoot) {
|
|
|
702
702
|
} catch {
|
|
703
703
|
return;
|
|
704
704
|
}
|
|
705
|
-
const
|
|
706
|
-
"git",
|
|
707
|
-
["config", `filter.ansible-vault.${key}`, value],
|
|
708
|
-
projectRoot
|
|
709
|
-
);
|
|
705
|
+
const setConfig = async (key, value) => runCommandQuiet("git", ["config", key, value], projectRoot);
|
|
710
706
|
try {
|
|
711
|
-
await
|
|
712
|
-
|
|
713
|
-
"ansible-vault
|
|
714
|
-
);
|
|
715
|
-
await setFilter(
|
|
716
|
-
"smudge",
|
|
707
|
+
await setConfig("filter.ansible-vault.clean", "sh .gitvault/clean.sh %f");
|
|
708
|
+
await setConfig(
|
|
709
|
+
"filter.ansible-vault.smudge",
|
|
717
710
|
"ansible-vault decrypt --vault-password-file .vault-password --output=- -"
|
|
718
711
|
);
|
|
719
|
-
await
|
|
712
|
+
await setConfig("filter.ansible-vault.required", "true");
|
|
713
|
+
await setConfig(
|
|
714
|
+
"merge.ansible-vault.name",
|
|
715
|
+
"Ansible-Vault aware 3-way merge"
|
|
716
|
+
);
|
|
717
|
+
await setConfig(
|
|
718
|
+
"merge.ansible-vault.driver",
|
|
719
|
+
"sh .gitvault/merge.sh %O %A %B %P"
|
|
720
|
+
);
|
|
721
|
+
await setConfig("diff.ansible-vault.textconv", "sh .gitvault/textconv.sh");
|
|
720
722
|
} catch {
|
|
721
723
|
console.warn(
|
|
722
724
|
"Could not configure the Ansible-Vault git filter automatically. Run `npm run setup:vault` before committing."
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"prettier": "npx prettier --write .",
|
|
10
10
|
"lint": "npx eslint .",
|
|
11
11
|
"prepare": "husky",
|
|
12
|
-
"setup:vault": "git config filter.ansible-vault.clean
|
|
12
|
+
"setup:vault": "git config filter.ansible-vault.clean 'sh .gitvault/clean.sh %f' && git config filter.ansible-vault.smudge 'ansible-vault decrypt --vault-password-file .vault-password --output=- -' && git config filter.ansible-vault.required true && git config merge.ansible-vault.name 'Ansible-Vault aware 3-way merge' && git config merge.ansible-vault.driver 'sh .gitvault/merge.sh %O %A %B %P' && git config diff.ansible-vault.textconv 'sh .gitvault/textconv.sh' && echo '✅ Ansible-Vault Git filters configured successfully!'",
|
|
13
13
|
"knip": "knip",
|
|
14
14
|
"depcruise": "depcruise . --exclude \"node_modules|dist|.husky|test\"",
|
|
15
15
|
"depcheck": "npm run knip && npm run depcruise",
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
# Use Ansible-Vault Git filters for production environment files
|
|
2
2
|
production.*.env filter=ansible-vault diff=ansible-vault merge=ansible-vault
|
|
3
|
+
|
|
4
|
+
# The vault filter/merge/diff helpers are invoked as shell scripts; force LF so a
|
|
5
|
+
# CRLF checkout on Windows can't break them.
|
|
6
|
+
.gitvault/*.sh text eol=lf
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Idempotent Ansible-Vault "clean" filter (encrypt on the way INTO git).
|
|
3
|
+
#
|
|
4
|
+
# Why this exists: `ansible-vault encrypt` uses a random salt, so encrypting the
|
|
5
|
+
# same plaintext twice produces DIFFERENT ciphertext. With a naive clean filter
|
|
6
|
+
# git re-encrypts on every status/diff/checkout and therefore ALWAYS sees the
|
|
7
|
+
# file as "modified" — phantom diffs, salt-only churn baked into history, and
|
|
8
|
+
# rebases/stashes that conflict on the ciphertext.
|
|
9
|
+
#
|
|
10
|
+
# This wrapper makes the filter deterministic: if the plaintext is unchanged
|
|
11
|
+
# from what is already committed, it re-emits the EXISTING ciphertext verbatim
|
|
12
|
+
# (no re-salt), so git sees no change. Only a real plaintext edit re-encrypts.
|
|
13
|
+
#
|
|
14
|
+
# Invoked by git as: sh .gitvault/clean.sh %f (%f = repo-relative path)
|
|
15
|
+
# stdin = working-tree plaintext
|
|
16
|
+
# stdout = ciphertext to store in the index/commit
|
|
17
|
+
set -eu
|
|
18
|
+
|
|
19
|
+
path="${1:-}"
|
|
20
|
+
pwfile=".vault-password"
|
|
21
|
+
|
|
22
|
+
new="$(mktemp)"
|
|
23
|
+
old_cipher="$(mktemp)"
|
|
24
|
+
old_plain="$(mktemp)"
|
|
25
|
+
trap 'rm -f "$new" "$old_cipher" "$old_plain"' EXIT
|
|
26
|
+
|
|
27
|
+
cat > "$new"
|
|
28
|
+
|
|
29
|
+
# The ciphertext already recorded for this path in the index (if any). Missing on
|
|
30
|
+
# a first add, or unreadable if it was committed as plaintext once — both cases
|
|
31
|
+
# fall through to a fresh encrypt.
|
|
32
|
+
if [ -n "$path" ] \
|
|
33
|
+
&& git cat-file -p ":$path" > "$old_cipher" 2>/dev/null \
|
|
34
|
+
&& ansible-vault decrypt --vault-password-file "$pwfile" --output=- "$old_cipher" > "$old_plain" 2>/dev/null \
|
|
35
|
+
&& cmp -s "$new" "$old_plain"; then
|
|
36
|
+
# Plaintext identical to what's stored → reuse the stored ciphertext (no re-salt).
|
|
37
|
+
cat "$old_cipher"
|
|
38
|
+
else
|
|
39
|
+
# New file or a genuine plaintext change → encrypt fresh.
|
|
40
|
+
ansible-vault encrypt --vault-password-file "$pwfile" --output=- "$new"
|
|
41
|
+
fi
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Ansible-Vault aware merge driver — merges at the PLAINTEXT level.
|
|
3
|
+
#
|
|
4
|
+
# Git's default merge would 3-way-merge the opaque ciphertext and conflict on
|
|
5
|
+
# nearly everything (different salts even when the plaintext agrees). This driver
|
|
6
|
+
# decrypts the three inputs, runs a normal 3-way text merge, then RE-ENCRYPTS the
|
|
7
|
+
# result. Git stores %A's bytes verbatim and then runs the smudge filter on them,
|
|
8
|
+
# so %A must contain CIPHERTEXT (verified behaviour).
|
|
9
|
+
#
|
|
10
|
+
# Invoked by git as: sh .gitvault/merge.sh %O %A %B %P
|
|
11
|
+
# %O = common-ancestor (base) ciphertext file
|
|
12
|
+
# %A = our version ciphertext file (ALSO the output file git reads back)
|
|
13
|
+
# %B = their version ciphertext file
|
|
14
|
+
# %P = the real pathname (used only for conflict-marker labels)
|
|
15
|
+
# Exit 0 = clean merge; non-zero = conflict (git marks the path unmerged, then
|
|
16
|
+
# smudges %A so the working tree shows readable plaintext conflict markers).
|
|
17
|
+
set -eu
|
|
18
|
+
|
|
19
|
+
base="$1"; ours="$2"; theirs="$3"; path="${4:-file}"
|
|
20
|
+
pwfile=".vault-password"
|
|
21
|
+
|
|
22
|
+
# Decrypt $1 to stdout; if it isn't vault-encrypted (e.g. an empty base), pass
|
|
23
|
+
# it through unchanged so the merge still has something to work with.
|
|
24
|
+
dec() {
|
|
25
|
+
ansible-vault decrypt --vault-password-file "$pwfile" --output=- "$1" 2>/dev/null || cat "$1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
o="$(mktemp)"; a="$(mktemp)"; b="$(mktemp)"
|
|
29
|
+
trap 'rm -f "$o" "$a" "$b"' EXIT
|
|
30
|
+
dec "$base" > "$o"
|
|
31
|
+
dec "$ours" > "$a"
|
|
32
|
+
dec "$theirs" > "$b"
|
|
33
|
+
|
|
34
|
+
# 3-way merge on plaintext, written in place into "$a". Returns the conflict
|
|
35
|
+
# count (0 = clean); capture it instead of letting `set -e` abort.
|
|
36
|
+
status=0
|
|
37
|
+
git merge-file -L "ours ($path)" -L "base ($path)" -L "theirs ($path)" "$a" "$o" "$b" || status=$?
|
|
38
|
+
|
|
39
|
+
# Re-encrypt the (possibly conflict-marked) result back into %A as ciphertext.
|
|
40
|
+
ansible-vault encrypt --vault-password-file "$pwfile" --output=- "$a" > "$ours"
|
|
41
|
+
|
|
42
|
+
exit "$status"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Ansible-Vault diff textconv: render a vault file as plaintext for `git diff`.
|
|
3
|
+
#
|
|
4
|
+
# git applies this to BOTH sides of a diff — the committed blob (ciphertext) and
|
|
5
|
+
# the working-tree copy (already decrypted by the smudge filter). So decrypt when
|
|
6
|
+
# possible and fall back to cat when the input isn't vault-encrypted, otherwise
|
|
7
|
+
# the working-tree side errors with "Input is not vault encrypted data".
|
|
8
|
+
#
|
|
9
|
+
# Invoked by git as: sh .gitvault/textconv.sh <path>
|
|
10
|
+
ansible-vault decrypt --vault-password-file .vault-password --output=- "$1" 2>/dev/null || cat "$1"
|
|
@@ -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"
|
|
@@ -81,6 +93,18 @@
|
|
|
81
93
|
"template": ".gitattributes.template",
|
|
82
94
|
"target": ".gitattributes"
|
|
83
95
|
},
|
|
96
|
+
{
|
|
97
|
+
"template": ".gitvault/clean.sh.template",
|
|
98
|
+
"target": ".gitvault/clean.sh"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"template": ".gitvault/merge.sh.template",
|
|
102
|
+
"target": ".gitvault/merge.sh"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"template": ".gitvault/textconv.sh.template",
|
|
106
|
+
"target": ".gitvault/textconv.sh"
|
|
107
|
+
},
|
|
84
108
|
{
|
|
85
109
|
"template": "production.frontend.env.template",
|
|
86
110
|
"target": "production.frontend.env"
|
|
@@ -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
|