Implement authentication context and API utility functions for authenticated requests
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 28s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 28s
This commit is contained in:
82
src/contexts/AuthContext.tsx
Normal file
82
src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface AuthUser {
|
||||
steamId: string
|
||||
token: string
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: AuthUser | null
|
||||
token: string | null
|
||||
login: (steamId: string, token: string) => void
|
||||
logout: () => void
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
const TOKEN_KEY = 'auth_token'
|
||||
const USER_KEY = 'auth_user'
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null)
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
|
||||
// Load auth state from localStorage on mount
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem(TOKEN_KEY)
|
||||
const storedUser = localStorage.getItem(USER_KEY)
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(storedUser)
|
||||
setToken(storedToken)
|
||||
setUser(parsedUser)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stored user:', error)
|
||||
// Clear invalid data
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = (steamId: string, token: string) => {
|
||||
const authUser: AuthUser = { steamId, token }
|
||||
|
||||
// Store in state
|
||||
setUser(authUser)
|
||||
setToken(token)
|
||||
|
||||
// Store in localStorage
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(authUser))
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
// Clear state
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
|
||||
const isAuthenticated = !!token && !!user
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, login, logout, isAuthenticated }}>
|
||||
{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
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { apiGet } from '../utils/api'
|
||||
|
||||
// Response from the new /api/server/statistics/list endpoint
|
||||
interface ServerListResponse {
|
||||
@@ -42,10 +43,7 @@ export function ServerProvider({ children }: { children: ReactNode }) {
|
||||
const fetchServerData = async () => {
|
||||
try {
|
||||
// Fetch server list from the new API endpoint
|
||||
const response = await fetch('/api/server/statistics/list', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
const response = await apiGet('/api/server/statistics/list')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { apiPost } from '../utils/api'
|
||||
|
||||
interface ChatMessage {
|
||||
steamID64: string
|
||||
@@ -52,46 +53,22 @@ export function StatsProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
// Fetch statistics
|
||||
const [playersRes, connectsRes, playTimeRes, killsRes, chatsRes, topPlayersRes] = await Promise.all([
|
||||
fetch('/api/server/statistics/total-player-count', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
apiPost('/api/server/statistics/total-player-count', requestBody),
|
||||
apiPost('/api/server/statistics/total-connect-count', requestBody),
|
||||
apiPost('/api/server/statistics/total-play-time', requestBody),
|
||||
apiPost('/api/server/statistics/total-kill-count', {
|
||||
...requestBody,
|
||||
headshotOnly: false,
|
||||
weaponFilter: [],
|
||||
playerFilter: []
|
||||
}),
|
||||
fetch('/api/server/statistics/total-connect-count', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
apiPost('/api/server/statistics/recent-chat-message', {
|
||||
timeRangeStart: Date.now() - 3600000, // last hour
|
||||
timeRangeEnd: Date.now()
|
||||
}),
|
||||
fetch('/api/server/statistics/total-play-time', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
}),
|
||||
fetch('/api/server/statistics/total-kill-count', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...requestBody,
|
||||
headshotOnly: false,
|
||||
weaponFilter: [],
|
||||
playerFilter: []
|
||||
})
|
||||
}),
|
||||
fetch('/api/server/statistics/recent-chat-message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
timeRangeStart: Date.now() - 3600000, // last hour
|
||||
timeRangeEnd: Date.now()
|
||||
})
|
||||
}),
|
||||
fetch('/api/server/statistics/top-play-time', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
timeRangeStart: 0, // all time
|
||||
timeRangeEnd: Date.now()
|
||||
})
|
||||
apiPost('/api/server/statistics/top-play-time', {
|
||||
timeRangeStart: 0, // all time
|
||||
timeRangeEnd: Date.now()
|
||||
})
|
||||
])
|
||||
|
||||
|
||||
33
src/main.tsx
33
src/main.tsx
@@ -4,6 +4,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import './index.css'
|
||||
import './i18n'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import { ServerProvider } from './contexts/ServerContext'
|
||||
import { StatsProvider } from './contexts/StatsContext'
|
||||
import ScrollToTop from './components/ScrollToTop'
|
||||
@@ -17,21 +18,23 @@ import AuthCallback from './pages/AuthCallback.tsx'
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<StatsProvider>
|
||||
<ServerProvider>
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/friends" element={<Friends />} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/forum" element={<Forum />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ServerProvider>
|
||||
</StatsProvider>
|
||||
<AuthProvider>
|
||||
<StatsProvider>
|
||||
<ServerProvider>
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/friends" element={<Friends />} />
|
||||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/forum" element={<Forum />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ServerProvider>
|
||||
</StatsProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import '../App.css'
|
||||
|
||||
interface AuthStatus {
|
||||
@@ -14,15 +15,22 @@ function AuthCallback() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>({ status: null })
|
||||
const { login } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
// Parse query parameters
|
||||
const status = searchParams.get('status') as 'success' | 'failed' | 'error' | null
|
||||
const steamId = searchParams.get('steamId') || undefined
|
||||
const token = searchParams.get('token') || undefined
|
||||
const message = searchParams.get('message') || undefined
|
||||
|
||||
setAuthStatus({ status, steamId, message })
|
||||
|
||||
// Store JWT token on successful authentication
|
||||
if (status === 'success' && steamId && token) {
|
||||
login(steamId, token)
|
||||
}
|
||||
|
||||
// Auto-redirect to home after 5 seconds on success
|
||||
if (status === 'success') {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -30,7 +38,7 @@ function AuthCallback() {
|
||||
}, 5000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [searchParams, navigate])
|
||||
}, [searchParams, navigate, login])
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (authStatus.status) {
|
||||
|
||||
86
src/utils/api.ts
Normal file
86
src/utils/api.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* API utility for making authenticated requests
|
||||
* Automatically injects JWT token from localStorage into request headers
|
||||
*/
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
headers?: HeadersInit
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated API request
|
||||
* @param url - The URL to fetch
|
||||
* @param options - Fetch options (method, body, headers, etc.)
|
||||
* @returns Promise with the response
|
||||
*/
|
||||
export async function apiRequest(url: string, options: RequestOptions = {}): Promise<Response> {
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem('auth_token')
|
||||
|
||||
// Prepare headers
|
||||
const headers = new Headers(options.headers || {})
|
||||
|
||||
// Add authorization header if token exists
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
}
|
||||
|
||||
// Add content-type if not already set and body exists
|
||||
if (options.body && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
|
||||
// Handle 401 Unauthorized - token expired or invalid
|
||||
if (response.status === 401) {
|
||||
// Clear auth state
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_user')
|
||||
|
||||
// Optionally redirect to login or emit event
|
||||
window.dispatchEvent(new Event('auth:unauthorized'))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for GET requests
|
||||
*/
|
||||
export async function apiGet(url: string, options?: RequestOptions): Promise<Response> {
|
||||
return apiRequest(url, { ...options, method: 'GET' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for POST requests
|
||||
*/
|
||||
export async function apiPost(url: string, data?: unknown, options?: RequestOptions): Promise<Response> {
|
||||
return apiRequest(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for PUT requests
|
||||
*/
|
||||
export async function apiPut(url: string, data?: unknown, options?: RequestOptions): Promise<Response> {
|
||||
return apiRequest(url, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for DELETE requests
|
||||
*/
|
||||
export async function apiDelete(url: string, options?: RequestOptions): Promise<Response> {
|
||||
return apiRequest(url, { ...options, method: 'DELETE' })
|
||||
}
|
||||
Reference in New Issue
Block a user