Refactor config, auth, and storage modules with runtime validation and encryption
- Centralized and refactored configuration management (`config`) with runtime validation for environment variables. - Introduced utilities for secure token storage, including AES-GCM encryption and fallback handling. - Enhanced `authStore` state management with token validation, secure persistence, and initialization from storage. - Modularized authentication utilities and updated export structure for better maintainability. - Improved error handling, input validation, and added detailed comments for enhanced clarity.
This commit is contained in:
77
frontend/src/stores/authStore.old.ts
Executable file
77
frontend/src/stores/authStore.old.ts
Executable file
@@ -0,0 +1,77 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
// User type - will be replaced with generated types later
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name?: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
organization_id?: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
// State
|
||||
user: User | null;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
|
||||
// Actions
|
||||
setUser: (user: User) => void;
|
||||
setTokens: (accessToken: string, refreshToken: string) => void;
|
||||
setAuth: (user: User, accessToken: string, refreshToken: string) => void;
|
||||
clearAuth: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
|
||||
// Actions
|
||||
setUser: (user) =>
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
|
||||
setTokens: (accessToken, refreshToken) =>
|
||||
set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
}),
|
||||
|
||||
setAuth: (user, accessToken, refreshToken) =>
|
||||
set({
|
||||
user,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
|
||||
clearAuth: () =>
|
||||
set({
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage', // localStorage key
|
||||
partialize: (state) => ({
|
||||
// Only persist these fields
|
||||
user: state.user,
|
||||
accessToken: state.accessToken,
|
||||
refreshToken: state.refreshToken,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
207
frontend/src/stores/authStore.ts
Executable file → Normal file
207
frontend/src/stores/authStore.ts
Executable file → Normal file
@@ -1,8 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
/**
|
||||
* Authentication Store - Zustand with secure token storage
|
||||
* Implements proper state management with validation
|
||||
*/
|
||||
|
||||
// User type - will be replaced with generated types later
|
||||
interface User {
|
||||
import { create } from 'zustand';
|
||||
import { saveTokens, getTokens, clearTokens } from '@/lib/auth/storage';
|
||||
|
||||
// User type - will be replaced with generated types in Phase 2
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name?: string;
|
||||
@@ -17,61 +22,163 @@ interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
tokenExpiresAt: number | null; // Unix timestamp
|
||||
|
||||
// Actions
|
||||
setAuth: (user: User, accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
|
||||
setTokens: (accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
|
||||
setUser: (user: User) => void;
|
||||
setTokens: (accessToken: string, refreshToken: string) => void;
|
||||
setAuth: (user: User, accessToken: string, refreshToken: string) => void;
|
||||
clearAuth: () => void;
|
||||
clearAuth: () => Promise<void>;
|
||||
loadAuthFromStorage: () => Promise<void>;
|
||||
isTokenExpired: () => boolean;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
/**
|
||||
* Validate token format (basic JWT structure check)
|
||||
*/
|
||||
function isValidToken(token: string): boolean {
|
||||
if (!token || typeof token !== 'string') return false;
|
||||
// JWT format: header.payload.signature
|
||||
const parts = token.split('.');
|
||||
return parts.length === 3 && parts.every((part) => part.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate token expiry timestamp
|
||||
* @param expiresIn - Seconds until expiry
|
||||
* @returns Unix timestamp
|
||||
*/
|
||||
function calculateExpiry(expiresIn?: number): number {
|
||||
// Default to 15 minutes if not provided
|
||||
const seconds = expiresIn || 900;
|
||||
return Date.now() + seconds * 1000;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
// Initial state
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: true, // Start as loading to check stored tokens
|
||||
tokenExpiresAt: null,
|
||||
|
||||
// Set complete auth state (user + tokens)
|
||||
setAuth: async (user, accessToken, refreshToken, expiresIn) => {
|
||||
// Validate inputs
|
||||
if (!user || !user.id || !user.email) {
|
||||
throw new Error('Invalid user object');
|
||||
}
|
||||
|
||||
if (!isValidToken(accessToken) || !isValidToken(refreshToken)) {
|
||||
throw new Error('Invalid token format');
|
||||
}
|
||||
|
||||
// Store tokens securely
|
||||
try {
|
||||
await saveTokens({ accessToken, refreshToken });
|
||||
|
||||
set({
|
||||
user,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
tokenExpiresAt: calculateExpiry(expiresIn),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save auth state:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Update tokens only (for refresh flow)
|
||||
setTokens: async (accessToken, refreshToken, expiresIn) => {
|
||||
if (!isValidToken(accessToken) || !isValidToken(refreshToken)) {
|
||||
throw new Error('Invalid token format');
|
||||
}
|
||||
|
||||
try {
|
||||
await saveTokens({ accessToken, refreshToken });
|
||||
|
||||
set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpiresAt: calculateExpiry(expiresIn),
|
||||
// Keep existing user and authenticated state
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Update user only
|
||||
setUser: (user) => {
|
||||
if (!user || !user.id || !user.email) {
|
||||
throw new Error('Invalid user object');
|
||||
}
|
||||
|
||||
set({ user });
|
||||
},
|
||||
|
||||
// Clear all auth state
|
||||
clearAuth: async () => {
|
||||
try {
|
||||
await clearTokens();
|
||||
} catch (error) {
|
||||
console.error('Failed to clear tokens:', error);
|
||||
}
|
||||
|
||||
set({
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
tokenExpiresAt: null,
|
||||
});
|
||||
},
|
||||
|
||||
// Actions
|
||||
setUser: (user) =>
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
// Load auth from storage on app start
|
||||
loadAuthFromStorage: async () => {
|
||||
try {
|
||||
const tokens = await getTokens();
|
||||
|
||||
setTokens: (accessToken, refreshToken) =>
|
||||
set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
}),
|
||||
|
||||
setAuth: (user, accessToken, refreshToken) =>
|
||||
set({
|
||||
user,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
isAuthenticated: true,
|
||||
}),
|
||||
|
||||
clearAuth: () =>
|
||||
set({
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage', // localStorage key
|
||||
partialize: (state) => ({
|
||||
// Only persist these fields
|
||||
user: state.user,
|
||||
accessToken: state.accessToken,
|
||||
refreshToken: state.refreshToken,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
}),
|
||||
if (tokens?.accessToken && tokens?.refreshToken) {
|
||||
// Validate token format
|
||||
if (isValidToken(tokens.accessToken) && isValidToken(tokens.refreshToken)) {
|
||||
set({
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
// User will be loaded separately via API call
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load auth from storage:', error);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// No valid tokens found
|
||||
set({ isLoading: false });
|
||||
},
|
||||
|
||||
// Check if current token is expired
|
||||
isTokenExpired: () => {
|
||||
const { tokenExpiresAt } = get();
|
||||
if (!tokenExpiresAt) return true;
|
||||
return Date.now() >= tokenExpiresAt;
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Initialize auth store from storage
|
||||
* Call this on app startup
|
||||
*/
|
||||
export async function initializeAuth(): Promise<void> {
|
||||
await useAuthStore.getState().loadAuthFromStorage();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Zustand stores
|
||||
// Examples: authStore, uiStore, etc.
|
||||
|
||||
export { useAuthStore } from './authStore';
|
||||
export { useAuthStore, initializeAuth, type User } from './authStore';
|
||||
|
||||
Reference in New Issue
Block a user