128 lines
3.3 KiB
TypeScript
128 lines
3.3 KiB
TypeScript
import { createContext, useContext, useCallback } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
import { pb } from "@/lib/pocketbase";
|
|
import type { AuthRecord } from "pocketbase";
|
|
|
|
/**
|
|
* Represents the shape of the authenticated user object from PocketBase.
|
|
*/
|
|
/**
|
|
* Context value for authentication state and actions.
|
|
*/
|
|
export interface AuthContextValue {
|
|
user: AuthRecord | null;
|
|
isLoading: boolean;
|
|
login: (email: string, password: string) => Promise<void>;
|
|
signup: (
|
|
email: string,
|
|
password: string,
|
|
passwordConfirm: string,
|
|
) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
|
|
/**
|
|
* Fetches the currently authenticated user from PocketBase.
|
|
*/
|
|
async function fetchUser(): Promise<AuthRecord | null> {
|
|
if (pb.authStore.isValid) {
|
|
return pb.authStore.record;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Provider for authentication context, using TanStack Query for state management.
|
|
*/
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const queryClient = useQueryClient();
|
|
const { data: user, isLoading } = useQuery({
|
|
queryKey: ["auth", "user"],
|
|
queryFn: fetchUser,
|
|
});
|
|
|
|
const loginMutation = useMutation({
|
|
mutationFn: async ({
|
|
email,
|
|
password,
|
|
}: {
|
|
email: string;
|
|
password: string;
|
|
}) => {
|
|
await pb.collection("users").authWithPassword(email, password);
|
|
return fetchUser();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
|
|
},
|
|
});
|
|
|
|
const signupMutation = useMutation({
|
|
mutationFn: async ({
|
|
email,
|
|
password,
|
|
passwordConfirm,
|
|
}: {
|
|
email: string;
|
|
password: string;
|
|
passwordConfirm: string;
|
|
}) => {
|
|
await pb.collection("users").create({ email, password, passwordConfirm });
|
|
await pb.collection("users").authWithPassword(email, password);
|
|
return fetchUser();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
|
|
},
|
|
});
|
|
|
|
const logoutMutation = useMutation({
|
|
mutationFn: async () => {
|
|
pb.authStore.clear();
|
|
return null;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
|
|
},
|
|
});
|
|
|
|
const login = useCallback(
|
|
async (email: string, password: string) => {
|
|
await loginMutation.mutateAsync({ email, password });
|
|
},
|
|
[loginMutation],
|
|
);
|
|
|
|
const signup = useCallback(
|
|
async (email: string, password: string, passwordConfirm: string) => {
|
|
await signupMutation.mutateAsync({ email, password, passwordConfirm });
|
|
},
|
|
[signupMutation],
|
|
);
|
|
|
|
const logout = useCallback(async () => {
|
|
await logoutMutation.mutateAsync();
|
|
}, [logoutMutation]);
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{ user: user ?? null, isLoading, login, signup, logout }}
|
|
>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to access authentication context.
|
|
* Throws if used outside of AuthProvider.
|
|
*/
|
|
export function useAuth(): AuthContextValue {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
|
|
return ctx;
|
|
}
|