Fix login page UX + add password reset flow
- Move Field component outside RegisterForm — fixes focus loss on every keystroke
- Remove setError('') at submit start — fixes error "flash" that looked like page reload
- Clear errors onChange instead, so error persists until user starts correcting
- Add 'Forgot password?' link in LoginForm
- Add ForgotPasswordForm (email input, success state)
- Add ResetPasswordPage at /reset-password?token=...
- Backend: POST /api/auth/forgot-password — generates 1h token, sends email
- Backend: POST /api/auth/reset-password — validates token, updates password hash
- Backend: migration 006 — reset_token/reset_token_expires columns
- Backend: sendPasswordResetEmail in email.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,52 @@ const transporter = nodemailer.createTransport({
|
||||
},
|
||||
})
|
||||
|
||||
export async function sendPasswordResetEmail(to: string, name: string, token: string): Promise<void> {
|
||||
const appUrl = process.env.APP_URL ?? 'https://app.hotelsync.ru'
|
||||
const resetUrl = `${appUrl}/reset-password?token=${token}`
|
||||
const fromAddr = process.env.SMTP_USER ?? 'noreply@hotelsync.ru'
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"HotelSync" <${fromAddr}>`,
|
||||
to,
|
||||
subject: 'Сброс пароля — HotelSync',
|
||||
html: `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
<body style="font-family:Arial,sans-serif;background:#f1f5f9;margin:0;padding:40px 16px;">
|
||||
<div style="max-width:520px;margin:0 auto;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,0.08);">
|
||||
<div style="background:linear-gradient(135deg,#4f46e5,#6366f1);padding:32px 40px;text-align:center;">
|
||||
<div style="display:inline-block;width:48px;height:48px;background:rgba(255,255,255,0.2);border-radius:12px;line-height:48px;font-size:24px;margin-bottom:12px;">🔑</div>
|
||||
<h1 style="color:#fff;margin:0;font-size:26px;font-weight:700;letter-spacing:-0.5px;">HotelSync</h1>
|
||||
<p style="color:#c7d2fe;margin:6px 0 0;font-size:14px;">Современная PMS-система</p>
|
||||
</div>
|
||||
<div style="padding:40px;">
|
||||
<h2 style="color:#1e293b;font-size:20px;margin:0 0 12px;font-weight:600;">Сброс пароля</h2>
|
||||
<p style="color:#475569;line-height:1.7;margin:0 0 8px;font-size:15px;">Привет, ${name}!</p>
|
||||
<p style="color:#475569;line-height:1.7;margin:0 0 28px;font-size:15px;">
|
||||
Мы получили запрос на сброс пароля для вашего аккаунта HotelSync. Нажмите на кнопку ниже, чтобы создать новый пароль.
|
||||
</p>
|
||||
<div style="text-align:center;margin:0 0 28px;">
|
||||
<a href="${resetUrl}" style="display:inline-block;background:#4f46e5;color:#fff;text-decoration:none;padding:14px 36px;border-radius:10px;font-weight:600;font-size:16px;letter-spacing:0.2px;">
|
||||
Создать новый пароль
|
||||
</a>
|
||||
</div>
|
||||
<p style="color:#94a3b8;font-size:13px;margin:0 0 16px;line-height:1.6;background:#f8fafc;padding:12px 16px;border-radius:8px;border-left:3px solid #e2e8f0;">
|
||||
⏱ Ссылка действительна <strong>1 час</strong>.<br>
|
||||
Если вы не запрашивали сброс пароля — просто проигнорируйте это письмо.
|
||||
</p>
|
||||
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0;">
|
||||
<p style="color:#94a3b8;font-size:12px;margin:0;line-height:1.6;">
|
||||
Кнопка не работает? Скопируйте ссылку в браузер:<br>
|
||||
<a href="${resetUrl}" style="color:#6366f1;word-break:break-all;font-size:11px;">${resetUrl}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendConfirmationEmail(to: string, name: string, token: string): Promise<void> {
|
||||
const apiUrl = process.env.API_URL ?? 'https://api.hotelsync.ru'
|
||||
const confirmUrl = `${apiUrl}/api/auth/confirm-email?token=${token}`
|
||||
|
||||
Reference in New Issue
Block a user