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
All checks were successful
Staging Build / build (push) Successful in 3m28s
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user