Files
shadow-gunbuilder-ai-proto/context/AuthContext.tsx
T

243 lines
5.8 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
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;
};
type AuthContextValue = {
user: AuthUser;
loading: boolean;
login: (params: { email: string; password: string }) => Promise<void>;
register: (params: RegisterParams) => Promise<void>;
logout: () => void;
refreshUser: () => Promise<void>;
getAuthHeaders: () => HeadersInit;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
const USER_KEY = "ballistic_auth_user";
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser>(null);
const [loading, setLoading] = useState<boolean>(true);
/**
* Persist user to localStorage (for quick initial render)
*/
const persistUser = useCallback((nextUser: AuthUser) => {
if (typeof window === "undefined") return;
if (nextUser) {
window.localStorage.setItem(USER_KEY, JSON.stringify(nextUser));
} else {
window.localStorage.removeItem(USER_KEY);
}
}, []);
/**
* Hydrate from localStorage and refresh from server
*/
useEffect(() => {
if (typeof window === "undefined") return;
const storedUser = window.localStorage.getItem(USER_KEY);
// Quick hydration from cached user
if (storedUser) {
try {
const parsedUser = JSON.parse(storedUser);
setUser(parsedUser);
} catch {
window.localStorage.removeItem(USER_KEY);
}
}
// Verify session with server
fetch("/api/auth/me", { credentials: 'include' })
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Not authenticated");
})
.then((data) => {
const nextUser: AuthUser = {
uuid: data.uuid,
email: data.email,
username: data.username || null,
displayName: data.displayName || null,
role: data.role || "USER",
passwordSetAt: data.passwordSetAt || null,
};
setUser(nextUser);
persistUser(nextUser);
})
.catch(() => {
setUser(null);
persistUser(null);
})
.finally(() => {
setLoading(false);
});
}, [persistUser]);
const refreshUser = useCallback(async () => {
const res = await fetch("/api/auth/me", {
cache: "no-store",
credentials: 'include',
});
if (!res.ok) {
const text = await res.text().catch(() => res.statusText);
throw new Error(text || "Failed to refresh user");
}
const data = await res.json();
const nextUser: AuthUser = {
uuid: data.uuid,
email: data.email,
username: data.username || null,
displayName: data.displayName || null,
role: data.role || "USER",
passwordSetAt: data.passwordSetAt || null,
};
setUser(nextUser);
persistUser(nextUser);
}, [persistUser]);
const login = useCallback(
async ({ email, password }: { email: string; password: string }) => {
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: 'include',
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.error || "Login failed");
}
const data = await res.json();
const nextUser: AuthUser = data.user;
setUser(nextUser);
persistUser(nextUser);
} finally {
setLoading(false);
}
},
[persistUser]
);
const register = useCallback(
async ({
email,
password,
displayName,
acceptedTos,
tosVersion,
}: RegisterParams) => {
setLoading(true);
try {
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: 'include',
body: JSON.stringify({
email,
password,
displayName,
acceptedTos,
tosVersion,
}),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.error || "Registration failed");
}
const data = await res.json();
const nextUser: AuthUser = data.user;
setUser(nextUser);
persistUser(nextUser);
} finally {
setLoading(false);
}
},
[persistUser]
);
const logout = useCallback(() => {
setUser(null);
persistUser(null);
// Clear server session cookies
fetch("/api/auth/logout", {
method: "POST",
credentials: 'include',
}).catch(() => {});
}, [persistUser]);
const getAuthHeaders = useCallback((): HeadersInit => {
if (typeof window === "undefined") return {};
const token =
localStorage.getItem("token") ||
localStorage.getItem("jwt") ||
localStorage.getItem("accessToken");
return token ? { Authorization: `Bearer ${token}` } : {};
}, []);
const value: AuthContextValue = {
user,
loading,
login,
register,
logout,
refreshUser,
getAuthHeaders,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error("useAuth must be used within an AuthProvider");
}
return ctx;
}