feat(T01): auth + user management + RBAC + base frontend + i18n
- Backend: FastAPI, JWT auth (HS256), bcrypt, RBAC middleware - User CRUD (admin-only), soft-delete, pagination - Pydantic BaseSettings config, async SQLAlchemy 2.0 - 50 backend tests, 88% coverage - Frontend: Next.js 14 App Router, Tailwind, design tokens - Login page, auth context, API client with auto-refresh - i18n: next-intl, DE/EN (31 keys each) - 6 base UI components (Button, Input, Card, Table, Modal, Toast) - 12 frontend tests, npm build success
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
login as apiLogin,
|
||||
getCurrentUser,
|
||||
setTokens,
|
||||
clearTokens,
|
||||
isAuthenticated,
|
||||
type UserResponse,
|
||||
type ApiError,
|
||||
} from './api';
|
||||
|
||||
export interface AuthState {
|
||||
user: UserResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const [state, setState] = useState<AuthState>({
|
||||
user: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
if (!isAuthenticated()) {
|
||||
setState({ user: null, loading: false, error: null });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
setState({ user, loading: false, error: null });
|
||||
} catch {
|
||||
clearTokens();
|
||||
setState({ user: null, loading: false, error: 'Session expired' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
}, [fetchUser]);
|
||||
|
||||
const login = useCallback(async (email: string, password: string) => {
|
||||
setState({ user: null, loading: true, error: null });
|
||||
try {
|
||||
const tokens = await apiLogin(email, password);
|
||||
setTokens(tokens);
|
||||
const user = await getCurrentUser();
|
||||
setState({ user, loading: false, error: null });
|
||||
return user;
|
||||
} catch (err) {
|
||||
const message = (err as ApiError)?.error?.message || 'Login failed';
|
||||
setState({ user: null, loading: false, error: message });
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearTokens();
|
||||
setState({ user: null, loading: false, error: null });
|
||||
}, []);
|
||||
|
||||
return { ...state, login, logout, refresh: fetchUser };
|
||||
}
|
||||
|
||||
export function useRequireAuth() {
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth.loading && !auth.user) {
|
||||
router.push('/login');
|
||||
}
|
||||
}, [auth.loading, auth.user, router]);
|
||||
|
||||
return auth;
|
||||
}
|
||||
Reference in New Issue
Block a user