Core Integration: - Create API client with TypeScript types for all endpoints - Implement authentication context provider for user state management - Add protected route component for dashboard access control - Connect login/register pages to backend authentication endpoints - Implement user session persistence with localStorage tokens Authentication: - Login page now connects to /api/auth/login endpoint - Register page connects to /api/auth/register with validation - Password strength requirements (min 8 chars) - Form validation and error handling - Automatic redirect to dashboard on successful auth - Logout functionality with session cleanup Protected Routes: - Dashboard pages require authentication - Non-authenticated users redirected to login - Loading spinner during auth verification - User name displayed in dashboard header - Proper session management Election/Vote APIs: - Dashboard fetches active elections from /api/elections/active - Display real election data with candidates count - Handle loading and error states - Skeleton loaders for better UX Type Safety: - Full TypeScript interfaces for all API responses - Proper error handling with try-catch blocks - API response types: AuthToken, VoterProfile, Election, Candidate, Vote, VoteHistory Environment: - API URL configurable via NEXT_PUBLIC_API_URL env variable - Default to http://localhost:8000 for local development 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
244 lines
5.2 KiB
TypeScript
244 lines
5.2 KiB
TypeScript
/**
|
|
* API Client for E-Voting Backend
|
|
* Handles all communication with the FastAPI backend
|
|
*/
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
|
|
|
export interface ApiResponse<T> {
|
|
data?: T
|
|
error?: string
|
|
status: number
|
|
}
|
|
|
|
export interface AuthToken {
|
|
access_token: string
|
|
expires_in: number
|
|
id: number
|
|
email: string
|
|
first_name: string
|
|
last_name: string
|
|
}
|
|
|
|
export interface VoterProfile {
|
|
id: number
|
|
email: string
|
|
first_name: string
|
|
last_name: string
|
|
created_at: string
|
|
}
|
|
|
|
export interface Election {
|
|
id: number
|
|
name: string
|
|
description: string
|
|
start_date: string
|
|
end_date: string
|
|
is_active: boolean
|
|
results_published: boolean
|
|
candidates: Candidate[]
|
|
}
|
|
|
|
export interface Candidate {
|
|
id: number
|
|
name: string
|
|
description: string
|
|
election_id: number
|
|
}
|
|
|
|
export interface Vote {
|
|
id: number
|
|
election_id: number
|
|
candidate_id: number
|
|
voter_id: number
|
|
ballot_hash: string
|
|
timestamp: string
|
|
}
|
|
|
|
export interface VoteHistory {
|
|
vote_id: number
|
|
election_id: number
|
|
election_name: string
|
|
candidate_name: string
|
|
vote_date: string
|
|
election_status: "active" | "closed" | "upcoming"
|
|
}
|
|
|
|
/**
|
|
* Get stored authentication token from localStorage
|
|
*/
|
|
export function getAuthToken(): string | null {
|
|
if (typeof window === "undefined") return null
|
|
return localStorage.getItem("auth_token")
|
|
}
|
|
|
|
/**
|
|
* Store authentication token in localStorage
|
|
*/
|
|
export function setAuthToken(token: string): void {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem("auth_token", token)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove authentication token from localStorage
|
|
*/
|
|
export function clearAuthToken(): void {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.removeItem("auth_token")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make API request with authentication
|
|
*/
|
|
async function apiRequest<T>(
|
|
endpoint: string,
|
|
options: RequestInit & { skipAuth?: boolean } = {}
|
|
): Promise<ApiResponse<T>> {
|
|
const { skipAuth = false, ...fetchOptions } = options
|
|
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
...((fetchOptions.headers as Record<string, string>) || {}),
|
|
}
|
|
|
|
// Add authentication token if available
|
|
if (!skipAuth) {
|
|
const token = getAuthToken()
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`
|
|
}
|
|
}
|
|
|
|
const response = await fetch(`${API_URL}${endpoint}`, {
|
|
...fetchOptions,
|
|
headers,
|
|
})
|
|
|
|
if (response.status === 401) {
|
|
// Token expired or invalid
|
|
clearAuthToken()
|
|
throw new Error("Unauthorized - please login again")
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}))
|
|
throw new Error(error.detail || `HTTP ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
return { data, status: response.status }
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error"
|
|
return { error: message, status: 500 }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authentication APIs
|
|
*/
|
|
export const authApi = {
|
|
async register(email: string, password: string, firstName: string, lastName: string) {
|
|
return apiRequest<AuthToken>("/api/auth/register", {
|
|
method: "POST",
|
|
skipAuth: true,
|
|
body: JSON.stringify({
|
|
email,
|
|
password,
|
|
first_name: firstName,
|
|
last_name: lastName,
|
|
}),
|
|
})
|
|
},
|
|
|
|
async login(email: string, password: string) {
|
|
return apiRequest<AuthToken>("/api/auth/login", {
|
|
method: "POST",
|
|
skipAuth: true,
|
|
body: JSON.stringify({ email, password }),
|
|
})
|
|
},
|
|
|
|
async getProfile() {
|
|
return apiRequest<VoterProfile>("/api/auth/profile")
|
|
},
|
|
|
|
logout() {
|
|
clearAuthToken()
|
|
return { data: null, status: 200 }
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Elections APIs
|
|
*/
|
|
export const electionsApi = {
|
|
async getActive() {
|
|
return apiRequest<Election[]>("/api/elections/active")
|
|
},
|
|
|
|
async getUpcoming() {
|
|
return apiRequest<Election[]>("/api/elections/upcoming")
|
|
},
|
|
|
|
async getCompleted() {
|
|
return apiRequest<Election[]>("/api/elections/completed")
|
|
},
|
|
|
|
async getById(id: number) {
|
|
return apiRequest<Election>(`/api/elections/${id}`)
|
|
},
|
|
|
|
async getCandidates(electionId: number) {
|
|
return apiRequest<Candidate[]>(`/api/elections/${electionId}/candidates`)
|
|
},
|
|
|
|
async getResults(electionId: number) {
|
|
return apiRequest<any>(`/api/elections/${electionId}/results`)
|
|
},
|
|
|
|
async publishResults(electionId: number) {
|
|
return apiRequest<any>(`/api/elections/${electionId}/publish-results`, {
|
|
method: "POST",
|
|
})
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Votes APIs
|
|
*/
|
|
export const votesApi = {
|
|
async submitVote(electionId: number, choix: string) {
|
|
return apiRequest<Vote>("/api/votes", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
election_id: electionId,
|
|
choix: choix,
|
|
}),
|
|
})
|
|
},
|
|
|
|
async getStatus(electionId: number) {
|
|
return apiRequest<{ has_voted: boolean }>(`/api/votes/status?election_id=${electionId}`)
|
|
},
|
|
|
|
async getHistory() {
|
|
return apiRequest<VoteHistory[]>("/api/votes/history")
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Health check
|
|
*/
|
|
export async function healthCheck() {
|
|
try {
|
|
const response = await fetch(`${API_URL}/health`)
|
|
return response.ok
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|