ACE / services /authService.ts
Severian's picture
Upload 27 files
159f9c9 verified
// 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'