feat(www): Add logic to the homepage and Steam integration (#258)

## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Upgraded API and authentication services with dynamic scaling,
enhanced load balancing, and real-time interaction endpoints.
- Introduced new commands to streamline local development and container
builds.
- Added new endpoints for retrieving Steam account information and
managing connections.
- Implemented a QR code authentication interface for Steam, enhancing
user login experiences.

- **Database Updates**
- Rolled out comprehensive schema migrations that improve data integrity
and indexing.
- Introduced new tables for managing Steam user credentials and machine
information.

- **UI Enhancements**
- Added refreshed animated assets and an improved QR code login flow for
a more engaging experience.
	- Introduced new styled components for displaying friends and games.

- **Maintenance**
- Completed extensive refactoring and configuration updates to optimize
performance and development workflows.
- Updated logging configurations and improved error handling mechanisms.
	- Streamlined resource definitions in the configuration files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Wanjohi
2025-04-13 14:30:45 +03:00
committed by GitHub
parent 8394bb4259
commit f408ec56cb
103 changed files with 12755 additions and 2053 deletions

View File

@@ -6,7 +6,7 @@ import { createInitializedContext } from "@nestri/www/common/context";
export const { use: useApi, provider: ApiProvider } = createInitializedContext(
"Api",
"ApiContext",
() => {
const team = useTeam();
const auth = useOpenAuth();

View File

@@ -4,23 +4,31 @@ import { useOpenAuth } from "@openauthjs/solid";
import { createSignal, onCleanup } from "solid-js";
import { createInitializedContext } from "../common/context";
// Global connection state to prevent multiple instances
let globalEventSource: EventSource | null = null;
let globalReconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 1;
let isConnecting = false;
let activeConnection: SteamConnection | null = null;
// FIXME: The redo button is not working as expected... it does not reinitialise the connection
// Type definitions for the events
interface SteamEventTypes {
'url': string;
'login-attempt': { username: string };
'login-success': { username: string; steamId: string };
'login-unsuccessful': { error: string };
'logged-off': { reason: string };
'connected': { sessionID: string };
'challenge': { sessionID: string; url: string };
'error': { message: string };
'completed': { sessionID: string };
}
// Type for the connection
type SteamConnection = {
addEventListener: <T extends keyof SteamEventTypes>(
event: T,
event: T,
callback: (data: SteamEventTypes[T]) => void
) => () => void;
removeEventListener: <T extends keyof SteamEventTypes>(
event: T,
event: T,
callback: (data: SteamEventTypes[T]) => void
) => void;
disconnect: () => void;
@@ -30,74 +38,67 @@ type SteamConnection = {
interface SteamContext {
ready: boolean;
client: {
// Regular API endpoints
whoami: () => Promise<any>;
games: () => Promise<any>;
// SSE connection for login
login: {
connect: () => SteamConnection;
connect: () => Promise<SteamConnection>;
};
};
}
// Create the initialized context
export const { use: useSteam, provider: SteamProvider } = createInitializedContext(
"Steam",
"SteamContext",
() => {
const team = useTeam();
const auth = useOpenAuth();
// Create the HTTP client for regular endpoints
const client = {
// Regular HTTP endpoints
whoami: async () => {
const token = await auth.access();
const response = await fetch(`${import.meta.env.VITE_STEAM_URL}/whoami`, {
headers: {
'Authorization': `Bearer ${token}`,
'x-nestri-team': team().id
}
});
return response.json();
},
games: async () => {
const token = await auth.access();
const response = await fetch(`${import.meta.env.VITE_STEAM_URL}/games`, {
headers: {
'Authorization': `Bearer ${token}`,
'x-nestri-team': team().id
}
});
return response.json();
},
// SSE connection factory for login
login: {
connect: async (): Promise<SteamConnection> => {
let eventSource: EventSource | null = null;
// Return existing connection if active
if (activeConnection && globalEventSource && globalEventSource.readyState !== 2) {
return activeConnection;
}
// Prevent multiple simultaneous connection attempts
if (isConnecting) {
console.log("Connection attempt already in progress, waiting...");
// Wait for existing connection attempt to finish
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!isConnecting && activeConnection) {
clearInterval(checkInterval);
resolve(activeConnection);
}
}, 100);
});
}
isConnecting = true;
const [isConnected, setIsConnected] = createSignal(false);
// Store event listeners
const listeners: Record<string, Array<(data: any) => void>> = {
'url': [],
'login-attempt': [],
'login-success': [],
'login-unsuccessful': [],
'logged-off': []
'connected': [],
'challenge': [],
'error': [],
'completed': []
};
// Method to add event listeners
const addEventListener = <T extends keyof SteamEventTypes>(
event: T,
event: T,
callback: (data: SteamEventTypes[T]) => void
) => {
if (!listeners[event]) {
listeners[event] = [];
}
listeners[event].push(callback as any);
// Return a function to remove this specific listener
return () => {
removeEventListener(event, callback);
@@ -106,7 +107,7 @@ export const { use: useSteam, provider: SteamProvider } = createInitializedConte
// Method to remove event listeners
const removeEventListener = <T extends keyof SteamEventTypes>(
event: T,
event: T,
callback: (data: SteamEventTypes[T]) => void
) => {
if (listeners[event]) {
@@ -117,16 +118,39 @@ export const { use: useSteam, provider: SteamProvider } = createInitializedConte
}
};
// Handle notifying listeners safely
const notifyListeners = (eventType: string, data: any) => {
if (listeners[eventType]) {
listeners[eventType].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in ${eventType} event handler:`, error);
}
});
}
};
// Initialize connection
const initConnection = async () => {
if (eventSource) {
eventSource.close();
if (globalReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.log(`Maximum reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);
notifyListeners('error', { message: 'Connection to Steam authentication failed after multiple attempts' });
isConnecting = false;
disconnect()
return;
}
if (globalEventSource) {
globalEventSource.close();
globalEventSource = null;
}
try {
const token = await auth.access();
eventSource = new EventSource(`${import.meta.env.VITE_STEAM_URL}/login`, {
// Create new EventSource connection
globalEventSource = new EventSource(`${import.meta.env.VITE_API_URL}/steam/login`, {
fetch: (input, init) =>
fetch(input, {
...init,
@@ -138,59 +162,74 @@ export const { use: useSteam, provider: SteamProvider } = createInitializedConte
}),
});
eventSource.onopen = () => {
globalEventSource.onopen = () => {
console.log('Connected to Steam login stream');
setIsConnected(true);
globalReconnectAttempts = 0; // Reset reconnect counter on successful connection
isConnecting = false;
};
// Set up event handlers for all specific events
['url', 'login-attempt', 'login-success', 'login-unsuccessful', 'logged-off'].forEach((eventType) => {
eventSource!.addEventListener(eventType, (event) => {
['connected', 'challenge', 'completed'].forEach((eventType) => {
globalEventSource!.addEventListener(eventType, (event) => {
try {
const data = JSON.parse(event.data);
console.log(`Received ${eventType} event:`, data);
// Notify all registered listeners for this event type
if (listeners[eventType]) {
listeners[eventType].forEach(callback => {
callback(data);
});
}
notifyListeners(eventType, data);
} catch (error) {
console.error(`Error parsing ${eventType} event data:`, error);
}
});
});
// Handle generic messages (fallback)
eventSource.onmessage = (event) => {
console.log('Received generic message:', event.data);
};
eventSource.onerror = (error) => {
console.error('Steam login stream error:', error);
// Handle connection errors (this is different from server-sent 'error' events)
globalEventSource.onerror = (error) => {
console.error('Steam login stream connection error:', error);
setIsConnected(false);
// Attempt to reconnect after a delay
setTimeout(initConnection, 5000);
// Close the connection to prevent automatic browser reconnect
if (globalEventSource) {
globalEventSource.close();
}
// Check if we should attempt to reconnect
if (globalReconnectAttempts <= MAX_RECONNECT_ATTEMPTS) {
const currentAttempt = globalReconnectAttempts + 1;
console.log(`Reconnecting (attempt ${currentAttempt}/${MAX_RECONNECT_ATTEMPTS})...`);
globalReconnectAttempts = currentAttempt;
// Exponential backoff for reconnection
const delay = Math.min(1000 * Math.pow(2, globalReconnectAttempts), 30000);
setTimeout(initConnection, delay);
} else {
console.error(`Maximum reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
// Notify listeners about connection failure
notifyListeners('error', { message: 'Connection to Steam authentication failed after multiple attempts' });
disconnect();
isConnecting = false;
}
};
} catch (error) {
console.error('Failed to connect to Steam login stream:', error);
setIsConnected(false);
isConnecting = false;
}
};
// Disconnection function
const disconnect = () => {
if (eventSource) {
eventSource.close();
eventSource = null;
if (globalEventSource) {
globalEventSource.close();
globalEventSource = null;
setIsConnected(false);
console.log('Disconnected from Steam login stream');
// Clear all listeners
Object.keys(listeners).forEach(key => {
listeners[key] = [];
});
activeConnection = null;
}
};
@@ -205,9 +244,17 @@ export const { use: useSteam, provider: SteamProvider } = createInitializedConte
isConnected: () => isConnected()
};
// Store the active connection
activeConnection = connection;
// Clean up on context destruction
onCleanup(() => {
disconnect();
// Instead of disconnecting on cleanup, we'll leave the connection
// active for other components to use
// Only disconnect if no components are using it
if (!isConnected()) {
disconnect();
}
});
return connection;