From 1b60b3517d62ccc2eb0c79577036f11f18e1aa2c Mon Sep 17 00:00:00 2001 From: Tony Yang Date: Tue, 15 Apr 2025 14:46:16 +0800 Subject: [PATCH] feat: hcaptcha --- .env | 3 +- functions/api/avatars/index.js | 99 +++++++++++--------- functions/api/login.js | 100 +++++++++++++-------- functions/api/messages.js | 66 ++++++++------ functions/api/motto.js | 52 ++++++----- functions/api/register.js | 110 ++++++++++++----------- index.html | 2 +- package-lock.json | 20 +++++ package.json | 2 + src/components/Board/BoardForm.vue | 23 ++++- src/components/Login/LoginForm.vue | 23 +++-- src/components/Profile/ProfileForm.vue | 24 ++++- src/components/Register/RegisterForm.vue | 21 ++++- src/lib/api.js | 90 +++++++++---------- src/main.js | 3 + src/style.css | 7 ++ src/views/BoardView.vue | 2 +- src/views/DailyMottoView.vue | 22 ++++- src/views/LoginView.vue | 4 +- src/views/RegisterView.vue | 4 +- 20 files changed, 430 insertions(+), 247 deletions(-) diff --git a/.env b/.env index af5fec9..245a102 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_R2_BASE_URL=https://pub-e115c4e749734702abd09206cba74257.r2.dev/ \ No newline at end of file +VITE_R2_BASE_URL=https://pub-e115c4e749734702abd09206cba74257.r2.dev/ +VITE_HCAPTCHA_SITEKEY=a7340f48-b55e-4c56-8d96-2e70ce3423e0 diff --git a/functions/api/avatars/index.js b/functions/api/avatars/index.js index 58e7401..8552635 100644 --- a/functions/api/avatars/index.js +++ b/functions/api/avatars/index.js @@ -1,50 +1,63 @@ import { verifyJWT } from '../../middleware/auth'; import { createErrorResponse, createSuccessResponse } from '../../utils'; import { fileTypeFromBuffer } from 'file-type'; +import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; -export async function onRequestPut(context) { - try { +export const onRequestPut = [ + async (context) => { + return hCaptchaPlugin({ + secret: context.env.hcaptcha_secret_key, + sitekey: context.env.hcaptcha_site_key, + onError: (context) => { + console.error("hCaptcha error:", context.error); + return createErrorResponse("hCaptcha verification failed", 403); + } + })(context); + }, + async (context) => { const { request, env } = context; - // Verify the JWT token - const authResult = await verifyJWT(context); - if (authResult) { - return authResult; // Return the error response from the middleware + try { + // Verify the JWT token + const authResult = await verifyJWT(context); + if (authResult) { + return authResult; // Return the error response from the middleware + } + + const formData = await request.formData(); + const avatar = formData.get('avatar'); + + if (!avatar) { + return createErrorResponse("Missing avatar", 400); + } + + if (avatar.size > 1 * 1024 * 1024) { + // Entity too large + return createErrorResponse("Avatar must be less than 1MB", 413); + } + + const buffer = await avatar.arrayBuffer(); + const fileTypeResult = await fileTypeFromBuffer(buffer); + + if (!fileTypeResult) { + return createErrorResponse("Unsupported file type", 400); + } + + if (fileTypeResult.mime !== 'image/jpeg' && fileTypeResult.mime !== 'image/png') { + return createErrorResponse("Avatar must be a JPG or PNG image", 400); + } + + // Upload the avatar to R2 + const objectName = `avatars/${context.user.userId}`; + await env.MY_BUCKET.put(objectName, buffer); + + // Store the filename in D1 + await env.DB.prepare("UPDATE users SET avatar = ? WHERE id = ?").bind(objectName, context.user.userId).run(); + + return createSuccessResponse({ message: "Avatar uploaded successfully" }); + } catch (error) { + console.error("Avatar upload error:", error); + return createErrorResponse("Avatar upload failed", 500); } - - const formData = await request.formData(); - const avatar = formData.get('avatar'); - - if (!avatar) { - return createErrorResponse("Missing avatar", 400); - } - - if (avatar.size > 1 * 1024 * 1024) { - // Entity too large - return createErrorResponse("Avatar must be less than 1MB", 413); - } - - const buffer = await avatar.arrayBuffer(); - const fileTypeResult = await fileTypeFromBuffer(buffer); - - if (!fileTypeResult) { - return createErrorResponse("Unsupported file type", 400); - } - - if (fileTypeResult.mime !== 'image/jpeg' && fileTypeResult.mime !== 'image/png') { - return createErrorResponse("Avatar must be a JPG or PNG image", 400); - } - - // Upload the avatar to R2 - const objectName = `avatars/${context.user.userId}`; - await env.MY_BUCKET.put(objectName, buffer); - - // Store the filename in D1 - await env.DB.prepare("UPDATE users SET avatar = ? WHERE id = ?").bind(objectName, context.user.userId).run(); - - return createSuccessResponse({ message: "Avatar uploaded successfully" }); - } catch (error) { - console.error("Avatar upload error:", error); - return createErrorResponse("Avatar upload failed", 500); - } -} + }, +]; diff --git a/functions/api/login.js b/functions/api/login.js index fad8420..f8b43de 100644 --- a/functions/api/login.js +++ b/functions/api/login.js @@ -1,58 +1,80 @@ import { SignJWT } from 'jose'; import { createSuccessResponse, createErrorResponse } from "../utils"; +import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; -export async function onRequestPost(context) { - try { - const { request, env } = context; +export const onRequestPost = [ + async (context) => { + return hCaptchaPlugin({ + secret: context.env.hcaptcha_secret_key, + sitekey: context.env.hcaptcha_site_key, + onError: (context) => { + console.error("hCaptcha error:", context.error); + return createErrorResponse("hCaptcha verification failed", 403); + } + })(context); + }, + async (context) => { + try { + const { request, env } = context; + let payload; - const { username, password } = await request.json(); + try { + const formData = await request.formData(); + payload = JSON.parse(formData.get('payload')); + } catch (e) { + console.error("Payload parsing error:", e); + return createErrorResponse("Invalid payload", 400); + } - if (!username || !password) { - return createErrorResponse("Missing username or password", 400); - } + const { username, password } = payload; - if (username.length < 3) { - return createErrorResponse("Username must be at least 3 characters", 400); - } + if (!username || !password) { + return createErrorResponse("Missing username or password", 400); + } - if (password.length < 8) { - return createErrorResponse("Password must be at least 8 characters", 400); - } + if (username.length < 3) { + return createErrorResponse("Username must be at least 3 characters", 400); + } - if (!/^[a-zA-Z0-9]+$/.test(username)) { - return createErrorResponse("Username must be alphanumeric", 400); - } + if (password.length < 8) { + return createErrorResponse("Password must be at least 8 characters", 400); + } - // Get the stored password from D1 - const { results } = await env.DB.prepare("SELECT password FROM users WHERE username = ?").bind(username).all(); + if (!/^[a-zA-Z0-9]+$/.test(username)) { + return createErrorResponse("Username must be alphanumeric", 400); + } - if (!results || results.length === 0) { - return new Response(JSON.stringify({"error": "Invalid username or password"}), { status: 403, headers: { 'Content-Type': 'application/json' } }); - } + // Get the stored password from D1 + const { results } = await env.DB.prepare("SELECT password FROM users WHERE username = ?").bind(username).all(); - const storedPassword = results[0].password; + if (!results || results.length === 0) { + return new Response(JSON.stringify({"error": "Invalid username or password"}), { status: 403, headers: { 'Content-Type': 'application/json' } }); + } - // Compare the password to the stored password - if (password !== storedPassword) { - return new Response(JSON.stringify({"error": "Invalid username or password"}), { status: 403, headers: { 'Content-Type': 'application/json' } }); - } + const storedPassword = results[0].password; - // Get the user ID - const { results: userResults } = await env.DB.prepare("SELECT * FROM users WHERE username = ?").bind(username).all(); - const jwtPayload = (({ id, username }) => ({ id, username }))(userResults[0]); + // Compare the password to the stored password + if (password !== storedPassword) { + return new Response(JSON.stringify({"error": "Invalid username or password"}), { status: 403, headers: { 'Content-Type': 'application/json' } }); + } - // Generate a JWT token - const jwt = await new SignJWT(jwtPayload) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setIssuer('urn:example:issuer') - .setAudience('urn:example:audience') - .setExpirationTime('2h') - .sign(new TextEncoder().encode(env.JWT_SECRET)); + // Get the user ID + const { results: userResults } = await env.DB.prepare("SELECT * FROM users WHERE username = ?").bind(username).all(); + const jwtPayload = (({ id, username }) => ({ id, username }))(userResults[0]); - return createSuccessResponse({ jwt }); + // Generate a JWT token + const jwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setIssuer('urn:example:issuer') + .setAudience('urn:example:audience') + .setExpirationTime('2h') + .sign(new TextEncoder().encode(env.JWT_SECRET)); + + return createSuccessResponse({ jwt }); } catch (error) { console.error("Login error:", error); return createErrorResponse("Login failed", 500); } -} + }, +]; diff --git a/functions/api/messages.js b/functions/api/messages.js index ab349b9..14aa798 100644 --- a/functions/api/messages.js +++ b/functions/api/messages.js @@ -1,5 +1,6 @@ import { verifyJWT } from '../middleware/auth'; import { createErrorResponse, createSuccessResponse } from '../utils'; +import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; export async function onRequestGet(context) { try { @@ -15,42 +16,57 @@ export async function onRequestGet(context) { } } -export async function onRequestPost(context) { - try { - const { request, env } = context; +export const onRequestPost = [ + async (context) => { + return hCaptchaPlugin({ + secret: context.env.hcaptcha_secret_key, + sitekey: context.env.hcaptcha_site_key, + onError: (context) => { + console.error("hCaptcha error:", context.error); + return createErrorResponse("hCaptcha verification failed", 403); + } + })(context); + }, + async (context) => { + try { + const { request, env } = context; + let payload; - // Verify the JWT token - const authResult = await verifyJWT(context); - if (authResult) { - return authResult; // Return the error response from the middleware - } + try { + const formData = await request.formData(); + payload = JSON.parse(formData.get('payload')); + } catch (e) { + console.error("Payload parsing error:", e); + return createErrorResponse("Invalid payload", 400); + } - const { message } = await request.json(); + const { message } = payload; - if (!message) { - return createErrorResponse("Empty message", 400); - } + if (!message) { + return createErrorResponse("Empty message", 400); + } - if (message.length > 200) { - return createErrorResponse("Message too long", 400); - } + if (message.length > 200) { + return createErrorResponse("Message too long", 400); + } - // Generate a unique ID for the message - const messageId = crypto.randomUUID(); + // Generate a unique ID for the message + const messageId = crypto.randomUUID(); - // Store the message in D1 - await env.DB.prepare("INSERT INTO messages (id, userId, message) VALUES (?, ?, ?)") - .bind(messageId, context.user.userId, message) - .run(); + // Store the message in D1 + await env.DB.prepare("INSERT INTO messages (id, userId, message) VALUES (?, ?, ?)") + .bind(messageId, context.user.userId, message) + .run(); - return new Response(JSON.stringify({ id: messageId, username: context.user.username, message }), { - headers: { 'Content-Type': 'application/json' }, - }); + return new Response(JSON.stringify({ id: messageId, username: context.user.username, message }), { + headers: { 'Content-Type': 'application/json' }, + }); } catch (error) { console.error("Message posting error:", error); return createErrorResponse("Message posting failed", 500); } -} + }, +]; export async function onRequestDelete(context) { try { diff --git a/functions/api/motto.js b/functions/api/motto.js index e4a4bd9..5df5835 100644 --- a/functions/api/motto.js +++ b/functions/api/motto.js @@ -1,25 +1,35 @@ import { verifyJWT } from '../middleware/auth'; import { createErrorResponse, createSuccessResponse } from '../utils'; +import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; -export async function onRequestGet(context) { - try { - // Verify the JWT token - const authResult = await verifyJWT(context); - if (authResult) { - return authResult; // Return the error response from the middleware +export const onRequestPost = [ + async (context) => { + return hCaptchaPlugin({ + secret: context.env.hcaptcha_secret_key, + sitekey: context.env.hcaptcha_site_key, + onError: (context) => { + console.error("hCaptcha error:", context.error); + return createErrorResponse("hCaptcha verification failed", 403); + } + })(context); + }, + async (context) => { + try { + // Verify the JWT token + const authResult = await verifyJWT(context); + if (authResult) { + return authResult; // Return the error response from the middleware + } + + const input = { prompt: '用繁體中文生成一句名言佳句' }; + + const response = await context.env.AI.run('@cf/meta/llama-3.2-1b-instruct', input); + const motto = response.response; + + return createSuccessResponse({ motto }); + } catch (error) { + console.error("Gen motto error:", error); + return createErrorResponse("Get motto failed", 500); } - - // Use Cloudflare AI Gateway to proxy the request to Google AI Studio - const ai = context.env.AI; - - const input = { prompt: '用繁體中文生成一句名言佳句' }; - - const response = await ai.run('@cf/meta/llama-3.2-1b-instruct', input); - const motto = response.response; - - return createSuccessResponse({ motto }); - } catch (error) { - console.error("Gen motto error:", error); - return createErrorResponse("Get motto failed", 500); - } -} + }, +]; diff --git a/functions/api/register.js b/functions/api/register.js index dd14ac2..fd97646 100644 --- a/functions/api/register.js +++ b/functions/api/register.js @@ -1,52 +1,62 @@ -import { createErrorResponse } from '../utils'; +import { createErrorResponse, createSuccessResponse } from '../utils'; +import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; -export async function onRequestPost(context) { - try { - const { request, env } = context; - - const { username, password } = await request.json(); - - if (!username || !password) { - return createErrorResponse("Missing username or password", 400); - } - - if (username.length < 3) { - return createErrorResponse("Username must be at least 3 characters", 400); - } - - if (password.length < 8) { - return createErrorResponse("Password must be at least 8 characters", 400); - } - - if (!/^[a-zA-Z0-9]+$/.test(username)) { - return createErrorResponse("Username must be alphanumeric", 400); - } - - // Check if the username already exists - const { results: existingUsers } = await env.DB.prepare("SELECT id FROM users WHERE username = ?").bind(username).all(); - if (existingUsers.length > 0) { - return createErrorResponse("Username already exists", 400); - } - - // Store the username and password in D1 - await env.DB.prepare("INSERT INTO users (username, password, avatar) VALUES (?, ?, ?)").bind(username, password, "avatars/default.png").run(); - - // Get the user ID - const { results } = await env.DB.prepare("SELECT id FROM users WHERE username = ?").bind(username).all(); - const userId = results[0].id; - - // Registration successful, return success response - return new Response( - JSON.stringify({ - success: true, - message: "Registration successful. Please login.", - }), - { - headers: { "Content-Type": "application/json" }, +export const onRequestPost = [ + async (context) => { + return hCaptchaPlugin({ + secret: context.env.hcaptcha_secret_key, + sitekey: context.env.hcaptcha_site_key, + onError: (context) => { + console.error("hCaptcha error:", context.error); + return createErrorResponse("hCaptcha verification failed", 403); } - ); - } catch (error) { - console.error("Registration error:", error); - return createErrorResponse("Server Error", 500); - } -} + })(context); + }, + async (context) => { + try { + const { request, env } = context; + let payload; + + try { + const formData = await request.formData(); + payload = JSON.parse(formData.get('payload')); + } catch (e) { + console.error("Payload parsing error:", e); + return createErrorResponse("Invalid payload", 400); + } + + const { username, password } = payload; + + if (!username || !password) { + return createErrorResponse("Missing username or password", 400); + } + + if (username.length < 3) { + return createErrorResponse("Username must be at least 3 characters", 400); + } + + if (password.length < 8) { + return createErrorResponse("Password must be at least 8 characters", 400); + } + + if (!/^[a-zA-Z0-9]+$/.test(username)) { + return createErrorResponse("Username must be alphanumeric", 400); + } + + // Check if the username already exists + const { results: existingUsers } = await env.DB.prepare("SELECT id FROM users WHERE username = ?").bind(username).all(); + if (existingUsers.length > 0) { + return createErrorResponse("Username already exists", 400); + } + + // Store the username and password in D1 + await env.DB.prepare("INSERT INTO users (username, password, avatar) VALUES (?, ?, ?)").bind(username, password, "avatars/default.png").run(); + + // Registration successful, return success response + return createSuccessResponse("Registration successful", 201); + } catch (error) { + console.error("Registration error:", error); + return createErrorResponse("Server Error", 500); + } + }, +]; diff --git a/index.html b/index.html index 3fbc713..a33c0ac 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Practicum of Attacking and Defense of Network Security diff --git a/package-lock.json b/package-lock.json index 7663810..3560a09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "vue", "version": "0.0.0", "dependencies": { + "@cloudflare/pages-plugin-hcaptcha": "^1.0.4", + "@hcaptcha/vue3-hcaptcha": "^1.3.0", "file-type": "^20.4.1", "jose": "^6.0.10", "pinia": "^3.0.2", @@ -67,6 +69,12 @@ "node": ">=6.9.0" } }, + "node_modules/@cloudflare/pages-plugin-hcaptcha": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cloudflare/pages-plugin-hcaptcha/-/pages-plugin-hcaptcha-1.0.4.tgz", + "integrity": "sha512-LZ1kWAhj3/wdAAnJs/fUG8akC+FKizLu2AdhVzr4aExncXA2wjXvphktG40pvEMIoMXH5LBqP9H7YNguR14Y7Q==", + "license": "MIT" + }, "node_modules/@cloudflare/workers-types": { "version": "4.20250412.0", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250412.0.tgz", @@ -499,6 +507,18 @@ "node": ">=18" } }, + "node_modules/@hcaptcha/vue3-hcaptcha": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz", + "integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==", + "license": "MIT", + "dependencies": { + "vue": "^3.2.19" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", diff --git a/package.json b/package.json index 4f9c7dc..f023306 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "deploy": "npm run build && npm run pages:deploy" }, "dependencies": { + "@cloudflare/pages-plugin-hcaptcha": "^1.0.4", + "@hcaptcha/vue3-hcaptcha": "^1.3.0", "file-type": "^20.4.1", "jose": "^6.0.10", "pinia": "^3.0.2", diff --git a/src/components/Board/BoardForm.vue b/src/components/Board/BoardForm.vue index 36d179d..13d40a3 100644 --- a/src/components/Board/BoardForm.vue +++ b/src/components/Board/BoardForm.vue @@ -1,22 +1,32 @@ @@ -39,8 +49,13 @@ function submit() { +
- +
@@ -56,4 +71,4 @@ function submit() { max-width: 100%; } } - \ No newline at end of file + diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 7778651..8fb70dd 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -1,16 +1,24 @@ @@ -81,9 +89,14 @@ const submit = () => { +
diff --git a/src/components/Profile/ProfileForm.vue b/src/components/Profile/ProfileForm.vue index 084c305..4faba95 100644 --- a/src/components/Profile/ProfileForm.vue +++ b/src/components/Profile/ProfileForm.vue @@ -2,6 +2,7 @@ import { ref, computed, defineProps } from 'vue'; import { useRouter } from 'vue-router'; import { uploadAvatar } from '../../lib/api'; +import VueHcaptcha from '@hcaptcha/vue3-hcaptcha'; import { useAuthStore } from '../../stores/auth'; const props = defineProps({ @@ -16,11 +17,20 @@ const router = useRouter(); const avatarFile = ref(null); const avatarError = ref(''); +const hcaptchaResponse = ref(''); const onFileChange = (event) => { avatarFile.value = event.target.files[0]; }; +const handleHcaptchaVerify = (token) => { + hcaptchaResponse.value = token; +}; + +const handleHcaptchaExpired = () => { + hcaptchaResponse.value = ''; +}; + const onSubmit = async () => { avatarError.value = ''; @@ -29,6 +39,11 @@ const onSubmit = async () => { return; } + if (!hcaptchaResponse.value) { + avatarError.value = '請完成驗證。'; + return; + } + if (avatarFile.value.size > 1 * 1024 * 1024) { avatarError.value = '頭貼檔案需小於 1MB。'; return; @@ -40,7 +55,7 @@ const onSubmit = async () => { } try { - await uploadAvatar(avatarFile.value, authStore.jwt); + await uploadAvatar(avatarFile.value, authStore.jwt, hcaptchaResponse.value); alert('Avatar uploaded successfully!'); // After successful upload, reload this page router.go(0); @@ -85,8 +100,13 @@ const avatarUrl = computed(() => { +
- +
diff --git a/src/components/Register/RegisterForm.vue b/src/components/Register/RegisterForm.vue index 2bed132..e469ec1 100644 --- a/src/components/Register/RegisterForm.vue +++ b/src/components/Register/RegisterForm.vue @@ -1,14 +1,24 @@ @@ -81,8 +91,13 @@ const submit = () => { +
- +
diff --git a/src/lib/api.js b/src/lib/api.js index 522e715..1b1ec15 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -2,14 +2,18 @@ import { unauthRedirectToLogin } from '../router'; const API_BASE_URL = '/api'; +export async function register(username, password, hCaptchaResponse) { + const formData = new FormData(); + const payload = { username, password }; + + formData.append('payload', JSON.stringify(payload)); + if (hCaptchaResponse) { + formData.append('h-captcha-response', hCaptchaResponse); + } -export async function register(username, password) { const response = await fetch(API_BASE_URL + "/register", { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ username, password }), + body: formData, }); if (!response.ok) { @@ -20,13 +24,18 @@ export async function register(username, password) { return response.json(); } -export async function login(username, password) { +export async function login(username, password, hCaptchaResponse) { + const formData = new FormData(); + const payload = { username, password }; + + formData.append('payload', JSON.stringify(payload)); + if (hCaptchaResponse) { + formData.append('h-captcha-response', hCaptchaResponse); + } + const response = await fetch(API_BASE_URL + "/login", { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ username, password }), + body: formData }); if (!response.ok) { @@ -37,14 +46,21 @@ export async function login(username, password) { return response.json(); } -export async function postMessage(message, jwt) { +export async function postMessage(message, jwt, hCaptchaResponse) { + const formData = new FormData(); + const payload = { message }; + + formData.append('payload', JSON.stringify(payload)); + if (hCaptchaResponse) { + formData.append('h-captcha-response', hCaptchaResponse); + } + const response = await fetch(API_BASE_URL + "/messages", { method: 'POST', headers: { - 'Content-Type': 'application/json', - 'Authorization': "Bearer " + jwt, + 'Authorization': "Bearer " + jwt }, - body: JSON.stringify({ message }), + body: formData }); if (!response.ok) { @@ -81,7 +97,7 @@ export async function deleteMessage(messageId, jwt) { return response.json(); } -export async function getMessages(jwt) { +export async function getMessages() { const response = await fetch(API_BASE_URL + "/messages", { method: 'GET', headers: { @@ -122,43 +138,20 @@ export async function getProfile(jwt) { return response.json(); } -export async function me(jwt) { - const response = await fetch(API_BASE_URL + "/me", { - method: 'GET', - headers: { - 'Authorization': "Bearer " + jwt, - }, - }); +export async function uploadAvatar(avatar, jwt, hCaptchaResponse) { + const formData = new FormData(); + formData.append('avatar', avatar); - if (!response.ok) { - if (response.status === 401) { - unauthRedirectToLogin(); - return; - } - const error = await response.json(); - throw new Error(error.error || 'User operation failed'); + if (hCaptchaResponse) { + formData.append('h-captcha-response', hCaptchaResponse); } - const userData = await response.json(); - const avatarFilename = userData.avatar; - - if (avatarFilename) { - return import.meta.env.VITE_R2_BASE_URL + "/" + avatarFilename; - } - - return null; -} - -export async function uploadAvatar(avatar, jwt) { - const body = new FormData(); - body.append('avatar', avatar); - const response = await fetch(API_BASE_URL + "/avatars", { method: 'PUT', headers: { 'Authorization': "Bearer " + jwt, }, - body: body, + body: formData, }); if (!response.ok) { @@ -174,12 +167,19 @@ export async function uploadAvatar(avatar, jwt) { return data; } -export async function generateMotto(jwt) { +export async function generateMotto(jwt, hCaptchaResponse) { + const formData = new FormData(); + if (hCaptchaResponse) { + formData.append('h-captcha-response', hCaptchaResponse); + } + try { const response = await fetch('/api/motto', { + method: 'POST', headers: { 'Authorization': `Bearer ${jwt}`, }, + body: formData }); if (!response.ok) { diff --git a/src/main.js b/src/main.js index 43a691e..0c1ba9e 100644 --- a/src/main.js +++ b/src/main.js @@ -9,4 +9,7 @@ const app = createApp(App); app.use(pinia); app.use(router); + +app.config.globalProperties.$hcaptchaSitekey = import.meta.env.VITE_HCAPTCHA_SITEKEY; + app.mount('#app'); diff --git a/src/style.css b/src/style.css index fb2876b..02f4e0c 100644 --- a/src/style.css +++ b/src/style.css @@ -5,4 +5,11 @@ html, body, #app { #app { display: flex; flex-direction: column; +} + +#hcap-script { + display: flex; + align-items: center; + justify-content: center; + padding: 1em 0; } \ No newline at end of file diff --git a/src/views/BoardView.vue b/src/views/BoardView.vue index f6edbef..5b4e255 100644 --- a/src/views/BoardView.vue +++ b/src/views/BoardView.vue @@ -23,7 +23,7 @@ const onSubmit = async (message) => { if (!isLoggedIn) return; try { - await postMessage(message.text, authStore.jwt); + await postMessage(message.text, authStore.jwt, message.hcaptchaResponse); const response = await getMessages(); messages.value = response.messages; diff --git a/src/views/DailyMottoView.vue b/src/views/DailyMottoView.vue index ece3768..71986c1 100644 --- a/src/views/DailyMottoView.vue +++ b/src/views/DailyMottoView.vue @@ -2,17 +2,27 @@ import { ref, onMounted } from 'vue'; import { generateMotto as generateMottoApi } from '../lib/api'; import { useAuthStore } from '../stores/auth'; +import VueHcaptcha from '@hcaptcha/vue3-hcaptcha'; const motto = ref(''); const mottoLoading = ref(false); +const hcaptchaResponse = ref(''); const authStore = useAuthStore(); +const handleHcaptchaVerify = (token) => { + hcaptchaResponse.value = token; +}; + +const handleHcaptchaExpired = () => { + hcaptchaResponse.value = ''; +}; + const generateMotto = async () => { mottoLoading.value = true; try { - const generatedMotto = await generateMottoApi(authStore.jwt); + const generatedMotto = await generateMottoApi(authStore.jwt, hcaptchaResponse.value); motto.value = generatedMotto; } catch (error) { console.error(error); @@ -28,9 +38,15 @@ const generateMotto = async () => {
每日金句生成器

Powered By Cloudflare Workers AI

- + +

每日金句:

+

{{ motto }}

- \ No newline at end of file + diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index c57c28b..cf67d2e 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -7,9 +7,9 @@ import { login } from '../lib/api'; const router = useRouter(); const authStore = useAuthStore(); -const onSubmit = async ({ username, password }) => { +const onSubmit = async ({ username, password, hcaptchaResponse }) => { try { - const response = await login(username, password); + const response = await login(username, password, hcaptchaResponse); const { jwt } = response; authStore.setJwt(jwt); alert('Login successful!'); diff --git a/src/views/RegisterView.vue b/src/views/RegisterView.vue index 7d2979d..ebbe1b1 100644 --- a/src/views/RegisterView.vue +++ b/src/views/RegisterView.vue @@ -5,9 +5,9 @@ import { register } from '../lib/api'; const router = useRouter(); -const handleNewUser = async ({ username, password }) => { +const handleNewUser = async ({ username, password, hcaptchaResponse }) => { try { - const response = await register(username, password); + const response = await register(username, password, hcaptchaResponse); alert(response.message || 'Registration successful! Please log in.'); // Redirect to login page