feat: forgot password flow, member page fixes, country name display
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s
Password reset: - /forgot-password page: enter email, receive reset link via email - /reset-password?token=xxx page: set new password with validation - user.requestPasswordReset: generates token, sends styled email - user.resetPassword: validates token, hashes new password - Does NOT trigger re-onboarding — only resets the password - 30-minute token expiry, cleared after use - Added passwordResetToken/passwordResetExpiresAt to User model Member detail page fixes: - Hide "Expertise & Capacity" card for applicants/audience roles - Show country names with flag emojis instead of raw ISO codes - Login "Forgot password?" now links to /forgot-password page Project detail page: - Team member details show full country names with flags Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -346,6 +346,42 @@ Together for a healthier ocean.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate password reset email template
|
||||
*/
|
||||
function getPasswordResetTemplate(url: string, expiryMinutes: number = 30): EmailTemplate {
|
||||
const content = `
|
||||
${sectionTitle('Reset your password')}
|
||||
${paragraph('We received a request to reset your password for the MOPC Portal. Click the button below to choose a new password.')}
|
||||
${infoBox(`<strong>This link expires in ${expiryMinutes} minutes</strong>`, 'warning')}
|
||||
${ctaButton(url, 'Reset Password')}
|
||||
<p style="color: ${BRAND.textMuted}; margin: 24px 0 0 0; font-size: 13px; text-align: center;">
|
||||
If you didn't request a password reset, you can safely ignore this email. Your password will not change.
|
||||
</p>
|
||||
`
|
||||
|
||||
return {
|
||||
subject: 'Reset your password — MOPC Portal',
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
Reset your password
|
||||
=========================
|
||||
|
||||
Click the link below to reset your password:
|
||||
|
||||
${url}
|
||||
|
||||
This link will expire in ${expiryMinutes} minutes.
|
||||
|
||||
If you didn't request this, you can safely ignore this email.
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate generic invitation email template (not round-specific)
|
||||
*/
|
||||
@@ -2232,6 +2268,26 @@ export async function sendStyledNotificationEmail(
|
||||
// Email Sending Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Send password reset email
|
||||
*/
|
||||
export async function sendPasswordResetEmail(
|
||||
email: string,
|
||||
url: string,
|
||||
expiryMinutes: number = 30
|
||||
): Promise<void> {
|
||||
const template = getPasswordResetTemplate(url, expiryMinutes)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send magic link email for authentication
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user