1
0
forked from wrenn/wrenn

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
This commit is contained in:
2026-03-24 16:56:10 +06:00
parent aaeccd32ce
commit 3932bc056e
26 changed files with 228 additions and 77 deletions

View File

@ -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<AuthRes
return authFetch('/api/v1/auth/login', { email, password });
}
export async function apiSignup(email: string, password: string): Promise<AuthResult> {
return authFetch('/api/v1/auth/signup', { email, password });
export async function apiSignup(email: string, password: string, name: string): Promise<AuthResult> {
return authFetch('/api/v1/auth/signup', { email, password, name });
}
async function authFetch(url: string, body: Record<string, string>): Promise<AuthResult> {

View File

@ -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<ApiResult<TeamWithRole>>
export async function switchTeam(
teamId: string
): Promise<ApiResult<{ token: string; user_id: string; team_id: string; email: string }>> {
): Promise<ApiResult<{ token: string; user_id: string; team_id: string; email: string; name: string }>> {
return apiFetch('POST', '/api/v1/auth/switch-team', { team_id: teamId });
}

View File

@ -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<string | null>(null);
let teamId = $state<string | null>(null);
let email = $state<string | null>(null);
let name = $state<string | null>(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);

View File

@ -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);

View File

@ -24,6 +24,10 @@ function createTeamsStore() {
set(newTeams: TeamWithRole[]) {
teams = newTeams;
loaded = true;
},
reset() {
teams = [];
loaded = false;
}
};
}