account settings and account creation

This commit is contained in:
2025-12-27 20:01:17 -05:00
parent aa2dfb0407
commit 2202abe89f
21 changed files with 1848 additions and 718 deletions
+70 -28
View File
@@ -15,28 +15,32 @@ const API_BASE_URL =
type AuthUser = {
uuid: string;
email: string;
username?: string | null;
passwordSetAt?: string | null;
displayName?: string | null;
role: string;
} | null;
export type RegisterParams = {
email: string;
password: string;
displayName?: string;
acceptedTos: boolean;
tosVersion: string;
};
// 1) update the type
type AuthContextValue = {
user: AuthUser;
token: string | null;
loading: boolean;
login: (params: { email: string; password: string }) => Promise<void>;
register: (params: {
email: string;
password: string;
displayName?: string;
}) => Promise<void>;
register: (params: RegisterParams) => Promise<void>;
logout: () => void;
getAuthHeaders: () => HeadersInit;
setSession: (token: string, user: NonNullable<AuthUser>) => Promise<void>;
refreshMeAndSession: () => Promise<void>;
/**
* Used for non-password auth flows (ex: magic link).
* Persists session exactly like login/register.
*/
setSession: (token: string, user: NonNullable<AuthUser>) => void;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
@@ -110,24 +114,55 @@ export function AuthProvider({ children }: { children: ReactNode }) {
* Used by login/register/magic-link
*/
const setSession = useCallback(
(nextToken: string, nextUser: NonNullable<AuthUser>) => {
async (nextToken: string, nextUser: NonNullable<AuthUser>) => {
setToken(nextToken);
setUser(nextUser);
persistAuth(nextToken, nextUser);
// ✅ Make middleware / server aware of the session
fetch("/api/auth/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: nextToken,
role: nextUser.role,
}),
}).catch(() => {});
// ✅ await so middleware sees cookie before navigation
try {
await fetch("/api/auth/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: nextToken,
role: nextUser.role,
}),
});
} catch {
// ignore
}
},
[persistAuth]
);
const refreshMeAndSession = useCallback(async () => {
if (!token) return;
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
headers: { Authorization: `Bearer ${token}` },
cache: "no-store",
});
if (!res.ok) {
const text = await res.text().catch(() => res.statusText);
throw new Error(text || "Failed to refresh session");
}
const me = await res.json();
const nextUser = {
uuid: me.uuid,
email: me.email,
username: me.username || null,
displayName: me.displayName || null,
role: me.role || "USER",
passwordSetAt: me.passwordSetAt || null,
} as NonNullable<AuthUser>;
await setSession(token, nextUser);
}, [token, setSession]);
const login = useCallback(
async ({ email, password }: { email: string; password: string }) => {
setLoading(true);
@@ -152,10 +187,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
uuid: data.uuid,
email: data.email ?? email,
displayName: data.displayName ?? null,
passwordSetAt: data.passwordSetAt ?? null,
role: data.role ?? "USER",
} as AuthUser);
setSession(nextToken, nextUser as NonNullable<AuthUser>);
await setSession(nextToken, nextUser as NonNullable<AuthUser>);
} finally {
setLoading(false);
}
@@ -168,17 +204,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
email,
password,
displayName,
}: {
email: string;
password: string;
displayName?: string;
}) => {
acceptedTos,
tosVersion,
}: RegisterParams) => {
setLoading(true);
try {
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, displayName }),
body: JSON.stringify({
email,
password,
displayName,
acceptedTos,
tosVersion,
}),
});
if (!res.ok) {
@@ -195,10 +235,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
uuid: data.uuid,
email: data.email ?? email,
displayName: data.displayName ?? displayName ?? null,
passwordSetAt: data.passwordSetAt ?? null,
role: data.role ?? "USER",
} as AuthUser);
setSession(nextToken, nextUser as NonNullable<AuthUser>);
await setSession(nextToken, nextUser as NonNullable<AuthUser>);
} finally {
setLoading(false);
}
@@ -228,6 +269,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
logout,
getAuthHeaders,
setSession,
refreshMeAndSession
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;