From bf4ab24043e5fb51466528c65865bdf6fa69c4dd Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 9 Aug 2026 20:03:41 +0200 Subject: [PATCH] feat: Add register + hcaptcha --- README.md | 22 ++++++++++++++++------ nuxt.config.ts | 2 ++ package-lock.json | 7 +++++++ package.json | 1 + server/api/auth/forgot-password.post.ts | 7 +++++++ server/api/auth/login.post.ts | 2 +- server/api/auth/register.post.ts | 24 ++++++++++++++++++++++++ server/utils/hcaptcha.ts | 14 ++++++++++++++ shared/api-types.ts | 10 ++++++++++ 9 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 server/api/auth/register.post.ts create mode 100644 server/utils/hcaptcha.ts diff --git a/README.md b/README.md index c315848..44f3928 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,24 @@ If you'd like to help localize Pretendo Network, you can contribute to the trans # Website refactor The website is currently in a refactor, here is what is still left on the frontend: -- [ ] Term pages (nuxt content) -- [ ] Docs (nuxt content) - [ ] Styling for progress page - [ ] Styling for progress on main page -- [ ] Donation tier page -- [ ] Mii editor -- [ ] Account page +- [ ] Account: Mii editor +- [ ] Account: View account +- [ ] Account: Delete account +- [ ] Account: Link discord account +- [ ] Account: Edit server environment +- [ ] Donations: stripe checkout frontend +- [ ] Auth: Login +- [ ] Auth: Logout +- [ ] Auth: Register +- [ ] Auth: Forgot password flow +- [ ] Content: Term pages +- [ ] Content: Documentation pages +- [x] Content: Blog And the tasks left on the backend: -- [ ] Registration (with captchas) +- [x] Registration (with captchas) - [x] Logout - [x] Login - [x] Password forgot flow @@ -41,6 +49,8 @@ And the tasks left on the backend: - [x] Account editing (mii saving, server environment changes) - [x] Delete account - [x] Discourse SSO +- [x] HCaptcha support +- [x] RSS feed Miscellanous tasks: - [ ] Merge upstream changes into refactor branch diff --git a/nuxt.config.ts b/nuxt.config.ts index 09d7dfa..d091838 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -34,6 +34,8 @@ export default defineNuxtConfig({ githubApiToken: '', stripeSecretKey: '', stripeNotificationEmail: '', + hcaptchaSiteKey: '', + hcaptchaSecretKey: '', grpcHost: '', grpcApiKey: '', mongoConnectionString: '', diff --git a/package-lock.json b/package-lock.json index 1d8e364..87328d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "discord-api-types": "^0.38.53", "eslint": "^9.39.5", "feed": "^6.0.0", + "hcaptcha": "^0.2.0", "mongodb": "^7.5.0", "nice-grpc": "^2.1.17", "nodemailer": "^9.0.5", @@ -12651,6 +12652,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hcaptcha": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz", + "integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw==", + "license": "MIT" + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", diff --git a/package.json b/package.json index e7cace2..f3615e9 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "discord-api-types": "^0.38.53", "eslint": "^9.39.5", "feed": "^6.0.0", + "hcaptcha": "^0.2.0", "mongodb": "^7.5.0", "nice-grpc": "^2.1.17", "nodemailer": "^9.0.5", diff --git a/server/api/auth/forgot-password.post.ts b/server/api/auth/forgot-password.post.ts index 536ca73..797e7b0 100644 --- a/server/api/auth/forgot-password.post.ts +++ b/server/api/auth/forgot-password.post.ts @@ -1,9 +1,16 @@ +import { hcaptchaVerify } from "~~/server/utils/hcaptcha"; import { ForgotPasswordSchema } from "~~/shared/api-types"; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, ForgotPasswordSchema); const grpc = useApiGrpc(event); + const captchaResult = await hcaptchaVerify(event, body.captchaResponse); + if (!captchaResult) throw createError({ + status: 400, + message: 'Invalid captcha', + }); + await grpc.forgotPassword({ emailAddressOrUsername: body.emailOrPassword, }) diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index f024749..0bde814 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -4,7 +4,7 @@ import { useLegacyApiGrpc } from "~~/server/utils/useGrpc"; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, LoginSchema); - const grpc = useLegacyApiGrpc(event); + const grpc = useApiGrpc(event); try { const res = await grpc.login({ diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts new file mode 100644 index 0000000..43a490c --- /dev/null +++ b/server/api/auth/register.post.ts @@ -0,0 +1,24 @@ +import { ApiAuthLogin, RegisterSchema } from "#shared/api-types" + +export default defineEventHandler(async (event): Promise => { + const body = await readZodBody(event, RegisterSchema); + const grpc = useApiGrpc(event); + + try { + const res = await grpc.register({ + email: body.email, + miiName: body.miiName, + captchaResponse: body.captchaResponse, + username: body.username, + password: body.password, + passwordConfirm: body.password + }); + + return { + accessToken: res.accessToken, + refreshToken: res.refreshToken + }; + } catch (error: unknown) { + throw error; + } +}); diff --git a/server/utils/hcaptcha.ts b/server/utils/hcaptcha.ts new file mode 100644 index 0000000..39476b2 --- /dev/null +++ b/server/utils/hcaptcha.ts @@ -0,0 +1,14 @@ +import type { H3Event } from 'h3' +import hcaptcha from 'hcaptcha' + +export async function hcaptchaVerify(event: H3Event, captchaResponse: string | null | undefined): Promise { + const config = useRuntimeConfig(event); + if (!config.hcaptchaSiteKey) return true; // No captcha is configured, always valid + if (!config.hcaptchaSecretKey) throw new Error("Hcaptcha not configured correctly, missing secret key"); + + if (!captchaResponse) return false; // No captcha filled in, invalid + const captchaVerify = await hcaptcha.verify(config.hcaptchaSiteKey, captchaResponse, undefined, config.hcaptchaSiteKey); + + if (!captchaVerify.success) return false; // Invalid captcha response + return true; +} diff --git a/shared/api-types.ts b/shared/api-types.ts index aacff2b..9634862 100644 --- a/shared/api-types.ts +++ b/shared/api-types.ts @@ -64,6 +64,15 @@ export const LoginSchema = z.object({ }); export type ApiAuthLoginRequest = z.infer; +export const RegisterSchema = z.object({ + email: z.email(), + username: z.string(), + miiName: z.string(), + password: z.string(), + captchaResponse: z.string().optional() +}); +export type ApiAuthRegisterRequest = z.infer; + export const AccountUpdateSchema = z.object({ mii: z.object({ name: z.string(), @@ -83,6 +92,7 @@ export type ApiAuthResetPasswordRequest = z.infer; export const ForgotPasswordSchema = z.object({ emailOrPassword: z.string(), + captchaResponse: z.string().optional() }); export type ApiAuthForgotPasswordRequest = z.infer;