diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index e706660bd..47605fd4b 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -35114,7 +35114,8 @@ "properties": { "ticket": {"type": "string", "description": "Email change ticket identifier"}, "new_email": {"$ref": "#/components/schemas/EmailType"}, - "original_proof": {"type": "string", "description": "Proof token obtained from verifying the original email"} + "original_proof": {"type": "string", "description": "Proof token obtained from verifying the original email"}, + "new_password": {"$ref": "#/components/schemas/PasswordType"} }, "required": ["ticket", "new_email", "original_proof"] }, diff --git a/fluxer_api/src/api/user/controllers/UserAccountController.ts b/fluxer_api/src/api/user/controllers/UserAccountController.ts index 55738f0a4..5232b1d2f 100644 --- a/fluxer_api/src/api/user/controllers/UserAccountController.ts +++ b/fluxer_api/src/api/user/controllers/UserAccountController.ts @@ -224,7 +224,7 @@ export function UserAccountController(app: HonoApp) { const body = ctx.req.valid('json'); const result = await ctx .get('emailChangeService') - .requestNewEmail(user, body.ticket, body.new_email, body.original_proof); + .requestNewEmail(user, body.ticket, body.new_email, body.original_proof, body.new_password); return ctx.json(result); }, ); diff --git a/fluxer_api/src/api/user/services/EmailChangeService.ts b/fluxer_api/src/api/user/services/EmailChangeService.ts index 72c5021bf..4182c77df 100644 --- a/fluxer_api/src/api/user/services/EmailChangeService.ts +++ b/fluxer_api/src/api/user/services/EmailChangeService.ts @@ -13,6 +13,7 @@ import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidat import {ms} from 'itty-time'; import type {ApiContext} from '../../ApiContext'; import {EMAIL_CLEARABLE_SUSPICIOUS_ACTIVITY_FLAGS} from '../../auth/AuthEmail'; +import * as AuthPassword from '../../auth/AuthPassword'; import type {User} from '../../models/User'; import type {EmailChangeRepository} from '../repositories/auth/EmailChangeRepository'; @@ -158,6 +159,7 @@ export class EmailChangeService { ticket: string, newEmail: string, originalProof: string, + newPassword?: string, ): Promise { const {email, emailDnsValidation, users, rateLimit} = this.apiContext.services; const row = await getActiveChangeTicketForUser(this.repo, ticket, user.id); @@ -182,6 +184,9 @@ export class EmailChangeService { if (existing && existing.id !== user.id) { throw InputValidationError.fromCode('new_email', ValidationErrorCodes.EMAIL_ALREADY_IN_USE); } + if (newPassword != null && (await AuthPassword.isPasswordPwned(this.apiContext, newPassword))) { + throw InputValidationError.fromCode('new_password', ValidationErrorCodes.PASSWORD_IS_TOO_COMMON); + } assertChangeCooldown(row.new_code_sent_at, this.RESEND_COOLDOWN_MS); await checkChangeRateLimit(rateLimit, { identifier: `email_change:new:${user.id}`, diff --git a/fluxer_app/src/features/auth/components/modals/ClaimAccountModal.tsx b/fluxer_app/src/features/auth/components/modals/ClaimAccountModal.tsx index 0f5d5987e..05a4d489f 100644 --- a/fluxer_app/src/features/auth/components/modals/ClaimAccountModal.tsx +++ b/fluxer_app/src/features/auth/components/modals/ClaimAccountModal.tsx @@ -60,6 +60,7 @@ export const ClaimAccountModal = observer(() => { const [stage, setStage] = useState('collect'); const [ticket, setTicket] = useState(null); const [originalProof, setOriginalProof] = useState(null); + const [emailToken, setEmailToken] = useState(null); const [resendNewAt, setResendNewAt] = useState(null); const [submittingAction, setSubmittingAction] = useState(false); const [now, setNow] = useState(Date.now()); @@ -88,7 +89,7 @@ export const ClaimAccountModal = observer(() => { if (!activeProof) { throw new Error('Missing original proof token'); } - const result = await UserCommands.requestEmailChangeNew(activeTicket!, data.email, activeProof); + const result = await UserCommands.requestEmailChangeNew(activeTicket!, data.email, activeProof, data.newPassword); setResendNewAt(result.resend_available_at ? new Date(result.resend_available_at) : null); form.setValue('verificationCode', ''); form.clearErrors('verificationCode'); @@ -99,9 +100,14 @@ export const ClaimAccountModal = observer(() => { if (!ticket || !originalProof) return; setSubmittingAction(true); try { - const {email_token} = await UserCommands.verifyEmailChangeNew(ticket, data.verificationCode, originalProof); + let activeEmailToken = emailToken; + if (!activeEmailToken) { + const verified = await UserCommands.verifyEmailChangeNew(ticket, data.verificationCode, originalProof); + activeEmailToken = verified.email_token; + setEmailToken(activeEmailToken); + } await UserCommands.update({ - email_token, + email_token: activeEmailToken, new_password: data.newPassword, }); ToastCommands.createToast({type: 'success', children: i18n._(ACCOUNT_CLAIMED_SUCCESSFULLY_DESCRIPTOR)}); diff --git a/fluxer_app/src/features/user/commands/UserCommands.ts b/fluxer_app/src/features/user/commands/UserCommands.ts index fd2597180..7d53c9f8b 100644 --- a/fluxer_app/src/features/user/commands/UserCommands.ts +++ b/fluxer_app/src/features/user/commands/UserCommands.ts @@ -158,8 +158,14 @@ function requestNewEmailBody( ticket: string, newEmail: string, originalProof: string, -): {ticket: string; new_email: string; original_proof: string} { - return {ticket, new_email: newEmail, original_proof: originalProof}; + newPassword?: string, +): {ticket: string; new_email: string; original_proof: string; new_password?: string} { + return { + ticket, + new_email: newEmail, + original_proof: originalProof, + ...(newPassword != null ? {new_password: newPassword} : {}), + }; } function verifyNewEmailBody( @@ -365,11 +371,12 @@ export async function requestEmailChangeNew( ticket: string, newEmail: string, originalProof: string, + newPassword?: string, ): Promise { try { logger.debug('Requesting new email code'); const response = await http.post(Endpoints.USER_EMAIL_CHANGE_REQUEST_NEW, { - body: requestNewEmailBody(ticket, newEmail, originalProof), + body: requestNewEmailBody(ticket, newEmail, originalProof, newPassword), }); return response.body; } catch (error) { diff --git a/packages/schema/src/domains/user/UserRequestSchemas.ts b/packages/schema/src/domains/user/UserRequestSchemas.ts index 6887174e9..772e92ab4 100644 --- a/packages/schema/src/domains/user/UserRequestSchemas.ts +++ b/packages/schema/src/domains/user/UserRequestSchemas.ts @@ -136,6 +136,9 @@ export type EmailChangeVerifyOriginalRequest = z.infer;