diff --git a/.env b/.env index 245a102..820e639 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ VITE_R2_BASE_URL=https://pub-e115c4e749734702abd09206cba74257.r2.dev/ VITE_HCAPTCHA_SITEKEY=a7340f48-b55e-4c56-8d96-2e70ce3423e0 +VITE_RECAPTCHA_SITEKEY=6LdTSxkrAAAAAOWt1LWFd9HYt8IRXyT0PaJXouC3 +VITE_TURNSTILE_SITEKEY=0x4AAAAAABL64iQLO7IcpeAL \ No newline at end of file diff --git a/functions/api/avatars/index.js b/functions/api/avatars/index.js index 8552635..95d0244 100644 --- a/functions/api/avatars/index.js +++ b/functions/api/avatars/index.js @@ -1,19 +1,10 @@ import { verifyJWT } from '../../middleware/auth'; +import { captchaPlugins } from '../../middleware/captcha'; import { createErrorResponse, createSuccessResponse } from '../../utils'; import { fileTypeFromBuffer } from 'file-type'; -import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; 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); - }, + ...captchaPlugins, async (context) => { const { request, env } = context; diff --git a/functions/api/login.js b/functions/api/login.js index f8b43de..32187b8 100644 --- a/functions/api/login.js +++ b/functions/api/login.js @@ -1,18 +1,9 @@ import { SignJWT } from 'jose'; import { createSuccessResponse, createErrorResponse } from "../utils"; -import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; +import { captchaPlugins } from '../middleware/captcha'; 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); - }, + ...captchaPlugins, async (context) => { try { const { request, env } = context; diff --git a/functions/api/messages.js b/functions/api/messages.js index 14aa798..451d25c 100644 --- a/functions/api/messages.js +++ b/functions/api/messages.js @@ -1,6 +1,6 @@ import { verifyJWT } from '../middleware/auth'; +import { captchaPlugins } from '../middleware/captcha'; import { createErrorResponse, createSuccessResponse } from '../utils'; -import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; export async function onRequestGet(context) { try { @@ -17,16 +17,7 @@ export async function onRequestGet(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); - }, + ...captchaPlugins, async (context) => { try { const { request, env } = context; diff --git a/functions/api/motto.js b/functions/api/motto.js index 5df5835..18a062d 100644 --- a/functions/api/motto.js +++ b/functions/api/motto.js @@ -1,18 +1,9 @@ import { verifyJWT } from '../middleware/auth'; import { createErrorResponse, createSuccessResponse } from '../utils'; -import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; +import { captchaPlugins } from '../middleware/captcha'; 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); - }, + ...captchaPlugins, async (context) => { try { // Verify the JWT token diff --git a/functions/api/register.js b/functions/api/register.js index fd97646..65aa44e 100644 --- a/functions/api/register.js +++ b/functions/api/register.js @@ -1,17 +1,8 @@ +import { captchaPlugins } from '../middleware/captcha'; import { createErrorResponse, createSuccessResponse } from '../utils'; -import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; 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); - }, + ...captchaPlugins, async (context) => { try { const { request, env } = context; diff --git a/functions/middleware/captcha.js b/functions/middleware/captcha.js new file mode 100644 index 0000000..4f86702 --- /dev/null +++ b/functions/middleware/captcha.js @@ -0,0 +1,58 @@ +import { createErrorResponse } from '../utils'; + +import hCaptchaPlugin from "@cloudflare/pages-plugin-hcaptcha"; +import turnstilePlugin from "@cloudflare/pages-plugin-turnstile"; + +export const captchaPlugins = [ + async (context) => { + try { + 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); + } catch (e) { + console.error("hCaptcha error:", e); + return createErrorResponse("hCaptcha verification failed", 400); + } + }, + async (context) => { + try { + const recaptchaResponse = (await context.request.clone().formData()).get("g-recaptcha-response").toString(); + const formData = new FormData(); + formData.append("secret", context.env.recaptcha_secret_key); + formData.append("response", recaptchaResponse); + + const response = await fetch("https://www.google.com/recaptcha/api/siteverify", { + method: "POST", + body: formData + }); + const data = await response.json(); + if (!data.success) { + console.error("reCAPTCHA error:", data); + return createErrorResponse("reCAPTCHA verification failed", 403); + } + } catch (e) { + console.error("reCAPTCHA error:", e); + return createErrorResponse("reCAPTCHA verification failed", 400); + } + return context.next(); + }, + async (context) => { + try { + return turnstilePlugin({ + secret: context.env.turnstile_secret_key, + onError: (context) => { + console.error("Turnstile error:", context.error); + return createErrorResponse("Turnstile verification failed", 403); + } + })(context) + } catch (e) { + console.error("Turnstile error:", e); + return createErrorResponse("Turnstile verification failed", 400); + } + } +] \ No newline at end of file diff --git a/index.html b/index.html index a33c0ac..0eb11e1 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 3560a09..b1ff965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "0.0.0", "dependencies": { "@cloudflare/pages-plugin-hcaptcha": "^1.0.4", + "@cloudflare/pages-plugin-turnstile": "^1.0.2", "@hcaptcha/vue3-hcaptcha": "^1.3.0", "file-type": "^20.4.1", "jose": "^6.0.10", "pinia": "^3.0.2", "vue": "^3.5.13", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "vue-turnstile": "^1.0.11", + "vue3-recaptcha-v2": "^2.1.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250327.0", @@ -75,6 +78,12 @@ "integrity": "sha512-LZ1kWAhj3/wdAAnJs/fUG8akC+FKizLu2AdhVzr4aExncXA2wjXvphktG40pvEMIoMXH5LBqP9H7YNguR14Y7Q==", "license": "MIT" }, + "node_modules/@cloudflare/pages-plugin-turnstile": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/pages-plugin-turnstile/-/pages-plugin-turnstile-1.0.2.tgz", + "integrity": "sha512-vKPqN/guV1sk/t8TUIZEGlrlld3iwmfdbuWex8jbhUmv/9ozf2HmBjVBYgnr5q7UPjQsWB7hwqZsx8z3k+BkyQ==", + "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", @@ -1917,6 +1926,24 @@ "peerDependencies": { "vue": "^3.2.0" } + }, + "node_modules/vue-turnstile": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vue-turnstile/-/vue-turnstile-1.0.11.tgz", + "integrity": "sha512-iaTBoZ5oUqtNRto6bmbn6FQvW0h/sK7mPUJc1Qn4em+cELXN59U2FQTcpWfKssV3OY6lEZzmCpcn/zrb7htK3A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.45" + } + }, + "node_modules/vue3-recaptcha-v2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vue3-recaptcha-v2/-/vue3-recaptcha-v2-2.1.0.tgz", + "integrity": "sha512-dy1qieyWkRHR0yfuHaiI4aPKAsDJ/9Gwl58bl7gU9UtDMOfFAAmimMbyuYTRdxNU90dapJ5LLf2u+2h+gfOiSg==", + "license": "MIT", + "peerDependencies": { + "vue": "^3" + } } } } diff --git a/package.json b/package.json index f023306..3163555 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,21 @@ "preview": "vite preview", "pages:dev": "wrangler pages dev --proxy 5173", "pages:deploy": "wrangler pages deploy dist", - "deploy": "npm run build && npm run pages:deploy" + "deploy": "npm run build && npm run pages:deploy", + "migrate": "wrangler d1 execute --file schema.sql --local awd-db", + "d1:migrate": "wrangler d1 execute --file schema.sql --local awd-db" }, "dependencies": { "@cloudflare/pages-plugin-hcaptcha": "^1.0.4", + "@cloudflare/pages-plugin-turnstile": "^1.0.2", "@hcaptcha/vue3-hcaptcha": "^1.3.0", "file-type": "^20.4.1", "jose": "^6.0.10", "pinia": "^3.0.2", "vue": "^3.5.13", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "vue-turnstile": "^1.0.11", + "vue3-recaptcha-v2": "^2.1.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250327.0", diff --git a/src/components/Board/BoardForm.vue b/src/components/Board/BoardForm.vue index cdc77ad..fa37888 100644 --- a/src/components/Board/BoardForm.vue +++ b/src/components/Board/BoardForm.vue @@ -1,36 +1,32 @@ @@ -52,15 +48,14 @@ function submit() { -
- +
diff --git a/src/components/CAPTCHA.vue b/src/components/CAPTCHA.vue new file mode 100644 index 0000000..7ae4e70 --- /dev/null +++ b/src/components/CAPTCHA.vue @@ -0,0 +1,109 @@ + + + \ No newline at end of file diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 6fced95..01c25af 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -1,26 +1,20 @@ @@ -92,16 +89,15 @@ const submit = () => { -
diff --git a/src/components/Profile/ProfileForm.vue b/src/components/Profile/ProfileForm.vue index 2c53cdd..4f6d8a5 100644 --- a/src/components/Profile/ProfileForm.vue +++ b/src/components/Profile/ProfileForm.vue @@ -2,7 +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 CAPTCHA from '../CAPTCHA.vue'; import { useAuthStore } from '../../stores/auth'; const props = defineProps({ @@ -15,22 +15,20 @@ const props = defineProps({ const authStore = useAuthStore(); const router = useRouter(); -const hcaptchaRef = ref(null); - const avatarFile = ref(null); const avatarError = ref(''); -const hcaptchaResponse = ref(''); +const captchaResponse = ref(null); const onFileChange = (event) => { avatarFile.value = event.target.files[0]; }; -const handleHcaptchaVerify = (token) => { - hcaptchaResponse.value = token; -}; +const captchaVerified = computed(() => { + return captchaResponse.value !== null; +}); -const handleHcaptchaExpired = () => { - hcaptchaResponse.value = ''; +const handleCaptchaVerified = (response) => { + captchaResponse.value = response; }; const onSubmit = async () => { @@ -41,7 +39,7 @@ const onSubmit = async () => { return; } - if (!hcaptchaResponse.value) { + if (!captchaVerified) { avatarError.value = '請完成驗證。'; return; } @@ -57,9 +55,8 @@ const onSubmit = async () => { } try { - await uploadAvatar(avatarFile.value, authStore.jwt, hcaptchaResponse.value); + await uploadAvatar(avatarFile.value, authStore.jwt, captchaResponse.value); alert('Avatar uploaded successfully!'); - hcaptchaRef.value.reset(); // After successful upload, reload this page router.go(0); } catch (error) { @@ -103,15 +100,14 @@ const avatarUrl = computed(() => { -
- +
diff --git a/src/components/Register/RegisterForm.vue b/src/components/Register/RegisterForm.vue index 951e0cc..252864a 100644 --- a/src/components/Register/RegisterForm.vue +++ b/src/components/Register/RegisterForm.vue @@ -1,25 +1,19 @@