diff options
| author | Adam Soltys <[email protected]> | 2026-01-21 12:13:33 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-01-21 12:13:33 -0800 |
| commit | 89286318b4e1094685a67fac658495fafee893de (patch) | |
| tree | dfb70d7b2acaf34c0e6f18c2d51275eb036a0d4a /src/routes | |
| parent | 4c3682b7dcc6fb77e94822756030f9aef430ee5e (diff) | |
| download | coinos-ui-89286318b4e1094685a67fac658495fafee893de.tar.gz coinos-ui-89286318b4e1094685a67fac658495fafee893de.zip | |
captcha
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/(app)/reset/[code]/+page.server.ts | 3 | ||||
| -rw-r--r-- | src/routes/(app)/reset/[code]/+page.svelte | 55 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.server.ts | 10 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.svelte | 47 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.server.ts | 4 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.svelte | 48 |
6 files changed, 157 insertions, 10 deletions
diff --git a/src/routes/(app)/reset/[code]/+page.server.ts b/src/routes/(app)/reset/[code]/+page.server.ts index 533e21f5..35d49be9 100644 --- a/src/routes/(app)/reset/[code]/+page.server.ts +++ b/src/routes/(app)/reset/[code]/+page.server.ts @@ -7,11 +7,12 @@ export const actions = { let error; const { code } = params; - const { password } = await fd(request); + const { password, recaptcha } = await fd(request); try { const user = await post("/reset", { code, password }); user.password = password; + user.recaptcha = recaptcha; try { await login(user, cookies, ip); diff --git a/src/routes/(app)/reset/[code]/+page.svelte b/src/routes/(app)/reset/[code]/+page.svelte index df9d1144..227d0106 100644 --- a/src/routes/(app)/reset/[code]/+page.svelte +++ b/src/routes/(app)/reset/[code]/+page.svelte @@ -1,16 +1,68 @@ <script> + import { browser } from "$app/environment"; + import { onDestroy, tick } from "svelte"; import { t } from "$lib/translations"; import PasswordInput from "$comp/PasswordInput.svelte"; import Icon from "$comp/Icon.svelte"; import Spinner from "$comp/Spinner.svelte"; import { fly } from "svelte/transition"; import { password } from "$lib/store"; + import { fail } from "$lib/utils"; let { form } = $props(); let email; let loading; + let recaptchaToken = ""; + let recaptchaSiteKey = "6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"; + + const getRecaptchaToken = () => + new Promise((resolve, reject) => { + if (!browser || !grecaptcha) return reject(new Error("captcha unavailable")); + grecaptcha.ready(() => { + grecaptcha + .execute(recaptchaSiteKey, { action: "reset" }) + .then(resolve) + .catch(reject); + }); + }); + + const handleSubmit = async (e) => { + e.preventDefault(); + loading = true; + try { + recaptchaToken = await getRecaptchaToken(); + await tick(); + e.currentTarget.submit(); + } catch (err) { + fail(err.message || "captcha failed"); + loading = false; + } + }; + + onDestroy(() => { + if (!browser) return; + const nodeBadge = document.querySelector(".grecaptcha-badge"); + if (nodeBadge) { + document.body.removeChild(nodeBadge.parentNode); + } + + const scriptSelector = + "script[src='https://www.google.com/recaptcha/api.js?render=" + + recaptchaSiteKey + + "']"; + const script = document.querySelector(scriptSelector); + if (script) { + script.remove(); + } + }); </script> +<svelte:head + ><script + src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM" + ></script></svelte:head +> + <div class="pt-10"> <div class="w-[243px] mx-auto mb-10"> <a href="/"> @@ -30,7 +82,8 @@ </div> {/if} - <form class="space-y-5" method="POST"> + <form class="space-y-5" method="POST" onsubmit={handleSubmit}> + <input type="hidden" name="recaptcha" value={recaptchaToken} /> <PasswordInput bind:value={$password} placeholder={$t("login.password")} diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts index dc68879a..dacb94b2 100644 --- a/src/routes/(static)/login/+page.server.ts +++ b/src/routes/(static)/login/+page.server.ts @@ -12,13 +12,15 @@ export const load = async ({ parent }) => { export const actions = { login: async ({ cookies, request }) => { const form = await fd(request); - let { username, password, token, loginRedirect, challenge } = form; + let { username, password, token, loginRedirect, challenge, recaptcha } = + form; const user = { - challenge, + challenge, username, password, token, + recaptcha, }; if (loginRedirect === "undefined") loginRedirect = undefined; @@ -35,7 +37,7 @@ export const actions = { nostr: async ({ cookies, fetch, request }) => { const form = await fd(request); - let { challenge, event, loginRedirect } = form; + let { challenge, event, loginRedirect, recaptcha } = form; if (loginRedirect === "null") loginRedirect = undefined; event = JSON.parse(event); @@ -43,7 +45,7 @@ export const actions = { const res = await fetch(`${PUBLIC_COINOS_URL}/nostrAuth`, { method: "POST", - body: JSON.stringify({ challenge, event }), + body: JSON.stringify({ challenge, event, recaptcha }), headers: { "content-type": "application/json", accept: "application/json", diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte index 450425f6..08656949 100644 --- a/src/routes/(static)/login/+page.svelte +++ b/src/routes/(static)/login/+page.svelte @@ -2,7 +2,7 @@ import { browser } from "$app/environment"; import handler from "$lib/handler"; import { applyAction, deserialize } from "$app/forms"; - import { onMount, tick } from "svelte"; + import { onDestroy, onMount, tick } from "svelte"; import { fly } from "svelte/transition"; import { enhance } from "$app/forms"; import Pinpad from "$comp/Pinpad.svelte"; @@ -37,6 +37,7 @@ }); let { challenge } = $derived(data); + let recaptchaSiteKey = "6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"; let token = $state(""); let cancel = () => (need2fa = false); @@ -53,6 +54,17 @@ let revealPassword = $state(false); + const getRecaptchaToken = () => + new Promise((resolve, reject) => { + if (!browser || !grecaptcha) return reject(new Error("captcha unavailable")); + grecaptcha.ready(() => { + grecaptcha + .execute(recaptchaSiteKey, { action: "login" }) + .then(resolve) + .catch(reject); + }); + }); + async function handleSubmit(e) { e.preventDefault(); let data = new FormData(this); @@ -62,6 +74,14 @@ data.set(k, user[k]); } + try { + const recaptcha = await getRecaptchaToken(); + data.set("recaptcha", recaptcha); + } catch (err) { + fail(err.message || "captcha failed"); + return; + } + const response = await fetch("?/login", { method: "POST", body: data, @@ -104,10 +124,12 @@ const formData = new FormData(); try { + const recaptcha = await getRecaptchaToken(); formData.append("loginRedirect", redirect); formData.append("token", token); formData.append("event", JSON.stringify(signedEvent)); formData.append("challenge", challenge); + formData.append("recaptcha", recaptcha); let response = await fetch("/login?/nostr", { method: "POST", @@ -121,8 +143,31 @@ fail(e.message); } }; + + onDestroy(() => { + if (!browser) return; + const nodeBadge = document.querySelector(".grecaptcha-badge"); + if (nodeBadge) { + document.body.removeChild(nodeBadge.parentNode); + } + + const scriptSelector = + "script[src='https://www.google.com/recaptcha/api.js?render=" + + recaptchaSiteKey + + "']"; + const script = document.querySelector(scriptSelector); + if (script) { + script.remove(); + } + }); </script> +<svelte:head + ><script + src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM" + ></script></svelte:head +> + <div class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 mb-20 space-y-5" > diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts index 47ff8d27..d2dbe299 100644 --- a/src/routes/(static)/register/+page.server.ts +++ b/src/routes/(static)/register/+page.server.ts @@ -15,11 +15,11 @@ export const actions = { default: async ({ cookies, request }) => { const ip = request.headers.get("cf-connecting-ip"); const form = await fd(request); - const { picture, username, password, challenge } = form; + const { picture, username, password, challenge, recaptcha } = form; let { loginRedirect } = form; if (loginRedirect === "undefined") loginRedirect = undefined; - const user = { picture, username, password, challenge }; + const user = { picture, username, password, challenge, recaptcha }; return register(user, ip, cookies, loginRedirect); }, }; diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte index 9e255b8e..f1ac582b 100644 --- a/src/routes/(static)/register/+page.svelte +++ b/src/routes/(static)/register/+page.svelte @@ -1,5 +1,5 @@ <script> - import { onMount } from "svelte"; + import { onDestroy, onMount } from "svelte"; import { browser } from "$app/environment"; import punks from "$lib/punks"; import { upload } from "$lib/upload"; @@ -28,6 +28,7 @@ let { form, data } = $props(); let { challenge } = $derived(data); + let recaptchaSiteKey = "6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"; onMount(() => { if (browser) { @@ -93,6 +94,17 @@ btn = $state(); let loading = $state(); + const getRecaptchaToken = () => + new Promise((resolve, reject) => { + if (!browser || !grecaptcha) return reject(new Error("captcha unavailable")); + grecaptcha.ready(() => { + grecaptcha + .execute(recaptchaSiteKey, { action: "register" }) + .then(resolve) + .catch(reject); + }); + }); + async function handleSubmit(e) { e.preventDefault(); @@ -106,6 +118,15 @@ data.set(k, user[k]); } + try { + const recaptcha = await getRecaptchaToken(); + data.set("recaptcha", recaptcha); + } catch (err) { + fail(err.message || "captcha failed"); + loading = false; + return; + } + data.set("picture", `${$page.url.origin}/api/public/${punks[index]}.webp`); if ($avatar) { try { @@ -189,10 +210,12 @@ const formData = new FormData(); try { + const recaptcha = await getRecaptchaToken(); formData.append("loginRedirect", redirect); formData.append("token", token); formData.append("event", JSON.stringify(signedEvent)); formData.append("challenge", challenge); + formData.append("recaptcha", recaptcha); let response = await fetch("/login?/nostr", { method: "POST", @@ -210,8 +233,31 @@ fail(e.message); } }; + + onDestroy(() => { + if (!browser) return; + const nodeBadge = document.querySelector(".grecaptcha-badge"); + if (nodeBadge) { + document.body.removeChild(nodeBadge.parentNode); + } + + const scriptSelector = + "script[src='https://www.google.com/recaptcha/api.js?render=" + + recaptchaSiteKey + + "']"; + const script = document.querySelector(scriptSelector); + if (script) { + script.remove(); + } + }); </script> +<svelte:head + ><script + src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM" + ></script></svelte:head +> + {#if need2fa} <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> {/if} |
