aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--src/lib/utils.ts8
-rw-r--r--src/routes/(app)/reset/[code]/+page.server.ts3
-rw-r--r--src/routes/(app)/reset/[code]/+page.svelte55
-rw-r--r--src/routes/(static)/login/+page.server.ts10
-rw-r--r--src/routes/(static)/login/+page.svelte47
-rw-r--r--src/routes/(static)/register/+page.server.ts4
-rw-r--r--src/routes/(static)/register/+page.svelte48
8 files changed, 165 insertions, 11 deletions
diff --git a/.gitignore b/.gitignore
index db03a1c7..5976fc80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ node_modules
ios
.history/
db
+.claude/
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 28a219e1..a3d9cd49 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -148,7 +148,13 @@ export const info = (m: string) => {
};
export const login = async (
- user: { username: string; password: string },
+ user: {
+ username: string;
+ password: string;
+ token?: string;
+ challenge?: string;
+ recaptcha?: string;
+ },
cookies,
ip: string,
) => {
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}