Add altcha captcha to rahvatarkus, adjust some wordings in rahvatarkus

This commit is contained in:
Mihkel Martin Kasterpalu 2025-02-12 04:33:08 +02:00
parent 4363f56c23
commit c6ec335b04
11 changed files with 207 additions and 20 deletions

View file

@ -5,6 +5,9 @@ CLIENT_SECRET=<spotifyAPISecret>
# Session token salt
SESH_SECRET=<longRandomString>
# Secret used to generate ALTCHA captchas
ALTCHA_HMAC=<longRandomString>
# SQLite DB location
DATABASE_URL=local.db

View file

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

33
pnpm-lock.yaml generated
View file

@ -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: {}

View file

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

View file

@ -0,0 +1,41 @@
<script lang="ts">
import { onMount } from 'svelte';
import Input from './ui/input/input.svelte';
// Importing altcha package will introduce a new element <altcha-widget>
onMount(async () => {
await import('altcha');
});
let { value = $bindable(), ...props } = $props();
const estonianStrings = {
ariaLinkLabel: 'Külasta Altcha.org',
error: 'Kinnitus nurjus. Proovi hiljem uuesti.',
expired: 'Kinnitus aegus. Proovi uuesti.',
footer:
'Turvab <a href="https://altcha.org/" target="_blank" aria-label="Külasta Altcha.org">ALTCHA</a>',
label: 'Ma ei ole robot',
verified: 'Kõik ok!',
verifying: 'Kinntan...',
waitAlert: 'Kinnitan... palun oota.'
};
</script>
<Input type="hidden" bind:value {...props} />
<!-- Configure your `challengeurl` and remove the `test` attribute, see docs: https://altcha.org/docs/website-integration/#using-altcha-widget -->
<altcha-widget
strings={JSON.stringify(estonianStrings)}
debug
challengeurl="/api/altcha"
spamfilter
blockspam
hidefooter
expire={180000}
onverified={(ev) => {
if (ev.detail.payload) {
value = ev.detail.payload;
}
}}
></altcha-widget>

View file

@ -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 });
}

View file

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

View file

@ -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 @@
<Form.Field {form} name="answer">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{data.question.content}?</Form.Label>
<Textarea {...props} bind:value={$formData.answer} class="resize-none" />
<Form.Label class="transition-colors">{data.question.content}?</Form.Label>
<Textarea
{...props}
bind:value={$formData.answer}
class="resize-none transition-colors"
/>
{/snippet}
</Form.Control>
<div class="flex justify-between">
<Form.FieldErrors />
<Form.Description>
{$formData.answer.length}/{$constraints.answer?.maxlength}
</Form.Description>
</div>
</Form.Field>
<Form.Field {form} name="questionId">
<Form.Control>
@ -68,10 +78,13 @@
<Input type="hidden" {...props} bind:value={$formData.questionId} />
{/snippet}
</Form.Control>
<Form.Description class="text-right">
{$formData.answer.length}/{$constraints.answer?.maxlength}
</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="altcha" class="mx-auto mt-3 w-[var(--altcha-max-width)]">
<Form.Control>
{#snippet children({ props })}
<Altcha {...props} bind:value={$formData.altcha} />
{/snippet}
</Form.Control>
</Form.Field>
</Card.Content>
<Card.Footer class="justify-center">

View file

@ -5,7 +5,8 @@ export const formSchema = z.object({
answer: z
.string()
.min(2, 'Vastus peab olema vähemalt 2 tähemärki.')
.max(150, 'Vastus ei või olla pikem kui 150 tähemärki.')
.max(150, 'Vastus ei või olla pikem kui 150 tähemärki.'),
altcha: z.string()
});
export type FormSchema = typeof formSchema;

View file

@ -9,6 +9,7 @@
import * as Form from '$lib/components/ui/form/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import Altcha from '$lib/components/Altcha.svelte';
let {
data
@ -29,9 +30,9 @@
resetForm: true,
onUpdated: ({ form: f }) => {
if (f.valid) {
toast.success(`You submitted ${JSON.stringify(f.data, null, 2)}`);
toast.success('Küsimus esitatud.');
} else {
toast.error('Please fix the errors in the form.');
toast.error('Küsimine nurjus, palun paranda vead.');
}
}
});
@ -42,7 +43,9 @@
<Card.Root>
<Card.Header>
<Card.Title>Küsi rahvalt</Card.Title>
<Card.Description>Iga vastus annab võimaluse küsida ühe küsimuse.</Card.Description>
<Card.Description
>Sul on alles {data.user.balance > 0 ? data.user.balance : data.poolSize > 0 ? 0 : 1} küsimust.</Card.Description
>
</Card.Header>
{#if data.user.balance === 0 && (!data.question || data.poolSize > 0)}
<Card.Content>
@ -54,16 +57,21 @@
<Form.Field {form} name="question">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Küsimus rahvale</Form.Label>
<Input {...props} bind:value={$formData.question} />
<Form.Label class="transition-colors">Küsimus rahvale</Form.Label>
<Input {...props} bind:value={$formData.question} class="transition-colors" />
{/snippet}
</Form.Control>
<Form.Description class="flex justify-between">
<p>
Sul on alles {data.user.balance > 0 ? data.user.balance : data.poolSize > 0 ? 0 : 1} küsimust.
</p>
<Form.FieldErrors />
<p>{$formData.question.length}/{$constraints.question?.maxlength}</p>
</Form.Description>
</Form.Field>
<Form.Field {form} name="altcha" class="mx-auto mt-3 w-[var(--altcha-max-width)]">
<Form.Control>
{#snippet children({ props })}
<Altcha {...props} bind:value={$formData.altcha} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</Card.Content>

View file

@ -4,7 +4,8 @@ export const formSchema = z.object({
question: z
.string()
.min(2, 'Küsimus peab olema vähemalt 2 tähemärki.')
.max(50, 'Küsimus ei või olla pikem kui 50 tähemärki.')
.max(50, 'Küsimus ei või olla pikem kui 50 tähemärki.'),
altcha: z.string()
});
export type FormSchema = typeof formSchema;