192 lines
4.7 KiB
TypeScript
192 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
ReactNode,
|
|
} from "react";
|
|
|
|
const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
|
|
|
type AuthUser = {
|
|
email: string;
|
|
displayName?: string | null;
|
|
role: string;
|
|
} | null;
|
|
|
|
type AuthContextValue = {
|
|
user: AuthUser;
|
|
token: string | null;
|
|
loading: boolean; // covers initial hydrate + active auth requests
|
|
login: (params: { email: string; password: string }) => Promise<void>;
|
|
register: (params: {
|
|
email: string;
|
|
password: string;
|
|
displayName?: string;
|
|
}) => Promise<void>;
|
|
logout: () => void;
|
|
getAuthHeaders: () => HeadersInit;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
|
|
const TOKEN_KEY = "ballistic_auth_token";
|
|
const USER_KEY = "ballistic_auth_user";
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<AuthUser>(null);
|
|
const [token, setToken] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true); // start true while we hydrate + reuse for calls
|
|
|
|
// hydrate from localStorage
|
|
useEffect(() => {
|
|
try {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const storedToken = window.localStorage.getItem(TOKEN_KEY);
|
|
const storedUser = window.localStorage.getItem(USER_KEY);
|
|
|
|
if (storedToken && storedUser) {
|
|
setToken(storedToken);
|
|
setUser(JSON.parse(storedUser));
|
|
}
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const setSession = useCallback((newToken: string, newUser: AuthUser) => {
|
|
setToken(newToken);
|
|
setUser(newUser);
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.localStorage.setItem(TOKEN_KEY, newToken);
|
|
window.localStorage.setItem(USER_KEY, JSON.stringify(newUser));
|
|
}
|
|
}, []);
|
|
|
|
const clearSession = useCallback(() => {
|
|
setToken(null);
|
|
setUser(null);
|
|
if (typeof window !== "undefined") {
|
|
window.localStorage.removeItem(TOKEN_KEY);
|
|
window.localStorage.removeItem(USER_KEY);
|
|
}
|
|
}, []);
|
|
|
|
const register = useCallback(
|
|
async (params: { email: string; password: string; displayName?: string }) => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: params.email,
|
|
password: params.password,
|
|
displayName: params.displayName,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(text || "Failed to create account");
|
|
}
|
|
|
|
const data: {
|
|
token: string;
|
|
email: string;
|
|
displayName?: string;
|
|
role: string;
|
|
} = await res.json();
|
|
|
|
setSession(data.token, {
|
|
email: data.email,
|
|
displayName: data.displayName ?? null,
|
|
role: data.role,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[setSession],
|
|
);
|
|
|
|
const login = useCallback(
|
|
async (params: { email: string; password: string }) => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: params.email,
|
|
password: params.password,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(text || "Failed to log in");
|
|
}
|
|
|
|
const data: {
|
|
token: string;
|
|
email: string;
|
|
displayName?: string;
|
|
role: string;
|
|
} = await res.json();
|
|
|
|
setSession(data.token, {
|
|
email: data.email,
|
|
displayName: data.displayName ?? null,
|
|
role: data.role,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[setSession],
|
|
);
|
|
|
|
const logout = useCallback(() => {
|
|
clearSession();
|
|
}, [clearSession]);
|
|
|
|
const getAuthHeaders = useCallback((): HeadersInit => {
|
|
if (!token) return {};
|
|
return {
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
}, [token]);
|
|
|
|
const value: AuthContextValue = {
|
|
user,
|
|
token,
|
|
loading,
|
|
login,
|
|
register,
|
|
logout,
|
|
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;
|
|
} |