Files
job-tracker/web/src/contexts/AuthContext.tsx
T

142 lines
4.5 KiB
TypeScript

import {
createContext,
useContext,
useReducer,
useCallback,
type ReactNode,
type Dispatch,
} from "react";
import {
loginUser,
registerUser,
type UserProfile,
type LoginPayload,
type RegisterPayload,
} from "../services/authApi";
// ── State ──────────────────────────────────────────────────────────────
interface AuthState {
user: UserProfile | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
};
// ── Actions ────────────────────────────────────────────────────────────
type AuthAction =
| { type: "AUTH_START" }
| { type: "AUTH_SUCCESS"; payload: UserProfile }
| { type: "AUTH_FAILURE"; payload: string }
| { type: "LOGOUT" }
| { type: "CLEAR_ERROR" };
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case "AUTH_START":
return { ...state, isLoading: true, error: null };
case "AUTH_SUCCESS":
return {
...state,
isLoading: false,
isAuthenticated: true,
user: action.payload,
error: null,
};
case "AUTH_FAILURE":
return {
...state,
isLoading: false,
error: action.payload,
};
case "LOGOUT":
return { ...initialState };
case "CLEAR_ERROR":
return { ...state, error: null };
default:
return state;
}
}
// ── Context ────────────────────────────────────────────────────────────
interface AuthContextValue {
state: AuthState;
dispatch: Dispatch<AuthAction>;
login: (payload: LoginPayload) => Promise<void>;
register: (payload: RegisterPayload) => Promise<void>;
logout: () => void;
clearError: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
// ── Provider ───────────────────────────────────────────────────────────
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, initialState);
const login = useCallback(async (payload: LoginPayload) => {
dispatch({ type: "AUTH_START" });
try {
const response = await loginUser(payload);
localStorage.setItem("access_token", response.access);
localStorage.setItem("refresh_token", response.refresh);
dispatch({ type: "AUTH_SUCCESS", payload: response.user });
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Login failed. Please try again.";
dispatch({ type: "AUTH_FAILURE", payload: message });
throw err;
}
}, []);
const register = useCallback(async (payload: RegisterPayload) => {
dispatch({ type: "AUTH_START" });
try {
await registerUser(payload);
// Registration succeeded — the page component handles redirect to /login.
// Set isLoading=false by clearing auth state (no auto-authentication).
dispatch({ type: "LOGOUT" });
} catch (err: unknown) {
const message =
err instanceof Error
? err.message
: "Registration failed. Please try again.";
dispatch({ type: "AUTH_FAILURE", payload: message });
throw err;
}
}, []);
const logout = useCallback(() => {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
dispatch({ type: "LOGOUT" });
}, []);
const clearError = useCallback(() => {
dispatch({ type: "CLEAR_ERROR" });
}, []);
return (
<AuthContext.Provider
value={{ state, dispatch, login, register, logout, clearError }}
>
{children}
</AuthContext.Provider>
);
}
// ── Hook ───────────────────────────────────────────────────────────────
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}