diff --git a/.env.example b/.env.example index 89159a0..90322e7 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,9 @@ CLIENT_SECRET= # Session token salt SESH_SECRET= +# Secret used to generate ALTCHA captchas +ALTCHA_HMAC= + # SQLite DB location DATABASE_URL=local.db diff --git a/package.json b/package.json index 0157660..010f7fe 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,8 @@ "@fontsource-variable/smooch-sans": "^5.1.1", "@upstash/ratelimit": "^2.0.5", "@upstash/redis": "^1.34.4", + "altcha": "^1.1.1", + "altcha-lib": "^1.2.0", "better-sqlite3": "^11.8.0", "drizzle-orm": "^0.38.4", "nanoid": "^5.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a8fe9c..ea380fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: '@upstash/redis': specifier: ^1.34.4 version: 1.34.4 + altcha: + specifier: ^1.1.1 + version: 1.1.1 + altcha-lib: + specifier: ^1.2.0 + version: 1.2.0 better-sqlite3: specifier: ^11.8.0 version: 11.8.1 @@ -157,6 +163,9 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@altcha/crypto@0.0.1': + resolution: {integrity: sha512-qZMdnoD3lAyvfSUMNtC2adRi666Pxdcw9zqfMU5qBOaJWqpN9K+eqQGWqeiKDMqL0SF+EytNG4kR/Pr/99GJ6g==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -959,6 +968,11 @@ packages: cpu: [s390x] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.18.0': + resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.31.0': resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==} cpu: [x64] @@ -1172,6 +1186,12 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + altcha-lib@1.2.0: + resolution: {integrity: sha512-S5WF8QLNRaM1hvK24XPhOLfu9is2EBCvH7+nv50sM5CaIdUCqQCd0WV/qm/ZZFGTdSoKLuDp+IapZxBLvC+SNg==} + + altcha@1.1.1: + resolution: {integrity: sha512-BPqLHiCAcVuF+dwshPyVBtpAXvgcSOO5DA3KfMLfQO3I5gxytE2bUrclYK5E8vWAzzrkiiz6OhFrZ69nUAOOzg==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2748,6 +2768,8 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@altcha/crypto@0.0.1': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -3292,6 +3314,9 @@ snapshots: '@rollup/rollup-linux-s390x-gnu@4.31.0': optional: true + '@rollup/rollup-linux-x64-gnu@4.18.0': + optional: true + '@rollup/rollup-linux-x64-gnu@4.31.0': optional: true @@ -3554,6 +3579,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + altcha-lib@1.2.0: {} + + altcha@1.1.1: + dependencies: + '@altcha/crypto': 0.0.1 + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': 4.18.0 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} diff --git a/src/app.css b/src/app.css index eb586c0..a75b1fa 100644 --- a/src/app.css +++ b/src/app.css @@ -32,6 +32,16 @@ --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; + + --altcha-border-width: 2px; + --altcha-border-radius: var(--radius); + --altcha-color-base: #ffffff; + --altcha-color-border: #f5f5f4; + --altcha-color-text: #0c0a09; + --altcha-color-border-focus: #78716c; + --altcha-color-error-text: #f23939; + --altcha-color-footer-bg: #f5f5f4; + --altcha-max-width: 260px; } .dark { @@ -51,7 +61,7 @@ --secondary-foreground: 60 9.1% 94%; --accent: 12 6.5% 15.1%; --accent-foreground: 60 9.1% 94%; - --destructive: 0 62.8% 30.6%; + --destructive: 0 70% 63.9%; --destructive-foreground: 60 9.1% 94%; --ring: 24 5.7% 82.9%; --sidebar-background: 240 5.9% 10%; @@ -62,6 +72,13 @@ --sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; + + --altcha-color-base: #0c0a09; + --altcha-color-text: #f1f1ee; + --altcha-color-border: #292524; + --altcha-color-border-focus: #a8a29e; + --altcha-color-error-text: #f23939; + --altcha-color-footer-bg: #292524; } } diff --git a/src/lib/components/Altcha.svelte b/src/lib/components/Altcha.svelte new file mode 100644 index 0000000..1fd203b --- /dev/null +++ b/src/lib/components/Altcha.svelte @@ -0,0 +1,41 @@ + + + + + + { + if (ev.detail.payload) { + value = ev.detail.payload; + } + }} +> diff --git a/src/routes/api/altcha/+server.ts b/src/routes/api/altcha/+server.ts new file mode 100644 index 0000000..96de3c5 --- /dev/null +++ b/src/routes/api/altcha/+server.ts @@ -0,0 +1,22 @@ +import { ALTCHA_HMAC } from '$env/static/private'; +import { json } from '@sveltejs/kit'; + +import { createChallenge, verifySolution } from 'altcha-lib'; + +export async function GET() { + const challenge = await createChallenge({ + hmacKey: ALTCHA_HMAC, + maxNumber: 100000 // the maximum random number + }); + console.log('challange get'); + + return json(challenge); +} + +export async function POST({ request }) { + const { payload }: { payload: string } = await request.json(); + console.log('challange done'); + const ok = await verifySolution(payload, ALTCHA_HMAC); + + return json({ ok }); +} diff --git a/src/routes/vinge/rahvatarkus/+page.server.ts b/src/routes/vinge/rahvatarkus/+page.server.ts index 41a3122..5e29779 100644 --- a/src/routes/vinge/rahvatarkus/+page.server.ts +++ b/src/routes/vinge/rahvatarkus/+page.server.ts @@ -87,6 +87,29 @@ export const actions: Actions = { }); } + const altchaValid = await event + .fetch('/api/altcha', { method: 'POST', body: JSON.stringify({ payload: form.data.altcha }) }) + .then((res) => { + return res.json(); + }) + .then((data) => { + return data; + }); + + if (!altchaValid) { + const message = + 'Altchale ei meeldinud see. Sa oled kas liiga boti laadse käitumisega või minu implementatsioon on kohutav.'; + + if (form.errors.answer) { + form.errors.answer.push(message); + } else { + form.errors.answer = [message]; + } + return fail(429, { + form + }); + } + const response = await event .fetch('/api/rahvatarkus/answer', { method: 'POST', @@ -180,6 +203,29 @@ export const actions: Actions = { }); } + const altchaValid = await event + .fetch('/api/altcha', { method: 'POST', body: JSON.stringify({ payload: form.data.altcha }) }) + .then((res) => { + return res.json(); + }) + .then((data) => { + return data; + }); + + if (!altchaValid) { + const message = + 'Altchale ei meeldinud see. Sa oled kas liiga boti laadse käitumisega või minu implementatsioon on kohutav.'; + + if (form.errors.question) { + form.errors.question.push(message); + } else { + form.errors.question = [message]; + } + return fail(429, { + form + }); + } + const response = await event .fetch('/api/rahvatarkus/question', { method: 'POST', diff --git a/src/routes/vinge/rahvatarkus/answer-form.svelte b/src/routes/vinge/rahvatarkus/answer-form.svelte index ab7c6c5..555cbf7 100644 --- a/src/routes/vinge/rahvatarkus/answer-form.svelte +++ b/src/routes/vinge/rahvatarkus/answer-form.svelte @@ -10,6 +10,7 @@ import * as Form from '$lib/components/ui/form/index.js'; import { Input } from '$lib/components/ui/input/index.js'; import { Textarea } from '$lib/components/ui/textarea/index.js'; + import Altcha from '$lib/components/Altcha.svelte'; let { data @@ -22,9 +23,9 @@ invalidateAll: 'force', onUpdated: ({ form: f }) => { if (f.valid) { - toast.success(`You submitted ${JSON.stringify(f.data, null, 2)}`); + toast.success('Vastus saadetud.'); } else { - toast.error('Please fix the errors in the form.'); + toast.error('Vastamine nurjus, palun paranda vead.'); } } }); @@ -56,11 +57,20 @@ {#snippet children({ props })} - {data.question.content}? -