nomadweb/frontend/src/lib/auth.tsx
2026-09-04 10:30:11 -05:00

147 lines
4.3 KiB
TypeScript

"use client";
import { createContext, useContext, useEffect, useState, useCallback } from "react";
import { api } from "@/lib/api";
import { syncPlanOnLogin } from "@/lib/planSync";
import type { UserProfile } from "@/lib/types";
interface AuthCtx {
user: UserProfile | null;
token: string | null;
favorites: string[];
planSyncStatus: "idle" | "syncing" | "synced" | "offline";
login: (email: string, password: string) => Promise<boolean>;
register: (email: string, password: string, name: string) => Promise<boolean>;
demoLogin: () => Promise<boolean>;
googleLogin: (idToken: string) => Promise<boolean>;
logout: () => void;
refreshUser: () => Promise<void>;
toggleFavorite: (slug: string) => Promise<void>;
isFavorite: (slug: string) => boolean;
}
const AuthContext = createContext<AuthCtx | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<UserProfile | null>(null);
const [token, setToken] = useState<string | null>(null);
const [favorites, setFavorites] = useState<string[]>([]);
const [planSyncStatus, setPlanSyncStatus] = useState<"idle" | "syncing" | "synced" | "offline">("idle");
const loadFavorites = useCallback(async (t: string) => {
try {
const data = await api.getFavorites(t);
setFavorites(data.slugs);
} catch { /* ignore */ }
}, []);
const reconcilePlan = useCallback(async (t: string) => {
setPlanSyncStatus("syncing");
try {
await syncPlanOnLogin(t);
setPlanSyncStatus("synced");
} catch {
setPlanSyncStatus("offline");
}
}, []);
useEffect(() => {
const saved = localStorage.getItem("nomad-token");
const savedUser = localStorage.getItem("nomad-user");
if (saved && savedUser) {
setToken(saved);
setUser(JSON.parse(savedUser));
loadFavorites(saved);
void reconcilePlan(saved);
}
}, [loadFavorites, reconcilePlan]);
const persist = async (t: string, u: UserProfile) => {
setToken(t);
setUser(u);
localStorage.setItem("nomad-token", t);
localStorage.setItem("nomad-user", JSON.stringify(u));
await loadFavorites(t);
await reconcilePlan(t);
};
const login = async (email: string, password: string) => {
try {
const data = await api.login(email, password);
await persist(data.token, data.user);
return true;
} catch { return false; }
};
const register = async (email: string, password: string, name: string) => {
try {
const data = await api.register(email, password, name);
await persist(data.token, data.user);
return true;
} catch { return false; }
};
const demoLogin = async () => {
try {
const data = await api.demoLogin();
await persist(data.token, data.user);
return true;
} catch { return false; }
};
const googleLogin = async (idToken: string) => {
try {
const data = await api.googleLogin(idToken);
await persist(data.token, data.user);
return true;
} catch { return false; }
};
const logout = () => {
setToken(null);
setUser(null);
setFavorites([]);
setPlanSyncStatus("idle");
localStorage.removeItem("nomad-token");
localStorage.removeItem("nomad-user");
};
const refreshUser = async () => {
const t = token || localStorage.getItem("nomad-token");
if (!t) return;
try {
const me = await api.getMe(t);
setUser(me as UserProfile);
localStorage.setItem("nomad-user", JSON.stringify(me));
} catch {
try {
const saved = localStorage.getItem("nomad-user");
if (saved) setUser(JSON.parse(saved));
} catch { /* ignore */ }
}
};
const toggleFavorite = async (slug: string) => {
if (!token) return;
try {
const data = await api.toggleFavorite(slug, token);
setFavorites(data.slugs);
} catch { /* ignore */ }
};
return (
<AuthContext.Provider value={{
user, token, favorites, planSyncStatus, login, register, demoLogin, googleLogin, logout, refreshUser,
toggleFavorite, isFavorite: (slug) => favorites.includes(slug),
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}