feat: add user authentication with separate login and register pages
- Custom User model with email as unique identifier (AUTH_USER_MODEL) - POST /api/auth/register/ with email validation, password min 8 chars, duplicate rejection - POST /api/auth/login/ returning JWT (access + refresh) tokens - Passwords hashed via Django's make_password - React LoginPage and RegisterPage with form validation - AuthContext with useReducer for auth state management - Axios API client with JWT token injection - TypeScript conversion of frontend scaffold
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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 {
|
||||
const user = await registerUser(payload);
|
||||
dispatch({ type: "AUTH_SUCCESS", payload: user });
|
||||
} 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;
|
||||
}
|
||||
Reference in New Issue
Block a user