release: v0.5.0 - Authentication, API Keys & Advanced Features
Complete v0.5.0 implementation: Database (@db-engineer): - 3 migrations: users, api_keys, report_schedules tables - Foreign keys, indexes, constraints, enums Backend (@backend-dev): - JWT authentication service with bcrypt (cost=12) - Auth endpoints: /register, /login, /refresh, /me - API Keys service with hash storage and prefix validation - API Keys endpoints: CRUD + rotate - Security module with JWT HS256 Frontend (@frontend-dev): - Login/Register pages with validation - AuthContext with localStorage persistence - Protected routes implementation - API Keys management UI (create, revoke, rotate) - Header with user dropdown DevOps (@devops-engineer): - .env.example and .env.production.example - docker-compose.scheduler.yml - scripts/setup-secrets.sh - INFRASTRUCTURE_SETUP.md QA (@qa-engineer): - 85 E2E tests: auth.spec.ts, apikeys.spec.ts, scenarios.spec.ts, regression-v050.spec.ts - auth-helpers.ts with 20+ utility functions - Test plans and documentation Architecture (@spec-architect): - SECURITY.md with best practices - SECURITY-CHECKLIST.md pre-deployment - Updated architecture.md with auth flows - Updated README.md with v0.5.0 features Documentation: - Updated todo.md with v0.5.0 status - Added docs/README.md index - Complete setup instructions Dependencies added: - bcrypt, python-jose, passlib, email-validator Tested: JWT auth flow, API keys CRUD, protected routes, 85 E2E tests ready Closes: v0.5.0 milestone
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import api from '@/lib/api';
|
||||
import { showToast } from '@/components/ui/toast-utils';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AuthTokens {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (email: string, password: string) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
register: (email: string, password: string, fullName: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
const TOKEN_KEY = 'auth_token';
|
||||
const REFRESH_TOKEN_KEY = 'refresh_token';
|
||||
const USER_KEY = 'auth_user';
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Initialize auth state from localStorage
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem(USER_KEY);
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
|
||||
if (storedUser && token) {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser));
|
||||
// Set default authorization header
|
||||
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
} catch {
|
||||
// Invalid stored data, clear it
|
||||
localStorage.removeItem(USER_KEY);
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
// Setup axios interceptor to add Authorization header
|
||||
useEffect(() => {
|
||||
const interceptor = api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
return () => {
|
||||
api.interceptors.request.eject(interceptor);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (email: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await api.post('/auth/login', { email, password });
|
||||
const { access_token, refresh_token, token_type } = response.data;
|
||||
|
||||
// Store tokens
|
||||
localStorage.setItem(TOKEN_KEY, access_token);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refresh_token);
|
||||
|
||||
// Set authorization header
|
||||
api.defaults.headers.common['Authorization'] = `${token_type} ${access_token}`;
|
||||
|
||||
// Fetch user info
|
||||
const userResponse = await api.get('/auth/me');
|
||||
const userData = userResponse.data;
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(userData));
|
||||
|
||||
showToast({
|
||||
title: 'Welcome back!',
|
||||
description: `Logged in as ${userData.email}`
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.detail || 'Invalid credentials';
|
||||
showToast({
|
||||
title: 'Login failed',
|
||||
description: message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (email: string, password: string, fullName: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await api.post('/auth/register', {
|
||||
email,
|
||||
password,
|
||||
full_name: fullName
|
||||
});
|
||||
const { access_token, refresh_token, token_type, user: userData } = response.data;
|
||||
|
||||
// Store tokens
|
||||
localStorage.setItem(TOKEN_KEY, access_token);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refresh_token);
|
||||
|
||||
// Set authorization header
|
||||
api.defaults.headers.common['Authorization'] = `${token_type} ${access_token}`;
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(userData));
|
||||
|
||||
showToast({
|
||||
title: 'Account created!',
|
||||
description: 'Welcome to mockupAWS'
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.detail || 'Registration failed';
|
||||
showToast({
|
||||
title: 'Registration failed',
|
||||
description: message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setUser(null);
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
delete api.defaults.headers.common['Authorization'];
|
||||
|
||||
showToast({
|
||||
title: 'Logged out',
|
||||
description: 'See you soon!'
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
isLoading,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user