142 lines
4.5 KiB
TypeScript
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;
|
|
}
|