feat: forgot password flow, member page fixes, country name display
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:
2026-03-05 13:49:43 +01:00
parent b6ba5d7145
commit ee8e90132e
10 changed files with 606 additions and 29 deletions

View File

@@ -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
*/