release: v0.5.0 - Authentication, API Keys & Advanced Features
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled

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:
Luca Sacchi Ricciardi
2026-04-07 19:22:47 +02:00
parent 9b9297b7dc
commit cc60ba17ea
49 changed files with 9847 additions and 176 deletions
+181
View File
@@ -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;
}