aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte9
-rw-r--r--src/routes/(app)/settings/delete/+page.server.ts26
-rw-r--r--src/routes/(app)/settings/delete/+page@(app).svelte82
-rw-r--r--src/routes/(app)/settings/security/+page.server.ts7
-rw-r--r--src/routes/(app)/settings/security/+page.svelte60
-rw-r--r--src/routes/(static)/account-reset/+page.svelte14
-rw-r--r--src/routes/(static)/login/+page.server.ts1
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 });
}