Spaces:
Sleeping
Sleeping
// Types | |
export interface User { | |
id: string | |
name: string | |
email: string | |
} | |
export interface AuthState { | |
user: User | null | |
isAuthenticated: boolean | |
isLoading: boolean | |
} | |
export interface LoginCredentials { | |
username: string | |
password: string | |
} | |
// Environment-based configuration with fallbacks | |
const getAuthConfig = () => { | |
// Get credentials from environment variables using type assertion | |
const env = (import.meta as any).env | |
const usernameHash = env.VITE_AUTH_USERNAME_HASH | |
const passwordHash = env.VITE_AUTH_PASSWORD_HASH | |
// Validate that required environment variables are present | |
if (!usernameHash || !passwordHash) { | |
console.error('Missing required authentication environment variables!') | |
console.error('Please set VITE_AUTH_USERNAME_HASH and VITE_AUTH_PASSWORD_HASH in your .env file') | |
throw new Error('Authentication configuration missing. Check environment variables.') | |
} | |
return { | |
username: usernameHash, | |
password: passwordHash | |
} | |
} | |
const getUserConfig = (): User => { | |
const env = (import.meta as any).env | |
return { | |
id: env.VITE_AUTH_USER_ID || "1", | |
name: env.VITE_AUTH_USER_NAME || "ACE Admin", | |
email: env.VITE_AUTH_USER_EMAIL || "admin@aceui.com" | |
} | |
} | |
// Helper function to hash input with SHA-256 using Web Crypto API (browser-compatible) | |
async function hashInput(input: string): Promise<string> { | |
const encoder = new TextEncoder() | |
const data = encoder.encode(input) | |
const hashBuffer = await crypto.subtle.digest('SHA-256', data) | |
const hashArray = Array.from(new Uint8Array(hashBuffer)) | |
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') | |
return hashHex | |
} | |
// JWT-like token management (simplified for client-side) | |
const TOKEN_KEY = 'ace_auth_token' | |
const USER_KEY = 'ace_auth_user' | |
const TOKEN_EXPIRY_HOURS = 24 | |
interface TokenData { | |
user: User | |
timestamp: number | |
expiresAt: number | |
} | |
class AuthService { | |
private listeners: Set<(authState: AuthState) => void> = new Set() | |
private currentState: AuthState = { | |
user: null, | |
isAuthenticated: false, | |
isLoading: true | |
} | |
constructor() { | |
this.checkExistingSession() | |
} | |
// Check for existing valid session on initialization | |
private checkExistingSession() { | |
try { | |
const token = localStorage.getItem(TOKEN_KEY) | |
const userData = localStorage.getItem(USER_KEY) | |
if (token && userData) { | |
const tokenData: TokenData = JSON.parse(token) | |
const user: User = JSON.parse(userData) | |
// Check if token is still valid | |
if (Date.now() < tokenData.expiresAt) { | |
this.updateState({ | |
user, | |
isAuthenticated: true, | |
isLoading: false | |
}) | |
return | |
} else { | |
// Token expired, clear storage | |
this.clearSession() | |
} | |
} | |
} catch (error) { | |
console.error('Error checking existing session:', error) | |
this.clearSession() | |
} | |
this.updateState({ | |
user: null, | |
isAuthenticated: false, | |
isLoading: false | |
}) | |
} | |
// Subscribe to auth state changes | |
subscribe(callback: (authState: AuthState) => void) { | |
this.listeners.add(callback) | |
// Immediately call with current state | |
callback(this.currentState) | |
// Return unsubscribe function | |
return () => { | |
this.listeners.delete(callback) | |
} | |
} | |
// Update state and notify listeners | |
private updateState(newState: AuthState) { | |
this.currentState = newState | |
this.listeners.forEach(callback => callback(newState)) | |
} | |
// Get current auth state | |
getState(): AuthState { | |
return this.currentState | |
} | |
// Login with credentials | |
async login(credentials: LoginCredentials): Promise<{ success: boolean; error?: string }> { | |
try { | |
this.updateState({ ...this.currentState, isLoading: true }) | |
// Simulate network delay for UX | |
await new Promise(resolve => setTimeout(resolve, 500)) | |
const hashedUsername = await hashInput(credentials.username) | |
const hashedPassword = await hashInput(credentials.password) | |
// Get auth configuration | |
const validCredentials = getAuthConfig() | |
const validUser = getUserConfig() | |
// Validate credentials | |
if (hashedUsername === validCredentials.username && | |
hashedPassword === validCredentials.password) { | |
// Create session token | |
const now = Date.now() | |
const expiresAt = now + (TOKEN_EXPIRY_HOURS * 60 * 60 * 1000) | |
const tokenData: TokenData = { | |
user: validUser, | |
timestamp: now, | |
expiresAt | |
} | |
// Store in localStorage | |
localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenData)) | |
localStorage.setItem(USER_KEY, JSON.stringify(validUser)) | |
this.updateState({ | |
user: validUser, | |
isAuthenticated: true, | |
isLoading: false | |
}) | |
return { success: true } | |
} else { | |
this.updateState({ | |
user: null, | |
isAuthenticated: false, | |
isLoading: false | |
}) | |
return { success: false, error: 'Invalid username or password' } | |
} | |
} catch (error) { | |
console.error('Login error:', error) | |
this.updateState({ | |
user: null, | |
isAuthenticated: false, | |
isLoading: false | |
}) | |
return { success: false, error: 'An error occurred during login' } | |
} | |
} | |
// Logout | |
async logout(): Promise<void> { | |
this.clearSession() | |
this.updateState({ | |
user: null, | |
isAuthenticated: false, | |
isLoading: false | |
}) | |
} | |
// Clear session data | |
private clearSession() { | |
localStorage.removeItem(TOKEN_KEY) | |
localStorage.removeItem(USER_KEY) | |
} | |
// Check if session is valid | |
isSessionValid(): boolean { | |
try { | |
const token = localStorage.getItem(TOKEN_KEY) | |
if (!token) return false | |
const tokenData: TokenData = JSON.parse(token) | |
return Date.now() < tokenData.expiresAt | |
} catch { | |
return false | |
} | |
} | |
} | |
// Create singleton instance | |
export const authService = new AuthService() | |
// React hook for using auth service | |
export function useAuth(): AuthState & { | |
login: (credentials: LoginCredentials) => Promise<{ success: boolean; error?: string }> | |
logout: () => Promise<void> | |
} { | |
const [authState, setAuthState] = React.useState<AuthState>(authService.getState()) | |
React.useEffect(() => { | |
const unsubscribe = authService.subscribe(setAuthState) | |
return unsubscribe | |
}, []) | |
return { | |
...authState, | |
login: authService.login.bind(authService), | |
logout: authService.logout.bind(authService) | |
} | |
} | |
// React import for the hook | |
import React from 'react' |