feat: Add register + hcaptcha

This commit is contained in:
mrjvs
2026-08-09 20:03:41 +02:00
parent f73a02fb57
commit bf4ab24043
9 changed files with 82 additions and 7 deletions

View File

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

View File

@@ -34,6 +34,8 @@ export default defineNuxtConfig({
githubApiToken: '',
stripeSecretKey: '',
stripeNotificationEmail: '',
hcaptchaSiteKey: '',
hcaptchaSecretKey: '',
grpcHost: '',
grpcApiKey: '',
mongoConnectionString: '',

7
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -1,9 +1,16 @@
import { hcaptchaVerify } from "~~/server/utils/hcaptcha";
import { ForgotPasswordSchema } from "~~/shared/api-types";
export default defineEventHandler(async (event): Promise<void> => {
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,
})

View File

@@ -4,7 +4,7 @@ import { useLegacyApiGrpc } from "~~/server/utils/useGrpc";
export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
const body = await readZodBody(event, LoginSchema);
const grpc = useLegacyApiGrpc(event);
const grpc = useApiGrpc(event);
try {
const res = await grpc.login({

View File

@@ -0,0 +1,24 @@
import { ApiAuthLogin, RegisterSchema } from "#shared/api-types"
export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
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;
}
});

14
server/utils/hcaptcha.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { H3Event } from 'h3'
import hcaptcha from 'hcaptcha'
export async function hcaptchaVerify(event: H3Event, captchaResponse: string | null | undefined): Promise<boolean> {
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;
}

View File

@@ -64,6 +64,15 @@ export const LoginSchema = z.object({
});
export type ApiAuthLoginRequest = z.infer<typeof LoginSchema>;
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<typeof RegisterSchema>;
export const AccountUpdateSchema = z.object({
mii: z.object({
name: z.string(),
@@ -83,6 +92,7 @@ export type ApiAuthResetPasswordRequest = z.infer<typeof ResetPasswordSchema>;
export const ForgotPasswordSchema = z.object({
emailOrPassword: z.string(),
captchaResponse: z.string().optional()
});
export type ApiAuthForgotPasswordRequest = z.infer<typeof ForgotPasswordSchema>;