feat(email): improve password reset emails, onboarding templates, and handle server action errors without production obfuscation
All checks were successful
Staging Build / build (push) Successful in 3m28s

This commit is contained in:
DanielS
2026-07-03 13:34:43 +02:00
parent f914e9cdf1
commit 12d06e46ad
4 changed files with 148 additions and 36 deletions

View File

@@ -30,8 +30,12 @@ export function ForgotPasswordForm({
setError(null);
try {
await resetPassword(email);
setSuccess(true);
const res = await resetPassword(email);
if (res.success) {
setSuccess(true);
} else {
setError(res.error || "Ein Fehler ist aufgetreten.");
}
} catch (error: unknown) {
setError(error instanceof Error ? error.message : "Ein Fehler ist aufgetreten.");
} finally {

View File

@@ -43,8 +43,12 @@ export function UpdatePasswordForm({
}
try {
await updatePassword(password);
router.push("/");
const res = await updatePassword(password);
if (res.success) {
router.push("/");
} else {
setError(res.error || "Ein Fehler ist aufgetreten");
}
} catch (error: unknown) {
setError(error instanceof Error ? error.message : "Ein Fehler ist aufgetreten");
} finally {

View File

@@ -4,6 +4,7 @@ import { createClient } from '@/lib/supabase/server'
import { createAdminClient } from '@/lib/supabase/admin'
import { headers } from 'next/headers'
import { sendLockoutEmail } from '@/lib/utils/email'
import { sendMail } from '@/utils/mail'
export async function signIn(email: string, password: string) {
try {
@@ -108,25 +109,92 @@ export async function signOut() {
return { success: true }
}
function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) {
return `
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f8fafc; padding: 40px 10px; margin: 0; width: 100%;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0;">
<!-- Header banner with gradient -->
<div style="background: linear-gradient(135deg, #1e3a8a 0%, #312e81 100%); padding: 32px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.5px;">${title}</h1>
</div>
<!-- Content -->
<div style="padding: 40px 32px; background-color: #ffffff;">
${messageHtml}
${buttonText && buttonUrl ? `
<div style="text-align: center; margin: 32px 0;">
<a href="${buttonUrl}" style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 15px; display: inline-block; box-shadow: 0 4px 10px rgba(37, 99, 235, 0.2); transition: all 0.2s ease;">
${buttonText}
</a>
</div>
<p style="color: #64748b; font-size: 13px; line-height: 1.6; margin-top: 32px; border-top: 1px solid #f1f5f9; padding-top: 20px;">
Falls der Button nicht funktioniert, kopieren Sie bitte diesen Link in Ihren Browser:<br>
<a href="${buttonUrl}" style="color: #2563eb; text-decoration: none; word-break: break-all;">${buttonUrl}</a>
</p>
` : ''}
<hr style="border: 0; border-top: 1px solid #f1f5f9; margin: 32px 0;">
<p style="color: #94a3b8; font-size: 11px; text-align: center; margin: 0; line-height: 1.6;">
Dies ist eine automatisch generierte E-Mail.<br>
© ${new Date().getFullYear()} CASPOS Shop. Alle Rechte vorbehalten.
</p>
</div>
</div>
</div>
`;
}
export async function resetPassword(email: string) {
const supabase = await createClient()
const origin = (await headers()).get('origin') || ''
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/update-password`,
})
if (error) {
throw new Error(error.message)
try {
const admin = createAdminClient()
const { data, error } = await admin.auth.admin.generateLink({
type: 'recovery',
email,
})
if (error) {
return { success: false, error: error.message }
}
const tokenHash = (data as any)?.properties?.hashed_token;
if (!tokenHash) {
return { success: false, error: 'Wiederherstellungs-Token konnte nicht generiert werden.' }
}
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'
const resetLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`
const messageHtml = `
<p style="color: #475569; font-size: 16px; line-height: 1.6; margin-top: 0;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Sie haben das Zurücksetzen Ihres Passworts angefordert.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Bitte klicken Sie auf den untenstehenden Button, um ein neues Passwort festzulegen:</p>
`;
await sendMail({
to: email,
subject: 'Passwort zurücksetzen CASPOS Shop',
text: `Hallo,\n\nSie haben das Zurücksetzen Ihres Passworts angefordert.\n\nBitte klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:\n${resetLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: getBeautifulEmailHtml('Passwort zurücksetzen', messageHtml, 'Passwort zurücksetzen', resetLink)
})
return { success: true }
} catch (err: any) {
console.error('Password reset error:', err)
return { success: false, error: err.message || 'Fehler beim Senden der Passwort-Zurücksetzen-E-Mail.' }
}
return { success: true }
}
export async function updatePassword(password: string) {
const supabase = await createClient()
const { error } = await supabase.auth.updateUser({ password })
if (error) {
throw new Error(error.message)
try {
const supabase = await createClient()
const { error } = await supabase.auth.updateUser({ password })
if (error) {
return { success: false, error: error.message }
}
return { success: true }
} catch (err: any) {
console.error('Update password error:', err)
return { success: false, error: err.message || 'Fehler beim Aktualisieren des Passworts.' }
}
return { success: true }
}
export async function verifyAdmin() {

View File

@@ -88,22 +88,17 @@ export async function createUser(data: { email: string; role?: 'admin' | 'partne
await sendMail({
to: data.email,
subject: 'Willkommen bei CASPOS Shop Ihr Benutzerkonto wurde erstellt',
text: `Hallo,\n\nein neues Benutzerkonto wurde für Sie auf CASPOS Shop erstellt.\n\nBitte klicken Sie auf den folgenden Link, um Ihr persönliches Passwort festzulegen und Ihr Konto zu aktivieren:\n${setupLink}\n\nDieser Link ist zeitlich begrenzt gültig.\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
<h2 style="color: #0f172a; margin-bottom: 16px;">Willkommen bei CASPOS Shop!</h2>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">für Sie wurde erfolgreich ein neues Benutzerkonto auf unserer Plattform eingerichtet.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Bitte klicken Sie auf den untenstehenden Button, um Ihr Passwort festzulegen und Ihr Konto zu aktivieren:</p>
<div style="margin: 24px 0;">
<a href="${setupLink}" style="background-color: #3b82f6; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block;">Passwort jetzt festlegen</a>
</div>
<p style="color: #64748b; font-size: 14px; line-height: 1.5;">Falls der Button nicht funktioniert, können Sie auch folgenden Link in Ihren Browser kopieren:<br>
<a href="${setupLink}" style="color: #3b82f6;">${setupLink}</a></p>
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;">
<p style="color: #94a3b8; font-size: 12px;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
</div>
`,
text: `Hallo,\n\nein neues Benutzerkonto wurde für Sie auf CASPOS Shop erstellt.\n\nBitte klicken Sie auf den folgenden Link, um Ihr persönliches Passwort festzulegen und Ihr Konto zu aktivieren:\n${setupLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: getBeautifulEmailHtml(
'Willkommen bei CASPOS Shop',
`
<p style="color: #475569; font-size: 16px; line-height: 1.6; margin-top: 0;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">für Sie wurde erfolgreich ein neues Benutzerkonto auf unserer Plattform eingerichtet.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Bitte klicken Sie auf den untenstehenden Button, um Ihr Passwort festzulegen und Ihr Konto zu aktivieren:</p>
`,
'Passwort festlegen',
setupLink
),
});
}
}
@@ -184,11 +179,16 @@ export async function resetPassword(email: string) {
// Build link pointing to our own /auth/confirm route
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
const resetLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
const messageHtml = `
<p style="color: #475569; font-size: 16px; line-height: 1.6; margin-top: 0;">Hallo,</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">der Administrator hat das Zurücksetzen Ihres Passworts angefordert.</p>
<p style="color: #475569; font-size: 16px; line-height: 1.6;">Bitte klicken Sie auf den untenstehenden Button, um ein neues Passwort für Ihr Konto festzulegen:</p>
`;
await sendMail({
to: email,
subject: 'Passwort zurücksetzen',
text: `Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen: ${resetLink}`,
html: `<p>Klicken Sie <a href="${resetLink}">hier</a>, um Ihr Passwort zurückzusetzen.</p>`,
subject: 'Passwort zurücksetzen CASPOS Shop',
text: `Hallo,\n\nder Administrator hat das Zurücksetzen Ihres Passworts angefordert.\n\nBitte klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:\n${resetLink}\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
html: getBeautifulEmailHtml('Passwort zurücksetzen', messageHtml, 'Passwort zurücksetzen', resetLink),
});
}
@@ -232,3 +232,39 @@ export async function updateUserProfile(id: string, data: { first_name: string |
revalidatePath('/admin/users');
}
function getBeautifulEmailHtml(title: string, messageHtml: string, buttonText?: string, buttonUrl?: string) {
return `
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f8fafc; padding: 40px 10px; margin: 0; width: 100%;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0;">
<!-- Header banner with gradient -->
<div style="background: linear-gradient(135deg, #1e3a8a 0%, #312e81 100%); padding: 32px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.5px;">${title}</h1>
</div>
<!-- Content -->
<div style="padding: 40px 32px; background-color: #ffffff;">
${messageHtml}
${buttonText && buttonUrl ? `
<div style="text-align: center; margin: 32px 0;">
<a href="${buttonUrl}" style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 15px; display: inline-block; box-shadow: 0 4px 10px rgba(37, 99, 235, 0.2); transition: all 0.2s ease;">
${buttonText}
</a>
</div>
<p style="color: #64748b; font-size: 13px; line-height: 1.6; margin-top: 32px; border-top: 1px solid #f1f5f9; padding-top: 20px;">
Falls der Button nicht funktioniert, kopieren Sie bitte diesen Link in Ihren Browser:<br>
<a href="${buttonUrl}" style="color: #2563eb; text-decoration: none; word-break: break-all;">${buttonUrl}</a>
</p>
` : ''}
<hr style="border: 0; border-top: 1px solid #f1f5f9; margin: 32px 0;">
<p style="color: #94a3b8; font-size: 11px; text-align: center; margin: 0; line-height: 1.6;">
Dies ist eine automatisch generierte E-Mail.<br>
© ${new Date().getFullYear()} CASPOS Shop. Alle Rechte vorbehalten.
</p>
</div>
</div>
</div>
`;
}