Remote connection

This commit is contained in:
2026-01-09 19:49:38 +01:00
parent 87c722b5ae
commit 8c551a90f4
28 changed files with 3134 additions and 327 deletions

BIN
server/data/metoyou.sqlite Normal file

Binary file not shown.

View File

@@ -23,5 +23,17 @@
"tags": [],
"createdAt": 1766902260144,
"lastSeen": 1766902260144
},
{
"id": "337ad599-736e-49c6-bf01-fb94c1b82a6d",
"name": "ASDASD",
"ownerId": "54c0953a-1e54-4c07-8da9-06c143d9354f",
"ownerPublicKey": "54c0953a-1e54-4c07-8da9-06c143d9354f",
"isPrivate": false,
"maxUsers": 50,
"currentUsers": 0,
"tags": [],
"createdAt": 1767240654523,
"lastSeen": 1767240654523
}
]

View File

@@ -19,6 +19,7 @@
"@types/cors": "^2.8.14",
"@types/express": "^4.17.18",
"@types/node": "^20.8.0",
"@types/sql.js": "^1.4.9",
"@types/uuid": "^9.0.4",
"@types/ws": "^8.5.8",
"ts-node-dev": "^2.0.0",

View File

@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import initSqlJs, { Database, Statement } from 'sql.js';
import initSqlJs from 'sql.js';
// Simple SQLite via sql.js persisted to a single file
const DATA_DIR = path.join(process.cwd(), 'data');
@@ -11,7 +11,7 @@ function ensureDataDir() {
}
let SQL: any = null;
let db: Database | null = null;
let db: any | null = null;
export async function initDB(): Promise<void> {
if (db) return;
@@ -56,7 +56,7 @@ export interface AuthUser {
export async function getUserByUsername(username: string): Promise<AuthUser | null> {
if (!db) await initDB();
const stmt: Statement = db!.prepare('SELECT id, username, passwordHash, displayName, createdAt FROM users WHERE username = ? LIMIT 1');
const stmt: any = db!.prepare('SELECT id, username, passwordHash, displayName, createdAt FROM users WHERE username = ? LIMIT 1');
stmt.bind([username]);
let row: AuthUser | null = null;
if (stmt.step()) {
@@ -75,7 +75,7 @@ export async function getUserByUsername(username: string): Promise<AuthUser | nu
export async function getUserById(id: string): Promise<AuthUser | null> {
if (!db) await initDB();
const stmt: Statement = db!.prepare('SELECT id, username, passwordHash, displayName, createdAt FROM users WHERE id = ? LIMIT 1');
const stmt: any = db!.prepare('SELECT id, username, passwordHash, displayName, createdAt FROM users WHERE id = ? LIMIT 1');
stmt.bind([id]);
let row: AuthUser | null = null;
if (stmt.step()) {

View File

@@ -88,6 +88,46 @@ app.get('/api/time', (req, res) => {
res.json({ now: Date.now() });
});
// Image proxy to allow rendering external images within CSP (img-src 'self' data: blob:)
app.get('/api/image-proxy', async (req, res) => {
try {
const url = String(req.query.url || '');
if (!/^https?:\/\//i.test(url)) {
return res.status(400).json({ error: 'Invalid URL' });
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const response = await fetch(url, { redirect: 'follow', signal: controller.signal });
clearTimeout(timeout);
if (!response.ok) {
return res.status(response.status).end();
}
const contentType = response.headers.get('content-type') || '';
if (!contentType.toLowerCase().startsWith('image/')) {
return res.status(415).json({ error: 'Unsupported content type' });
}
const arrayBuffer = await response.arrayBuffer();
const MAX_BYTES = 8 * 1024 * 1024; // 8MB limit
if (arrayBuffer.byteLength > MAX_BYTES) {
return res.status(413).json({ error: 'Image too large' });
}
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.send(Buffer.from(arrayBuffer));
} catch (err) {
if ((err as any)?.name === 'AbortError') {
return res.status(504).json({ error: 'Timeout fetching image' });
}
console.error('Image proxy error:', err);
res.status(502).json({ error: 'Failed to fetch image' });
}
});
// Basic auth (demo - file-based)
interface AuthUser { id: string; username: string; passwordHash: string; displayName: string; createdAt: number; }
let authUsers: AuthUser[] = [];
@@ -320,33 +360,33 @@ const server = createServer(app);
const wss = new WebSocketServer({ server });
wss.on('connection', (ws: WebSocket) => {
const oderId = uuidv4();
connectedUsers.set(oderId, { oderId, ws });
const connectionId = uuidv4();
connectedUsers.set(connectionId, { oderId: connectionId, ws });
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
handleWebSocketMessage(oderId, message);
handleWebSocketMessage(connectionId, message);
} catch (err) {
console.error('Invalid WebSocket message:', err);
}
});
ws.on('close', () => {
const user = connectedUsers.get(oderId);
const user = connectedUsers.get(connectionId);
if (user?.serverId) {
// Notify others in the room
// Notify others in the room - use user.oderId (the actual user ID), not connectionId
broadcastToServer(user.serverId, {
type: 'user_left',
oderId,
oderId: user.oderId,
displayName: user.displayName,
}, oderId);
}, user.oderId);
}
connectedUsers.delete(oderId);
connectedUsers.delete(connectionId);
});
// Send connection acknowledgment
ws.send(JSON.stringify({ type: 'connected', oderId, serverTime: Date.now() }));
// Send connection acknowledgment with the connectionId (client will identify with their actual oderId)
ws.send(JSON.stringify({ type: 'connected', connectionId, serverTime: Date.now() }));
});
function handleWebSocketMessage(connectionId: string, message: any): void {
@@ -366,14 +406,15 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
case 'join_server':
user.serverId = message.serverId;
connectedUsers.set(connectionId, user);
console.log(`User ${user.displayName} (${user.oderId}) joined server ${message.serverId}`);
console.log(`User ${user.displayName || 'Anonymous'} (${user.oderId}) joined server ${message.serverId}`);
// Get list of current users in server (exclude this user by oderId)
// Only include users that have been identified (have displayName)
const usersInServer = Array.from(connectedUsers.values())
.filter(u => u.serverId === message.serverId && u.oderId !== user.oderId)
.map(u => ({ oderId: u.oderId, displayName: u.displayName }));
.filter(u => u.serverId === message.serverId && u.oderId !== user.oderId && u.displayName)
.map(u => ({ oderId: u.oderId, displayName: u.displayName || 'Anonymous' }));
console.log(`Sending server_users to ${user.displayName}:`, usersInServer);
console.log(`Sending server_users to ${user.displayName || 'Anonymous'}:`, usersInServer);
user.ws.send(JSON.stringify({
type: 'server_users',
users: usersInServer,
@@ -383,7 +424,7 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
broadcastToServer(message.serverId, {
type: 'user_joined',
oderId: user.oderId,
displayName: user.displayName,
displayName: user.displayName || 'Anonymous',
}, user.oderId);
break;
@@ -396,7 +437,7 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
broadcastToServer(oldServerId, {
type: 'user_left',
oderId: user.oderId,
displayName: user.displayName,
displayName: user.displayName || 'Anonymous',
}, user.oderId);
}
break;

View File

@@ -1,3 +1,4 @@
declare module 'sql.js';
declare module 'sql.js' {
export default function initSqlJs(config?: { locateFile?: (file: string) => string }): Promise<any>;
export type Database = any;