thread-master/apps/web/src/auth/AuthContext.tsx

80 lines
2.1 KiB
TypeScript
Raw Normal View History

2026-07-10 05:10:31 +00:00
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import type { Member } from "../domain/types";
import type { MemberProfilePatch } from "../data/repos";
import { useRepos } from "../data/DataContext";
type AuthContextValue = {
member: Member | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
reload: () => Promise<void>;
updateProfile: (patch: MemberProfilePatch) => Promise<Member>;
};
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const repos = useRepos();
const [member, setMember] = useState<Member | null>(null);
const [loading, setLoading] = useState(true);
const reload = useCallback(async () => {
// 已進過 app 後不要再把 loading=true否則 RequireAuth 會卸載整棵 /app
// 各頁本地 state如用量「全體」分頁會被重掛成預設。
try {
const me = await repos.auth.me();
setMember(me);
} finally {
setLoading(false);
}
}, [repos.auth]);
useEffect(() => {
void reload();
}, [reload]);
const login = useCallback(
async (email: string, password: string) => {
const { member: m } = await repos.auth.login(email, password);
setMember(m);
},
[repos.auth],
);
const logout = useCallback(async () => {
await repos.auth.logout();
setMember(null);
}, [repos.auth]);
const updateProfile = useCallback(
async (patch: MemberProfilePatch) => {
const next = await repos.auth.updateProfile(patch);
setMember(next);
return next;
},
[repos.auth],
);
const value = useMemo(
() => ({ member, loading, login, logout, reload, updateProfile }),
[member, loading, login, logout, reload, updateProfile],
);
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 AuthProvider");
return ctx;
}