mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-16 10:45:37 +02:00
⭐ 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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user