diff options
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte | 9 | ||||
| -rw-r--r-- | src/routes/(app)/settings/delete/+page.server.ts | 26 | ||||
| -rw-r--r-- | src/routes/(app)/settings/delete/+page@(app).svelte | 82 | ||||
| -rw-r--r-- | src/routes/(app)/settings/security/+page.server.ts | 7 | ||||
| -rw-r--r-- | src/routes/(app)/settings/security/+page.svelte | 60 | ||||
| -rw-r--r-- | src/routes/(static)/account-reset/+page.svelte | 14 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.server.ts | 1 |
7 files changed, 197 insertions, 2 deletions
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte index 7b42a42d..5f64edd1 100644 --- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte +++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte @@ -39,7 +39,10 @@ ); $effect(() => (maxfee = quoted ? fee : fallback)); - let total = $derived((amount || 0) + (maxfee || 0) + (ourfee || 0)); + // maxfee comes back from the input as whatever was typed: as a number + // here, so the total adds it rather than gluing the digits on ("1" made + // a 10,000-sat send read as ten million) + let total = $derived((Number(amount) || 0) + (Number(maxfee) || 0) + (Number(ourfee) || 0)); // Server-side messages worth showing verbatim: a balance shortfall, or the // pre-flight check that the max fee is below what routing now costs. @@ -162,6 +165,10 @@ <input id="maxfee" name="fee" + type="number" + min="0" + step="1" + inputmode="numeric" bind:value={maxfee} class="clean !grow" /> diff --git a/src/routes/(app)/settings/delete/+page.server.ts b/src/routes/(app)/settings/delete/+page.server.ts new file mode 100644 index 00000000..05f4f026 --- /dev/null +++ b/src/routes/(app)/settings/delete/+page.server.ts @@ -0,0 +1,26 @@ +import { auth, post } from "$lib/utils"; +import { fail, redirect } from "@sveltejs/kit"; + +export async function load({ parent, url }) { + const { user } = await parent(); + if (!user) redirect(307, `/login?redirect=${encodeURIComponent(url.pathname)}`); +} + +export const actions = { + default: async ({ cookies, request }) => { + const form = await request.formData(); + const confirm = String(form.get("confirm") || ""); + const pin = String(form.get("pin") || ""); + + try { + // The server wipes everything (lib/deluser), including disabling the + // account's bolt12 offers, and refuses while a real balance remains. + await post("/delete", { confirm, ...(pin ? { pin } : {}) }, auth(cookies)); + } catch (e) { + return fail(400, { error: e?.message || String(e), confirm }); + } + + // The account is gone; /logout clears the session cookies. + redirect(303, "/logout"); + }, +}; diff --git a/src/routes/(app)/settings/delete/+page@(app).svelte b/src/routes/(app)/settings/delete/+page@(app).svelte new file mode 100644 index 00000000..e19a6604 --- /dev/null +++ b/src/routes/(app)/settings/delete/+page@(app).svelte @@ -0,0 +1,82 @@ +<script> + // Rendered outside the settings layout (+page@(app)) so this form isn't + // nested inside the settings save form. + import { enhance } from "$app/forms"; + import { t } from "$lib/translations"; + + let { data, form } = $props(); + let { user } = $derived(data); + + let confirm = $state(form?.confirm || ""); + let working = $state(false); + let matches = $derived( + confirm.trim().toLowerCase() === user.username.toLowerCase(), + ); +</script> + +<div class="container px-4 max-w-xl mx-auto space-y-5 mt-10"> + <h1 class="text-3xl font-bold text-center"> + {$t("user.settings.deleteAccount")} + </h1> + + <div class="rounded-2xl border-2 border-error p-4 space-y-3"> + <p class="flex gap-2"> + <iconify-icon + noobserver + icon="ph:warning-bold" + width="24" + class="text-error shrink-0" + ></iconify-icon> + <span>{$t("user.settings.deleteAccountWarning")}</span> + </p> + <p class="text-secondary">{$t("user.settings.deleteAccountBalance")}</p> + </div> + + {#if form?.error}<p class="text-error">{form.error}</p>{/if} + + <form + method="POST" + class="space-y-3" + use:enhance={() => { + working = true; + return async ({ update }) => { + await update({ reset: false }); + working = false; + }; + }} + > + <label class="block space-y-1"> + <span class="font-bold">{$t("user.settings.deleteAccountConfirm")}</span> + <input + class="input input-bordered w-full" + name="confirm" + autocomplete="off" + autocapitalize="off" + spellcheck="false" + placeholder={user.username} + bind:value={confirm} + /> + </label> + + {#if user.haspin} + <input + class="input input-bordered w-full" + type="password" + inputmode="numeric" + name="pin" + placeholder={$t("user.settings.securityPIN")} + /> + {/if} + + <button + class="btn btn-error w-full rounded-2xl" + disabled={!matches || working} + > + <iconify-icon noobserver icon="ph:trash-bold" width="24"></iconify-icon> + {$t("user.settings.deleteAccountButton")} + </button> + <a href="/settings/security" class="btn w-full rounded-2xl"> + {$t("payments.cancel")} + </a> + </form> +</div> diff --git a/src/routes/(app)/settings/security/+page.server.ts b/src/routes/(app)/settings/security/+page.server.ts index 4145935e..e7cc93a3 100644 --- a/src/routes/(app)/settings/security/+page.server.ts +++ b/src/routes/(app)/settings/security/+page.server.ts @@ -1,4 +1,11 @@ import updateUser from "$lib/settings"; +import { auth, get } from "$lib/utils"; + +export const load = async ({ cookies }) => { + const token = cookies.get("token"); + const ro = token ? await get("/ro", auth(cookies)) : undefined; + return { ro, token }; +}; export const actions = { default: updateUser, diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte index 8c38bca1..b8c71aa6 100644 --- a/src/routes/(app)/settings/security/+page.svelte +++ b/src/routes/(app)/settings/security/+page.svelte @@ -10,7 +10,7 @@ import { page } from "$app/stores"; let { data } = $props(); - let { user } = $derived(data); + let { user, ro, token: apiToken } = $derived(data); let { id } = user; let { haspin } = $derived(user); @@ -23,6 +23,7 @@ pin = $state(""), revealSeed, revealNsec = $state(), + revealToken = $state(), token = $state(""), setting2fa = $state(), settingPin = $state(), @@ -232,6 +233,52 @@ {/if} </div> +{#snippet tokenBox(value)} + <div + class="bg-primary text-primary-content rounded-lg p-4 flex gap-4 items-center" + > + <div class="w-full break-all">{value}</div> + <button type="button" class="ml-auto" onclick={() => copy(value)}> + <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon> + </button> + </div> +{/snippet} + +{#if ro} + <div class="space-y-4"> + <div> + <span class="font-bold mb-1">{$t("user.settings.apiTokens")}</span> + <p class="text-secondary"> + {$t("user.settings.apiTokensDescription")} + <a href="/docs" class="underline">{$t("user.settings.apiDocs")}</a> + </p> + </div> + + <div> + <span class="font-bold mb-1">{$t("user.settings.readOnlyToken")}</span> + <p class="text-secondary mb-2"> + {$t("user.settings.readOnlyTokenDescription")} + </p> + {@render tokenBox(ro)} + </div> + + <div> + <span class="font-bold mb-1">{$t("user.settings.fullAccessToken")}</span> + <p class="text-secondary mb-2"> + {$t("user.settings.fullAccessTokenDescription")} + </p> + {#if revealToken} + {@render tokenBox(apiToken)} + {:else} + <button type="button" class="btn" onclick={() => (revealToken = true)}> + <iconify-icon noobserver icon="ph:key-bold" width="32"></iconify-icon> + {$t("user.settings.revealToken")} + </button> + {/if} + </div> + </div> +{/if} + <div> <a href={`/account/savings`} class="contents"> <button class="btn btn-lg w-full rounded-2xl whitespace-nowrap"> @@ -241,3 +288,14 @@ </button> </a> </div> + +<div> + <span class="font-bold mb-1">{$t("user.settings.deleteAccount")}</span> + <p class="text-secondary mb-4"> + {$t("user.settings.deleteAccountDescription")} + </p> + <a href="/settings/delete" class="btn btn-outline btn-error"> + <iconify-icon noobserver icon="ph:trash-bold" width="32"></iconify-icon> + {$t("user.settings.deleteAccount")} + </a> +</div> diff --git a/src/routes/(static)/account-reset/+page.svelte b/src/routes/(static)/account-reset/+page.svelte new file mode 100644 index 00000000..b0545676 --- /dev/null +++ b/src/routes/(static)/account-reset/+page.svelte @@ -0,0 +1,14 @@ +<script> + import { t } from "$lib/translations"; +</script> + +<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" +> + <h1 class="text-2xl font-bold text-center">{$t("login.accountReset")}</h1> + + <div class="text-secondary"> + {$t("login.accountResetDesc")} + <a href="mailto:[email protected]" class="font-bold">[email protected]</a> + </div> +</div> diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts index ce343790..24c34584 100644 --- a/src/routes/(static)/login/+page.server.ts +++ b/src/routes/(static)/login/+page.server.ts @@ -28,6 +28,7 @@ export const actions = { await login(user, cookies, request.headers.get("cf-connecting-ip"), request.headers.get("host")); } catch (e) { const { message } = e as Error; + if (message === "account reset") redirect(303, "/account-reset"); return fail(400, { error: "Login failed", message, ...form }); } |
