132 lines
3.2 KiB
TypeScript
132 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
|
|
export type Customer = {
|
|
id: string;
|
|
email: string;
|
|
first_name: string | null;
|
|
last_name: string | null;
|
|
phone: string | null;
|
|
created_at: string;
|
|
};
|
|
|
|
type AuthState = {
|
|
customer: Customer | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
login: (email: string, password: string) => Promise<void>;
|
|
register: (data: {
|
|
email: string;
|
|
password: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
}) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
refreshCustomer: () => Promise<void>;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthState | null>(null);
|
|
|
|
async function apiJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(url, {
|
|
...init,
|
|
headers: { "Content-Type": "application/json", ...init?.headers },
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
const msg = body?.error ?? `Auth API error: ${res.status}`;
|
|
throw new Error(msg);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [customer, setCustomer] = useState<Customer | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// On mount: check if user is already authenticated via cookie
|
|
useEffect(() => {
|
|
async function checkAuth() {
|
|
try {
|
|
const data = await apiJson<{ customer: Customer }>("/api/account/me");
|
|
setCustomer(data.customer);
|
|
} catch {
|
|
setCustomer(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
checkAuth();
|
|
}, []);
|
|
|
|
const login = useCallback(async (email: string, password: string) => {
|
|
const data = await apiJson<{ customer: Customer }>("/api/account/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
setCustomer(data.customer);
|
|
}, []);
|
|
|
|
const register = useCallback(
|
|
async (regData: {
|
|
email: string;
|
|
password: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
}) => {
|
|
const data = await apiJson<{ customer: Customer }>(
|
|
"/api/account/register",
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify(regData),
|
|
},
|
|
);
|
|
setCustomer(data.customer);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const logout = useCallback(async () => {
|
|
await apiJson("/api/account/logout", { method: "POST" });
|
|
setCustomer(null);
|
|
}, []);
|
|
|
|
const refreshCustomer = useCallback(async () => {
|
|
try {
|
|
const data = await apiJson<{ customer: Customer }>("/api/account/me");
|
|
setCustomer(data.customer);
|
|
} catch {
|
|
setCustomer(null);
|
|
}
|
|
}, []);
|
|
|
|
const value = useMemo<AuthState>(
|
|
() => ({
|
|
customer,
|
|
isAuthenticated: !!customer,
|
|
isLoading,
|
|
login,
|
|
register,
|
|
logout,
|
|
refreshCustomer,
|
|
}),
|
|
[customer, isLoading, login, register, logout, refreshCustomer],
|
|
);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
export function useAuth() {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
|
|
return ctx;
|
|
}
|