fix(drafts): land /add redirects on the real workspace username, not "me"

The `/add` → `/edit/u/{username}/draft_{uuid}` redirects ran during
SvelteKit's load phase, BEFORE the (logged) layout's async `getUserExt`
populated `userStore`. `get(userStore)?.username` returned undefined and
fell back to the `'me'` placeholder on every fresh nav, producing
`u/me/draft_{uuid}` paths instead of the user's real namespace — broke
ownership checks against `authed.username` and silently scoped autosaves
under the wrong path.

Layout now persists `username` to localStorage on every successful
`getUserExt`, and `getUsernameForNamespace` (new shared helper, used by
all four `/add/+page.ts` files) reads the live store first, falls back
to the cached value, and only then to `'me'` for true first-ever loads.
This commit is contained in:
Diego Imbert
2026-06-07 17:45:30 +02:00
parent 9a4bab057b
commit 0e5c2310d0
6 changed files with 49 additions and 18 deletions
+31
View File
@@ -0,0 +1,31 @@
import { get } from 'svelte/store'
import { userStore } from '$lib/stores'
/**
* Workspace username for the authed user, suitable as the `u/{X}/...`
* namespace in editor draft paths.
*
* The `/add` → `/edit/u/{username}/draft_{uuid}` redirects in each
* editor's `+page.ts` run during SvelteKit's load phase — BEFORE the
* (logged) layout's async `getUserExt` populates `userStore`. Reading
* `get(userStore)?.username` from there returns `undefined` on every
* fresh nav, falling back to the `'me'` placeholder and producing
* `u/me/draft_{uuid}` paths instead of the real namespace.
*
* The layout writes `username` to `localStorage` on every successful
* `getUserExt`, so this helper:
* 1. Reads `userStore` first (live value when it IS populated, e.g.
* same-tab re-navs after the first load).
* 2. Falls back to `localStorage` (warm-cache from the previous
* session — covers the load-phase race on cold reload).
* 3. Falls back to `'me'` (true first-ever load with no session yet).
*/
export function getUsernameForNamespace(): string {
const live = get(userStore)?.username
if (live) return live
try {
const cached = localStorage.getItem('username')
if (cached) return cached
} catch {}
return 'me'
}
@@ -136,6 +136,16 @@
if (!deepEqual(user, $userStore)) {
userStore.set(user)
}
// Persist the workspace username so the synchronous `/add` →
// `/edit/u/{user}/draft_{uuid}` redirects (in each editor's
// `+page.ts`) can land on the right namespace without waiting
// for this async layout fetch. Without this they fall back to
// the `'me'` placeholder on every fresh nav.
try {
if (user?.username) localStorage.setItem('username', user.username)
} catch (e) {
console.error('Could not persist username to local storage', e)
}
if (isCloudHosted() && user?.is_admin) {
isPremiumStore.set(await WorkspaceService.getIsPremium({ workspace }))
}
@@ -1,7 +1,6 @@
import { redirect } from '@sveltejs/kit'
import { base } from '$app/paths'
import { get } from 'svelte/store'
import { userStore } from '$lib/stores'
import { getUsernameForNamespace } from '$lib/userNamespace'
import type { PageLoad } from './$types'
// See /scripts/add/+page.ts for the rationale — redirect at load time
@@ -10,7 +9,7 @@ import type { PageLoad } from './$types'
export const prerender = false
export const load: PageLoad = ({ url }) => {
const username = get(userStore)?.username ?? 'me'
const username = getUsernameForNamespace()
const uuid = crypto.randomUUID()
const params = new URLSearchParams(url.searchParams)
params.set('new_draft', 'true')
@@ -1,7 +1,6 @@
import { redirect } from '@sveltejs/kit'
import { base } from '$app/paths'
import { get } from 'svelte/store'
import { userStore } from '$lib/stores'
import { getUsernameForNamespace } from '$lib/userNamespace'
import type { PageLoad } from './$types'
// See /scripts/add/+page.ts for the rationale — redirect at load time
@@ -10,7 +9,7 @@ import type { PageLoad } from './$types'
export const prerender = false
export const load: PageLoad = ({ url }) => {
const username = get(userStore)?.username ?? 'me'
const username = getUsernameForNamespace()
const uuid = crypto.randomUUID()
const params = new URLSearchParams(url.searchParams)
params.set('new_draft', 'true')
@@ -1,7 +1,6 @@
import { redirect } from '@sveltejs/kit'
import { base } from '$app/paths'
import { get } from 'svelte/store'
import { userStore } from '$lib/stores'
import { getUsernameForNamespace } from '$lib/userNamespace'
import type { PageLoad } from './$types'
// See /scripts/add/+page.ts for the rationale — redirect at load time
@@ -10,7 +9,7 @@ import type { PageLoad } from './$types'
export const prerender = false
export const load: PageLoad = ({ url }) => {
const username = get(userStore)?.username ?? 'me'
const username = getUsernameForNamespace()
const uuid = crypto.randomUUID()
const params = new URLSearchParams(url.searchParams)
params.set('new_draft', 'true')
@@ -1,7 +1,6 @@
import { redirect } from '@sveltejs/kit'
import { base } from '$app/paths'
import { get } from 'svelte/store'
import { userStore } from '$lib/stores'
import { getUsernameForNamespace } from '$lib/userNamespace'
import type { PageLoad } from './$types'
// Redirect at the load phase, before the +page.svelte ever mounts. The
@@ -9,17 +8,11 @@ import type { PageLoad } from './$types'
// one frame before navigating, hence the white flash; doing the work in
// `load` lets SvelteKit cancel the route transition and head straight
// for the edit page.
//
// `userStore` is populated client-side by the (logged) layout, so by
// the time this load runs (always after layout setup) it usually has
// the real username. The `'me'` fallback is the same one the original
// onMount used — keeps behavior identical for the rare race where the
// store hasn't filled yet.
export const prerender = false
export const load: PageLoad = ({ url }) => {
const username = get(userStore)?.username ?? 'me'
const username = getUsernameForNamespace()
const uuid = crypto.randomUUID()
const params = new URLSearchParams(url.searchParams)
params.set('new_draft', 'true')