diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 12:40:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 12:40:39 -0700 |
| commit | 2c9ab7a73c662e15229c340ba471fb3aeec41b70 (patch) | |
| tree | bf571daf23066adb3c24d312a871ce5d21e3c8a8 | |
| parent | bcc13f148d6097f9786ed2855808c31f9a2041c2 (diff) | |
| download | coinos-ui-2c9ab7a73c662e15229c340ba471fb3aeec41b70.tar.gz coinos-ui-2c9ab7a73c662e15229c340ba471fb3aeec41b70.zip | |
migrate: the retired name has to be letters and numbers only
The server enforces /^[\p{L}\p{N}]{2,24}$/u, so <name>_v2 was rejected
outright. Candidates are <name>v2, v3, … each trimmed to fit the 24-char
limit (so a long username still lands inside it), and the rename walks
to the next one if a retired name is already taken.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
| -rw-r--r-- | src/routes/(app)/migrate/+page.server.ts | 38 | ||||
| -rw-r--r-- | src/routes/(app)/migrate/+page.svelte | 3 |
2 files changed, 32 insertions, 9 deletions
diff --git a/src/routes/(app)/migrate/+page.server.ts b/src/routes/(app)/migrate/+page.server.ts index dc3beb02..5182e8a0 100644 --- a/src/routes/(app)/migrate/+page.server.ts +++ b/src/routes/(app)/migrate/+page.server.ts @@ -20,12 +20,26 @@ async function invoiceFor(address: string, sats: number) { return inv.pr as string; } +// The server only accepts letters and numbers, 2–24 of them, so the retired +// name can't carry a separator. Candidates are <name>v2, <name>v3, … each +// trimmed to fit, so a long username still lands inside the limit. +function retiredNames(username: string) { + const base = String(username).replace(/[^\p{L}\p{N}]/gu, ""); + const out: string[] = []; + for (let n = 2; n <= 9; n++) { + const suffix = `v${n}`; + out.push(base.slice(0, 24 - suffix.length) + suffix); + } + return out; +} + export async function load({ parent, url }) { const { user } = await parent(); if (!user) redirect(307, `/login?redirect=${encodeURIComponent(url.pathname + url.search)}`); return { to: url.searchParams.get("to") || "", back: url.searchParams.get("back") || "", + newName: retiredNames(user.username)[0], }; } @@ -62,13 +76,23 @@ export const actions = { } if (!sent) throw lastErr; } - // Releasing the username is what lets them keep it on v3. - await post( - "/user", - { username: `${username}_v2`, ...(pin ? { pin } : {}) }, - auth(cookies), - ); - return { ok: true, sent, released: username }; + // Releasing the username is what lets them keep it on v3. If the + // retired name is itself taken, step to the next candidate. + let renamed = ""; + let renameErr; + for (const candidate of retiredNames(username)) { + try { + await post("/user", { username: candidate, ...(pin ? { pin } : {}) }, auth(cookies)); + renamed = candidate; + break; + } catch (e) { + renameErr = e; + const msg = String((e as any)?.message || e); + if (!/taken/i.test(msg)) throw e; + } + } + if (!renamed) throw renameErr; + return { ok: true, sent, released: username, renamed }; } catch (e) { const msg = e && typeof e === "object" && "message" in e ? String(e.message) : String(e); return { error: msg, sent }; diff --git a/src/routes/(app)/migrate/+page.svelte b/src/routes/(app)/migrate/+page.svelte index eae612f1..e1e4a79d 100644 --- a/src/routes/(app)/migrate/+page.svelte +++ b/src/routes/(app)/migrate/+page.svelte @@ -9,11 +9,10 @@ import { enhance } from "$app/forms"; let { data, form } = $props(); - let { user, to, back } = $derived(data); + let { user, to, back, newName } = $derived(data); let working = $state(false); let balance = $derived(user?.balance || 0); - let newName = $derived(`${user?.username}_v2`); let backUrl = $derived.by(() => { try { const u = new URL(back || "https://v3.coinos.io/"); |
