From 3932bc056e57bb4dbdb5766b55c78b619a1b19c3 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Tue, 24 Mar 2026 16:56:10 +0600 Subject: [PATCH] Add user names, team-scoped sandbox guard, and login robustness fixes - Add name column to users (migration + sqlc regen); propagate through JWT claims, auth context, all auth/OAuth handlers, service layer, and frontend - Sidebar and team page show name instead of email; team page splits Name/Email into separate columns - Block sandbox creation in UI and API when user has no active team context - loginTeam helper falls back to first active team when no default is set, fixing login for invited users with no is_default membership - Exclude soft-deleted teams from GetDefaultTeamForUser, GetBYOCTeams queries - Guard host creation against soft-deleted teams in service/host.go - SwitchTeam re-fetches name from DB instead of trusting stale JWT claim - Reset teams store on login so stale data from a previous session never persists - Update openapi.yaml: add name to SignupRequest and AuthResponse schemas --- db/migrations/20260324100234_user_names.sql | 5 ++ db/queries/teams.sql | 6 +- db/queries/users.sql | 11 ++- frontend/src/lib/api/auth.ts | 5 +- frontend/src/lib/api/team.ts | 3 +- frontend/src/lib/auth.svelte.ts | 13 +++- frontend/src/lib/components/Sidebar.svelte | 2 +- frontend/src/lib/teams.svelte.ts | 4 + .../routes/auth/github/callback/+page.svelte | 5 +- .../routes/dashboard/capsules/+page.svelte | 8 +- .../src/routes/dashboard/team/+page.svelte | 26 +++++-- frontend/src/routes/login/+page.svelte | 23 +++++- internal/api/handlers_auth.go | 74 ++++++++++++++++--- internal/api/handlers_oauth.go | 32 +++----- internal/api/handlers_sandbox.go | 4 + internal/api/handlers_team.go | 2 + internal/api/middleware_auth.go | 1 + internal/api/middleware_jwt.go | 1 + internal/api/openapi.yaml | 7 +- internal/auth/context.go | 1 + internal/auth/jwt.go | 4 +- internal/db/models.go | 1 + internal/db/teams.sql.go | 8 +- internal/db/users.sql.go | 48 +++++++++--- internal/service/host.go | 5 +- internal/service/team.go | 6 +- 26 files changed, 228 insertions(+), 77 deletions(-) create mode 100644 db/migrations/20260324100234_user_names.sql diff --git a/db/migrations/20260324100234_user_names.sql b/db/migrations/20260324100234_user_names.sql new file mode 100644 index 0000000..2775d12 --- /dev/null +++ b/db/migrations/20260324100234_user_names.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN name TEXT NOT NULL DEFAULT ''; + +-- +goose Down +ALTER TABLE users DROP COLUMN name; diff --git a/db/queries/teams.sql b/db/queries/teams.sql index 935c4dd..2117e95 100644 --- a/db/queries/teams.sql +++ b/db/queries/teams.sql @@ -13,14 +13,14 @@ VALUES ($1, $2, $3, $4); -- name: GetDefaultTeamForUser :one SELECT t.* FROM teams t JOIN users_teams ut ON ut.team_id = t.id -WHERE ut.user_id = $1 AND ut.is_default = TRUE +WHERE ut.user_id = $1 AND ut.is_default = TRUE AND t.deleted_at IS NULL LIMIT 1; -- name: SetTeamBYOC :exec UPDATE teams SET is_byoc = $2 WHERE id = $1; -- name: GetBYOCTeams :many -SELECT * FROM teams WHERE is_byoc = TRUE ORDER BY created_at; +SELECT * FROM teams WHERE is_byoc = TRUE AND deleted_at IS NULL ORDER BY created_at; -- name: GetTeamMembership :one SELECT * FROM users_teams WHERE user_id = $1 AND team_id = $2; @@ -42,7 +42,7 @@ WHERE ut.user_id = $1 AND t.deleted_at IS NULL ORDER BY ut.created_at; -- name: GetTeamMembers :many -SELECT u.id, u.email, ut.role, ut.created_at AS joined_at +SELECT u.id, u.name, u.email, ut.role, ut.created_at AS joined_at FROM users_teams ut JOIN users u ON u.id = ut.user_id WHERE ut.team_id = $1 diff --git a/db/queries/users.sql b/db/queries/users.sql index 171ef70..a244fc9 100644 --- a/db/queries/users.sql +++ b/db/queries/users.sql @@ -1,6 +1,6 @@ -- name: InsertUser :one -INSERT INTO users (id, email, password_hash) -VALUES ($1, $2, $3) +INSERT INTO users (id, email, password_hash, name) +VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetUserByEmail :one @@ -10,8 +10,8 @@ SELECT * FROM users WHERE email = $1; SELECT * FROM users WHERE id = $1; -- name: InsertUserOAuth :one -INSERT INTO users (id, email) -VALUES ($1, $2) +INSERT INTO users (id, email, name) +VALUES ($1, $2, $3) RETURNING *; -- name: SetUserAdmin :exec @@ -37,3 +37,6 @@ SELECT EXISTS( -- name: SearchUsersByEmailPrefix :many SELECT id, email FROM users WHERE email LIKE $1 || '%' ORDER BY email LIMIT 10; + +-- name: UpdateUserName :exec +UPDATE users SET name = $2, updated_at = NOW() WHERE id = $1; diff --git a/frontend/src/lib/api/auth.ts b/frontend/src/lib/api/auth.ts index 51b987a..845b8a3 100644 --- a/frontend/src/lib/api/auth.ts +++ b/frontend/src/lib/api/auth.ts @@ -3,6 +3,7 @@ export type AuthResponse = { user_id: string; team_id: string; email: string; + name: string; }; export type AuthResult = { ok: true; data: AuthResponse } | { ok: false; error: string }; @@ -11,8 +12,8 @@ export async function apiLogin(email: string, password: string): Promise { - return authFetch('/api/v1/auth/signup', { email, password }); +export async function apiSignup(email: string, password: string, name: string): Promise { + return authFetch('/api/v1/auth/signup', { email, password, name }); } async function authFetch(url: string, body: Record): Promise { diff --git a/frontend/src/lib/api/team.ts b/frontend/src/lib/api/team.ts index 0fae217..a1ff935 100644 --- a/frontend/src/lib/api/team.ts +++ b/frontend/src/lib/api/team.ts @@ -2,6 +2,7 @@ import { apiFetch, type ApiResult } from '$lib/api/client'; export type TeamMember = { user_id: string; + name: string; email: string; role: 'owner' | 'admin' | 'member'; joined_at: string; @@ -42,7 +43,7 @@ export async function createTeam(name: string): Promise> export async function switchTeam( teamId: string -): Promise> { +): Promise> { return apiFetch('POST', '/api/v1/auth/switch-team', { team_id: teamId }); } diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index 86325df..b42cf52 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -4,7 +4,8 @@ const STORAGE_KEYS = { token: 'wrenn_token', userId: 'wrenn_user_id', teamId: 'wrenn_team_id', - email: 'wrenn_email' + email: 'wrenn_email', + name: 'wrenn_name' } as const; function isTokenExpired(token: string): boolean { @@ -23,6 +24,7 @@ function createAuth() { let userId = $state(null); let teamId = $state(null); let email = $state(null); + let name = $state(null); let initialized = $state(false); // Initialize from localStorage synchronously at module load. @@ -33,6 +35,7 @@ function createAuth() { userId = localStorage.getItem(STORAGE_KEYS.userId); teamId = localStorage.getItem(STORAGE_KEYS.teamId); email = localStorage.getItem(STORAGE_KEYS.email); + name = localStorage.getItem(STORAGE_KEYS.name); } else if (stored) { // Expired — clean up. for (const key of Object.values(STORAGE_KEYS)) { @@ -57,6 +60,9 @@ function createAuth() { get email() { return email; }, + get name() { + return name; + }, get isAuthenticated() { return isAuthenticated; }, @@ -64,16 +70,18 @@ function createAuth() { return initialized; }, - login(data: { token: string; user_id: string; team_id: string; email: string }) { + login(data: { token: string; user_id: string; team_id: string; email: string; name: string }) { token = data.token; userId = data.user_id; teamId = data.team_id; email = data.email; + name = data.name; localStorage.setItem(STORAGE_KEYS.token, data.token); localStorage.setItem(STORAGE_KEYS.userId, data.user_id); localStorage.setItem(STORAGE_KEYS.teamId, data.team_id); localStorage.setItem(STORAGE_KEYS.email, data.email); + localStorage.setItem(STORAGE_KEYS.name, data.name); }, logout() { @@ -81,6 +89,7 @@ function createAuth() { userId = null; teamId = null; email = null; + name = null; for (const key of Object.values(STORAGE_KEYS)) { localStorage.removeItem(key); diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index b2fe99a..47feb54 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -27,7 +27,7 @@ let teamPopoverOpen = $state(false); let currentTeamName = $derived(teamsStore.list.find((t) => t.id === auth.teamId)?.name ?? ''); - let userName = $derived(auth.email ?? ''); + let userName = $derived(auth.name || auth.email || ''); // Create team dialog let showCreateTeam = $state(false); diff --git a/frontend/src/lib/teams.svelte.ts b/frontend/src/lib/teams.svelte.ts index f9596d3..01bdbb3 100644 --- a/frontend/src/lib/teams.svelte.ts +++ b/frontend/src/lib/teams.svelte.ts @@ -24,6 +24,10 @@ function createTeamsStore() { set(newTeams: TeamWithRole[]) { teams = newTeams; loaded = true; + }, + reset() { + teams = []; + loaded = false; } }; } diff --git a/frontend/src/routes/auth/github/callback/+page.svelte b/frontend/src/routes/auth/github/callback/+page.svelte index 692427d..8ca4472 100644 --- a/frontend/src/routes/auth/github/callback/+page.svelte +++ b/frontend/src/routes/auth/github/callback/+page.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { auth } from '$lib/auth.svelte'; + import { teams } from '$lib/teams.svelte'; const params = $page.url.searchParams; const error = params.get('error'); @@ -13,9 +14,11 @@ const userId = params.get('user_id'); const teamId = params.get('team_id'); const email = params.get('email'); + const name = params.get('name') ?? ''; if (token && userId && teamId && email) { - auth.login({ token, user_id: userId, team_id: teamId, email }); + teams.reset(); + auth.login({ token, user_id: userId, team_id: teamId, email, name }); goto('/dashboard'); } else { goto('/login?error=missing_token'); diff --git a/frontend/src/routes/dashboard/capsules/+page.svelte b/frontend/src/routes/dashboard/capsules/+page.svelte index 13cf682..d9818e2 100644 --- a/frontend/src/routes/dashboard/capsules/+page.svelte +++ b/frontend/src/routes/dashboard/capsules/+page.svelte @@ -2,6 +2,7 @@ import Sidebar from '$lib/components/Sidebar.svelte'; import { onMount } from 'svelte'; import { toast } from '$lib/toast.svelte'; + import { auth } from '$lib/auth.svelte'; import { listCapsules, createCapsule, @@ -440,7 +441,9 @@