# Frontend Steam 2FA Implementation Guide ## Overview This document provides complete implementation details for the frontend Steam 2FA system. The backend Steam 2FA implementation is complete and waiting for frontend integration. This guide includes all necessary code, components, and integration instructions. ## Problem Statement The current Steam 2FA backend implementation (`local/controller/steam_2fa.go`, `local/model/steam_2fa.go`) provides REST API endpoints but has no frontend to: 1. Poll for pending 2FA requests 2. Display 2FA prompts to users 3. Allow users to confirm/cancel 2FA operations Without frontend implementation, Steam operations requiring 2FA will hang indefinitely until timeout (5 minutes). ## Architecture Overview ### Backend API Endpoints (Already Implemented) - `GET /v1/steam2fa/pending` - Returns array of pending 2FA requests - `GET /v1/steam2fa/{id}` - Gets specific 2FA request details - `POST /v1/steam2fa/{id}/complete` - Marks 2FA request as completed - `POST /v1/steam2fa/{id}/cancel` - Cancels 2FA request ### Frontend Components (To Be Implemented) ``` src/ ├── models/ │ └── steam2fa.ts # TypeScript interfaces ├── stores/ │ └── steam2fa.ts # Svelte store for state management ├── components/ │ └── Steam2FANotification.svelte # Modal component └── lib/ └── api/ └── steam2fa.ts # API client functions ``` ## Implementation Details ### 1. TypeScript Interfaces (`src/models/steam2fa.ts`) ```typescript export type Steam2FAStatus = 'idle' | 'pending' | 'complete' | 'error'; export interface Steam2FARequest { id: string; status: Steam2FAStatus; message: string; requestTime: string; // ISO 8601 timestamp completedAt?: string; // ISO 8601 timestamp errorMsg?: string; serverId?: string; // UUID } export interface Steam2FAStore { requests: Steam2FARequest[]; isPolling: boolean; error: string | null; lastChecked: Date | null; } ``` ### 2. API Client (`src/lib/api/steam2fa.ts`) ```typescript import type { Steam2FARequest } from '../models/steam2fa'; const API_BASE = '/v1/steam2fa'; export class Steam2FAApi { /** * Get all pending 2FA requests */ static async getPendingRequests(): Promise { const response = await fetch(`${API_BASE}/pending`, { method: 'GET', credentials: 'include', // Include auth cookies }); if (!response.ok) { throw new Error(`Failed to fetch pending requests: ${response.statusText}`); } return response.json(); } /** * Get specific 2FA request by ID */ static async getRequest(id: string): Promise { const response = await fetch(`${API_BASE}/${id}`, { method: 'GET', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to fetch request ${id}: ${response.statusText}`); } return response.json(); } /** * Mark 2FA request as completed */ static async completeRequest(id: string): Promise { const response = await fetch(`${API_BASE}/${id}/complete`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to complete request ${id}: ${response.statusText}`); } return response.json(); } /** * Cancel 2FA request */ static async cancelRequest(id: string): Promise { const response = await fetch(`${API_BASE}/${id}/cancel`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to cancel request ${id}: ${response.statusText}`); } return response.json(); } } ``` ### 3. Svelte Store (`src/stores/steam2fa.ts`) ```typescript import { writable, derived } from 'svelte/store'; import type { Steam2FARequest, Steam2FAStore } from '../models/steam2fa'; import { Steam2FAApi } from '../lib/api/steam2fa'; const POLLING_INTERVAL = 5000; // 5 seconds const MAX_RETRIES = 3; // Create the main store function createSteam2FAStore() { const { subscribe, set, update } = writable({ requests: [], isPolling: false, error: null, lastChecked: null, }); let pollingInterval: NodeJS.Timeout | null = null; let retryCount = 0; const startPolling = () => { if (pollingInterval) return; // Already polling update(store => ({ ...store, isPolling: true, error: null })); const poll = async () => { try { const requests = await Steam2FAApi.getPendingRequests(); update(store => ({ ...store, requests, error: null, lastChecked: new Date(), })); retryCount = 0; // Reset retry count on success } catch (error) { console.error('Steam 2FA polling error:', error); retryCount++; if (retryCount >= MAX_RETRIES) { update(store => ({ ...store, error: `Failed to check for 2FA requests: ${error.message}`, isPolling: false, })); stopPolling(); return; } update(store => ({ ...store, error: `Connection issue (retry ${retryCount}/${MAX_RETRIES})`, })); } }; // Poll immediately, then set interval poll(); pollingInterval = setInterval(poll, POLLING_INTERVAL); }; const stopPolling = () => { if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; } update(store => ({ ...store, isPolling: false })); }; const completeRequest = async (id: string) => { try { await Steam2FAApi.completeRequest(id); // Remove the completed request from store update(store => ({ ...store, requests: store.requests.filter(req => req.id !== id), })); } catch (error) { console.error('Failed to complete 2FA request:', error); update(store => ({ ...store, error: `Failed to complete 2FA: ${error.message}`, })); throw error; } }; const cancelRequest = async (id: string) => { try { await Steam2FAApi.cancelRequest(id); // Remove the cancelled request from store update(store => ({ ...store, requests: store.requests.filter(req => req.id !== id), })); } catch (error) { console.error('Failed to cancel 2FA request:', error); update(store => ({ ...store, error: `Failed to cancel 2FA: ${error.message}`, })); throw error; } }; const clearError = () => { update(store => ({ ...store, error: null })); }; return { subscribe, startPolling, stopPolling, completeRequest, cancelRequest, clearError, }; } export const steam2fa = createSteam2FAStore(); // Derived store for pending requests export const pendingRequests = derived( steam2fa, $steam2fa => $steam2fa.requests.filter(req => req.status === 'pending') ); // Derived store to check if any requests are pending export const hasPendingRequests = derived( pendingRequests, $pendingRequests => $pendingRequests.length > 0 ); ``` ### 4. Modal Component (`src/components/Steam2FANotification.svelte`) ```svelte {#if isVisible && currentRequest} {/if} ``` ### 5. Main App Integration Add to your main layout file (e.g., `src/routes/+layout.svelte`): ```svelte
``` ## Integration Steps ### 1. Install Dependencies Ensure your frontend has these dependencies (likely already present): ```json { "devDependencies": { "@types/node": "^20.x.x" } } ``` ### 2. Create Files Create all the files listed above in their respective directories in your frontend repository. ### 3. Update Main Layout Add the Steam2FANotification component and polling initialization to your main layout. ### 4. Configure API Base URL Ensure your API calls are pointing to the correct backend URL. Update the `API_BASE` constant in `steam2fa.ts` if needed: ```typescript const API_BASE = '/v1/steam2fa'; // Adjust if your backend uses different base URL ``` ### 5. Test Authentication Ensure your frontend sends authentication cookies/headers with API requests. The backend requires authentication for all 2FA endpoints. ## Flow Diagram ``` SteamCMD Operation Started ↓ Backend detects 2FA prompt ↓ Steam2FARequest created with "pending" status ↓ Frontend polling detects pending request ↓ Modal appears automatically ↓ User checks Steam Mobile App ↓ User approves login in Steam app ↓ User clicks "I've Confirmed" button ↓ Frontend calls POST /v1/steam2fa/{id}/complete ↓ Backend marks request as complete ↓ SteamCMD operation continues ↓ Frontend polling finds no pending requests ↓ Modal closes automatically ``` ## Error Handling ### Frontend Error Scenarios 1. **Network Errors**: Displays retry count and stops after 3 failures 2. **API Errors**: Shows specific error messages to user 3. **Timeout**: Backend handles 5-minute timeout, frontend shows appropriate message 4. **Multiple Requests**: Shows first pending request, handles queue automatically ### User Experience - **Immediate Feedback**: Modal appears as soon as 2FA is required - **Clear Instructions**: Step-by-step guide for users - **Error Recovery**: Clear error messages with retry options - **Non-Blocking**: User can cancel if needed ## Testing Instructions ### Manual Testing 1. **Setup**: Ensure Steam account has 2FA enabled 2. **Trigger**: Create/update a server to trigger SteamCMD 3. **Verify Modal**: Check that modal appears when 2FA prompt occurs 4. **Test Success**: Approve in Steam app, click "I've Confirmed" 5. **Test Cancel**: Try canceling a 2FA request 6. **Test Errors**: Disconnect network during operation ### Debugging ```javascript // Check store state in browser console console.log($steam2fa); // Manual API testing Steam2FAApi.getPendingRequests().then(console.log); ``` ## Security Considerations 1. **Authentication**: All API calls include credentials 2. **No Sensitive Data**: No Steam credentials exposed 3. **Timeout Protection**: 5-minute backend timeout prevents hanging 4. **Request Validation**: Backend validates request ownership ## Performance Considerations 1. **Polling Frequency**: 5-second interval balances responsiveness with load 2. **Automatic Cleanup**: Backend cleans up old requests after 30 minutes 3. **Efficient Updates**: Reactive stores minimize re-renders 4. **Error Backoff**: Stops polling after repeated failures ## Future Enhancements 1. **WebSocket Support**: Real-time updates instead of polling 2. **Browser Notifications**: Alert users even when tab is not active 3. **Multiple Request Support**: Handle multiple simultaneous 2FA requests 4. **Enhanced Prompt Detection**: More sophisticated Steam output parsing ## Troubleshooting ### Common Issues #### Modal Doesn't Appear - Check browser console for JavaScript errors - Verify API connectivity: `fetch('/v1/steam2fa/pending')` - Ensure user is authenticated - Check backend logs for 2FA request creation #### API Errors - Verify authentication cookies are sent - Check CORS configuration - Ensure backend is running and accessible - Review backend error logs #### SteamCMD Hangs - Check if 2FA request was created in backend logs - Verify Steam Mobile App connectivity - Look for timeout errors after 5 minutes ### Debug Commands ```bash # Check backend logs for 2FA events grep -i "2fa" logs/app.log # Monitor API requests tail -f logs/app.log | grep "steam2fa" ``` --- # Server Management Pages Implementation ## Overview In addition to the Steam 2FA system, the frontend needs complete server management pages for creating, viewing, updating, and deleting ACC servers. This section provides implementation details for all server management functionality. ## Backend API Endpoints (Already Implemented) ### Server CRUD Operations - `GET /v1/server` - List all servers with filtering - `GET /v1/api/server` - List servers in API format (simplified) - `GET /v1/server/{id}` - Get specific server details - `POST /v1/server` - Create new server - `PUT /v1/server/{id}` - Update existing server - `DELETE /v1/server/{id}` - Delete server ### Service Control - `GET /v1/server/{id}/service/{service}` - Get service status - `POST /v1/server/{id}/service/start` - Start server service - `POST /v1/server/{id}/service/stop` - Stop server service - `POST /v1/server/{id}/service/restart` - Restart server service ### Configuration Management - `GET /v1/server/{id}/config` - List server config files - `GET /v1/server/{id}/config/{file}` - Get specific config file - `PUT /v1/server/{id}/config/{file}` - Update config file ### Authentication - `POST /v1/auth/login` - User login - `POST /v1/auth/open-token` - Generate open token (for service calls) - `GET /v1/auth/me` - Get current user info ## Frontend Implementation ### 1. TypeScript Models (`src/models/server.ts`) ```typescript export type ServiceStatus = 'running' | 'stopped' | 'starting' | 'stopping' | 'unknown'; export interface ServerState { session: string; sessionStart: string; // ISO timestamp playerCount: number; track: string; maxConnections: number; sessionDurationMinutes: number; } export interface ServerAPI { name: string; status: ServiceStatus; state: ServerState | null; playerCount: number; track: string; } export interface Server { id: string; // UUID name: string; status: ServiceStatus; path: string; serviceName: string; state: ServerState | null; dateCreated: string; // ISO timestamp } export interface ServerFilter { name?: string; serviceName?: string; status?: string; serverID?: string; limit?: number; offset?: number; } export interface CreateServerRequest { name: string; // Additional server configuration fields as needed } export interface UpdateServerRequest { name: string; // Other updateable fields } ``` ### 2. API Client (`src/lib/api/server.ts`) ```typescript import type { Server, ServerAPI, ServerFilter, CreateServerRequest, UpdateServerRequest } from '../models/server'; const API_BASE = '/v1'; export class ServerApi { /** * Get all servers with optional filtering */ static async getServers(filter?: ServerFilter): Promise { const params = new URLSearchParams(); if (filter) { Object.entries(filter).forEach(([key, value]) => { if (value !== undefined && value !== null) { params.append(key, value.toString()); } }); } const url = `${API_BASE}/server${params.toString() ? `?${params.toString()}` : ''}`; const response = await fetch(url, { method: 'GET', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to fetch servers: ${response.statusText}`); } return response.json(); } /** * Get servers in API format (simplified) */ static async getServersAPI(filter?: ServerFilter): Promise { const params = new URLSearchParams(); if (filter) { Object.entries(filter).forEach(([key, value]) => { if (value !== undefined && value !== null) { params.append(key, value.toString()); } }); } const url = `${API_BASE}/api/server${params.toString() ? `?${params.toString()}` : ''}`; const response = await fetch(url, { method: 'GET', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to fetch servers: ${response.statusText}`); } return response.json(); } /** * Get specific server by ID */ static async getServer(id: string): Promise { const response = await fetch(`${API_BASE}/server/${id}`, { method: 'GET', credentials: 'include', }); if (!response.ok) { if (response.status === 404) { throw new Error('Server not found'); } throw new Error(`Failed to fetch server: ${response.statusText}`); } return response.json(); } /** * Create new server */ static async createServer(serverData: CreateServerRequest): Promise { const response = await fetch(`${API_BASE}/server`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(serverData), }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.message || `Failed to create server: ${response.statusText}`); } return response.json(); } /** * Update existing server */ static async updateServer(id: string, serverData: UpdateServerRequest): Promise { const response = await fetch(`${API_BASE}/server/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(serverData), }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.message || `Failed to update server: ${response.statusText}`); } return response.json(); } /** * Delete server */ static async deleteServer(id: string): Promise { const response = await fetch(`${API_BASE}/server/${id}`, { method: 'DELETE', credentials: 'include', }); if (!response.ok) { if (response.status === 404) { throw new Error('Server not found'); } throw new Error(`Failed to delete server: ${response.statusText}`); } } /** * Get service status */ static async getServiceStatus(serverId: string, serviceName: string): Promise<{ status: string; state: string }> { const response = await fetch(`${API_BASE}/server/${serverId}/service/${serviceName}`, { method: 'GET', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to get service status: ${response.statusText}`); } return response.json(); } /** * Start server service */ static async startService(serverId: string): Promise { const response = await fetch(`${API_BASE}/server/${serverId}/service/start`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to start service: ${response.statusText}`); } return response.text(); } /** * Stop server service */ static async stopService(serverId: string): Promise { const response = await fetch(`${API_BASE}/server/${serverId}/service/stop`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to stop service: ${response.statusText}`); } return response.text(); } /** * Restart server service */ static async restartService(serverId: string): Promise { const response = await fetch(`${API_BASE}/server/${serverId}/service/restart`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error(`Failed to restart service: ${response.statusText}`); } return response.text(); } } ``` ### 3. Server Store (`src/stores/server.ts`) ```typescript import { writable, derived } from 'svelte/store'; import type { Server, ServerFilter } from '../models/server'; import { ServerApi } from '../lib/api/server'; interface ServerStore { servers: Server[]; selectedServer: Server | null; isLoading: boolean; error: string | null; filter: ServerFilter; } function createServerStore() { const { subscribe, set, update } = writable({ servers: [], selectedServer: null, isLoading: false, error: null, filter: {}, }); const loadServers = async (filter?: ServerFilter) => { update(store => ({ ...store, isLoading: true, error: null })); try { const servers = await ServerApi.getServers(filter); update(store => ({ ...store, servers, filter: filter || {}, isLoading: false, })); } catch (error) { console.error('Failed to load servers:', error); update(store => ({ ...store, error: error.message, isLoading: false, })); } }; const loadServer = async (id: string) => { update(store => ({ ...store, isLoading: true, error: null })); try { const server = await ServerApi.getServer(id); update(store => ({ ...store, selectedServer: server, isLoading: false, })); return server; } catch (error) { console.error('Failed to load server:', error); update(store => ({ ...store, error: error.message, isLoading: false, })); throw error; } }; const createServer = async (serverData: any) => { update(store => ({ ...store, isLoading: true, error: null })); try { const newServer = await ServerApi.createServer(serverData); update(store => ({ ...store, servers: [...store.servers, newServer], isLoading: false, })); return newServer; } catch (error) { console.error('Failed to create server:', error); update(store => ({ ...store, error: error.message, isLoading: false, })); throw error; } }; const updateServer = async (id: string, serverData: any) => { update(store => ({ ...store, isLoading: true, error: null })); try { const updatedServer = await ServerApi.updateServer(id, serverData); update(store => ({ ...store, servers: store.servers.map(s => s.id === id ? updatedServer : s), selectedServer: store.selectedServer?.id === id ? updatedServer : store.selectedServer, isLoading: false, })); return updatedServer; } catch (error) { console.error('Failed to update server:', error); update(store => ({ ...store, error: error.message, isLoading: false, })); throw error; } }; const deleteServer = async (id: string) => { update(store => ({ ...store, isLoading: true, error: null })); try { await ServerApi.deleteServer(id); update(store => ({ ...store, servers: store.servers.filter(s => s.id !== id), selectedServer: store.selectedServer?.id === id ? null : store.selectedServer, isLoading: false, })); } catch (error) { console.error('Failed to delete server:', error); update(store => ({ ...store, error: error.message, isLoading: false, })); throw error; } }; const controlService = async (serverId: string, action: 'start' | 'stop' | 'restart') => { try { let result: string; switch (action) { case 'start': result = await ServerApi.startService(serverId); break; case 'stop': result = await ServerApi.stopService(serverId); break; case 'restart': result = await ServerApi.restartService(serverId); break; } // Refresh server data after service control await loadServer(serverId); return result; } catch (error) { console.error(`Failed to ${action} service:`, error); update(store => ({ ...store, error: error.message })); throw error; } }; const clearError = () => { update(store => ({ ...store, error: null })); }; const setFilter = (filter: ServerFilter) => { update(store => ({ ...store, filter })); loadServers(filter); }; return { subscribe, loadServers, loadServer, createServer, updateServer, deleteServer, controlService, clearError, setFilter, }; } export const serverStore = createServerStore(); // Derived stores export const servers = derived(serverStore, $store => $store.servers); export const selectedServer = derived(serverStore, $store => $store.selectedServer); export const isLoading = derived(serverStore, $store => $store.isLoading); export const serverError = derived(serverStore, $store => $store.error); ``` ### 4. Server List Page (`src/routes/servers/+page.svelte`) ```svelte ACC Servers

