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:
Marko (Hermes Implementer)
2026-05-26 00:54:27 +00:00
parent b91b7c364e
commit 6867a91b67
28 changed files with 3313 additions and 55 deletions
+139
View File
@@ -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;
}