| 
 | 1 | +import crypto from 'node:crypto'  | 
1 | 2 | import { getFormProps, getInputProps, useForm } from '@conform-to/react'  | 
2 | 3 | import { getZodConstraint, parseWithZod } from '@conform-to/zod'  | 
3 | 4 | import { type SEOHandle } from '@nasa-gcn/remix-seo'  | 
@@ -41,8 +42,29 @@ export async function loader({ request }: Route.LoaderArgs) {  | 
41 | 42 | export async function action({ request }: Route.ActionArgs) {  | 
42 | 43 | 	const resetPasswordUsername = await requireResetPasswordUsername(request)  | 
43 | 44 | 	const formData = await request.formData()  | 
44 |  | -	const submission = parseWithZod(formData, {  | 
45 |  | -		schema: ResetPasswordSchema,  | 
 | 45 | +	const submission = await parseWithZod(formData, {  | 
 | 46 | +		schema: ResetPasswordSchema.superRefine(async ({ password }, ctx) => {  | 
 | 47 | +			const hash = crypto  | 
 | 48 | +				.createHash('sha1')  | 
 | 49 | +				.update(password, 'utf8')  | 
 | 50 | +				.digest('hex')  | 
 | 51 | +				.toUpperCase()  | 
 | 52 | +			const [prefix, suffix] = [hash.slice(0, 5), hash.slice(5)]  | 
 | 53 | +			const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`)  | 
 | 54 | +			if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)  | 
 | 55 | +			const data = await res.text()  | 
 | 56 | +			const matches = data  | 
 | 57 | +				.split('/\r?\n/')  | 
 | 58 | +				.filter((line) => line.includes(suffix))  | 
 | 59 | +			if (matches.length) {  | 
 | 60 | +				ctx.addIssue({  | 
 | 61 | +					path: ['password'],  | 
 | 62 | +					code: 'custom',  | 
 | 63 | +					message: 'Password is too common',  | 
 | 64 | +				})  | 
 | 65 | +			}  | 
 | 66 | +		}),  | 
 | 67 | +		async: true,  | 
46 | 68 | 	})  | 
47 | 69 | 	if (submission.status !== 'success') {  | 
48 | 70 | 		return data(  | 
 | 
0 commit comments