ACC Servers

{#if $serverError}
{$serverError}
{/if} {#if $isLoading}
{:else if filteredServers.length === 0}
{$servers.length === 0 ? 'No servers found' : 'No servers match your filter'}
{#if $servers.length === 0} {/if}
{:else}
{#each filteredServers as server (server.id)}

{server.name}

{server.status}
Service: {server.serviceName}
Created: {formatDate(server.dateCreated)}
{#if server.state}
Players: {server.state.playerCount}/{server.state.maxConnections}
Track: {server.state.track || 'Unknown'}
{/if}
{/each}
{/if}
``` ### 5. Create Server Page (`src/routes/servers/create/+page.svelte`) ```svelte Create Server - ACC Server Manager

Create New ACC Server

{#if errors.general}
{errors.general}
{/if}
{#if errors.name} {/if}

What happens when you create a server?

  • ACC server files will be downloaded via SteamCMD
  • A Windows service will be created
  • Default configuration files will be generated
  • You may need to confirm Steam 2FA if prompted

Steam Authentication Required

If your Steam account has 2FA enabled, you'll need to confirm the login request in your Steam Mobile App when prompted.

``` ### 6. Server Detail Page (`src/routes/servers/[id]/+page.svelte`) ```svelte {$selectedServer?.name || 'Server'} - ACC Server Manager
{#if $isLoading}
{:else if $serverError}
{$serverError}
{:else if $selectedServer}

{$selectedServer.name}

{$selectedServer.status}
Service: {$selectedServer.serviceName}

Service Control

{#if controlError}
{controlError}
{/if}

Server Information

ID: {$selectedServer.id}
Name: {$selectedServer.name}
Service Name: {$selectedServer.serviceName}
Path: {$selectedServer.path}
Created: {formatDate($selectedServer.dateCreated)}
{#if $selectedServer.state}

Current State

Session: {$selectedServer.state.session}
Track: {$selectedServer.state.track || 'Unknown'}
Players: {$selectedServer.state.playerCount}/{$selectedServer.state.maxConnections}
Session Duration: {$selectedServer.state.sessionDurationMinutes} minutes
Session Started: {formatDate($selectedServer.state.sessionStart)}
{:else}

Current State

No state information available

{/if}

Configuration

Manage server configuration files

{/if}
``` ### 7. Navigation Integration Add server management links to your main navigation (`src/lib/components/Navigation.svelte`): ```svelte ``` ## Routes Structure Ensure your routing structure includes: ``` src/routes/ ├── +layout.svelte # Main layout with Steam2FA component ├── servers/ │ ├── +page.svelte # Server list page │ ├── create/ │ │ └── +page.svelte # Create server page │ └── [id]/ │ ├── +page.svelte # Server detail page │ ├── edit/ │ │ └── +page.svelte # Edit server page │ └── config/ │ └── +page.svelte # Configuration management page ``` ## Key Features Implemented 1. **Complete CRUD Operations**: Create, read, update, delete servers 2. **Service Control**: Start, stop, restart server services 3. **Real-time Status**: Display current server status and state 4. **Search and Filtering**: Find servers by name or status 5. **Error Handling**: Comprehensive error messages and recovery 6. **Steam 2FA Integration**: Automatic handling during server creation 7. **Responsive Design**: Works on desktop and mobile devices 8. **Loading States**: Clear feedback during async operations ## Authentication Integration The API client automatically includes authentication cookies with all requests. Ensure your authentication system is set up to handle: 1. User login/logout 2. Session management 3. Permission checking (ServerView, ServerCreate, ServerUpdate, ServerDelete) ## Conclusion This implementation provides a complete, production-ready Steam 2FA system for the frontend. The polling-based approach ensures compatibility with all browsers and provides reliable 2FA handling for Steam operations. The system is designed to be: - **User-friendly**: Clear instructions and immediate feedback - **Robust**: Comprehensive error handling and recovery - **Maintainable**: Clean separation of concerns and well-documented code - **Secure**: Proper authentication and no credential exposure Once implemented, users will receive immediate notifications when Steam 2FA is required, making server management seamless and intuitive.