feat(profile): add password change and activity filters

Implement password change API and UI in the Account tab
Add secondary filters for contributions, reactions, and comments
Display referral summary in the profile header
This commit is contained in:
2026-05-03 13:52:35 +08:00
parent 0e835f9c03
commit 282481bbcc
8 changed files with 453 additions and 23 deletions

View File

@@ -53,9 +53,11 @@ type AuthMessageKey =
| 'emailVerified'
| 'checkPasswordResetEmail'
| 'passwordResetComplete'
| 'passwordChanged'
| 'invalidCredentials'
| 'verifyEmailFirst'
| 'invalidResetToken'
| 'currentPasswordInvalid'
| 'invalidReferralCode'
| 'emailSubject'
| 'emailHtml'
@@ -171,9 +173,11 @@ function authMessage(locale: string, key: AuthMessageKey, params: Record<string,
emailVerified: 'server.auth.emailVerified',
checkPasswordResetEmail: 'server.auth.checkPasswordResetEmail',
passwordResetComplete: 'server.auth.passwordResetComplete',
passwordChanged: 'server.auth.passwordChanged',
invalidCredentials: 'server.auth.invalidCredentials',
verifyEmailFirst: 'server.auth.verifyEmailFirst',
invalidResetToken: 'server.auth.invalidResetToken',
currentPasswordInvalid: 'server.auth.currentPasswordInvalid',
invalidReferralCode: 'server.auth.invalidReferralCode',
emailSubject: 'email.auth.verificationSubject',
emailHtml: 'email.auth.verificationHtml',
@@ -1007,6 +1011,40 @@ export async function updateCurrentUser(
return (await publicUserById(user.id)) ?? toPublicUser(user);
}
export async function changeCurrentUserPassword(
userId: number,
payload: Record<string, unknown>,
currentSessionToken: string,
locale = defaultLocale
): Promise<{ message: string }> {
const currentPassword = typeof payload.currentPassword === 'string' ? payload.currentPassword : '';
const nextPassword = await cleanPassword(payload.password, locale);
if (!currentPassword) {
throw statusError(await authMessage(locale, 'currentPasswordInvalid'), 400);
}
const user = await queryOne<LoginUserRow>(
'SELECT id, email, display_name, email_verified_at, password_hash FROM users WHERE id = $1',
[userId]
);
if (!user || !(await verifyPassword(currentPassword, user.password_hash))) {
throw statusError(await authMessage(locale, 'currentPasswordInvalid'), 400);
}
const passwordHash = await hashPassword(nextPassword);
const currentSessionHash = hashToken(currentSessionToken);
await withTransaction(async (client) => {
await client.query('UPDATE users SET password_hash = $1, updated_at = now() WHERE id = $2', [passwordHash, user.id]);
await client.query('UPDATE password_reset_tokens SET used_at = now() WHERE user_id = $1 AND used_at IS NULL', [user.id]);
await client.query('DELETE FROM user_sessions WHERE user_id = $1 AND token_hash <> $2', [user.id, currentSessionHash]);
});
return { message: await authMessage(locale, 'passwordChanged') };
}
export async function getReferralSummary(userId: number): Promise<ReferralSummary> {
return withTransaction(async (client) => {
const code = await ensureReferralCode(client, userId);