Add eslint
This commit is contained in:
48
server/dist/db.d.ts
vendored
48
server/dist/db.d.ts
vendored
@@ -1,43 +1,43 @@
|
||||
export declare function initDB(): Promise<void>;
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
displayName: string;
|
||||
createdAt: number;
|
||||
id: string;
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
displayName: string;
|
||||
createdAt: number;
|
||||
}
|
||||
export declare function getUserByUsername(username: string): Promise<AuthUser | null>;
|
||||
export declare function getUserById(id: string): Promise<AuthUser | null>;
|
||||
export declare function createUser(user: AuthUser): Promise<void>;
|
||||
export interface ServerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
ownerPublicKey: string;
|
||||
isPrivate: boolean;
|
||||
maxUsers: number;
|
||||
currentUsers: number;
|
||||
tags: string[];
|
||||
createdAt: number;
|
||||
lastSeen: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
ownerPublicKey: string;
|
||||
isPrivate: boolean;
|
||||
maxUsers: number;
|
||||
currentUsers: number;
|
||||
tags: string[];
|
||||
createdAt: number;
|
||||
lastSeen: number;
|
||||
}
|
||||
export declare function getAllPublicServers(): Promise<ServerInfo[]>;
|
||||
export declare function getServerById(id: string): Promise<ServerInfo | null>;
|
||||
export declare function upsertServer(server: ServerInfo): Promise<void>;
|
||||
export declare function deleteServer(id: string): Promise<void>;
|
||||
export interface JoinRequest {
|
||||
id: string;
|
||||
serverId: string;
|
||||
userId: string;
|
||||
userPublicKey: string;
|
||||
displayName: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
createdAt: number;
|
||||
id: string;
|
||||
serverId: string;
|
||||
userId: string;
|
||||
userPublicKey: string;
|
||||
displayName: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
createdAt: number;
|
||||
}
|
||||
export declare function createJoinRequest(req: JoinRequest): Promise<void>;
|
||||
export declare function getJoinRequestById(id: string): Promise<JoinRequest | null>;
|
||||
export declare function getPendingRequestsForServer(serverId: string): Promise<JoinRequest[]>;
|
||||
export declare function updateJoinRequestStatus(id: string, status: JoinRequest['status']): Promise<void>;
|
||||
export declare function deleteStaleJoinRequests(maxAgeMs: number): Promise<void>;
|
||||
//# sourceMappingURL=db.d.ts.map
|
||||
// # sourceMappingURL=db.d.ts.map
|
||||
|
||||
2
server/dist/index.d.ts
vendored
2
server/dist/index.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
// # sourceMappingURL=index.d.ts.map
|
||||
|
||||
@@ -7,19 +7,23 @@ const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const DB_FILE = path.join(DATA_DIR, 'metoyou.sqlite');
|
||||
|
||||
function ensureDataDir() {
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
if (!fs.existsSync(DATA_DIR))
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
let SQL: any = null;
|
||||
let db: any | null = null;
|
||||
|
||||
export async function initDB(): Promise<void> {
|
||||
if (db) return;
|
||||
if (db)
|
||||
return;
|
||||
|
||||
SQL = await initSqlJs({ locateFile: (file: string) => require.resolve('sql.js/dist/sql-wasm.wasm') });
|
||||
ensureDataDir();
|
||||
|
||||
if (fs.existsSync(DB_FILE)) {
|
||||
const fileBuffer = fs.readFileSync(DB_FILE);
|
||||
|
||||
db = new SQL.Database(new Uint8Array(fileBuffer));
|
||||
} else {
|
||||
db = new SQL.Database();
|
||||
@@ -68,9 +72,12 @@ export async function initDB(): Promise<void> {
|
||||
}
|
||||
|
||||
function persist(): void {
|
||||
if (!db) return;
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
const data = db.export();
|
||||
const buffer = Buffer.from(data);
|
||||
|
||||
fs.writeFileSync(DB_FILE, buffer);
|
||||
}
|
||||
|
||||
@@ -87,46 +94,61 @@ export interface AuthUser {
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string): Promise<AuthUser | null> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
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()) {
|
||||
const r = stmt.getAsObject() as any;
|
||||
|
||||
row = {
|
||||
id: String(r.id),
|
||||
username: String(r.username),
|
||||
passwordHash: String(r.passwordHash),
|
||||
displayName: String(r.displayName),
|
||||
createdAt: Number(r.createdAt),
|
||||
createdAt: Number(r.createdAt)
|
||||
};
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function getUserById(id: string): Promise<AuthUser | null> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
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()) {
|
||||
const r = stmt.getAsObject() as any;
|
||||
|
||||
row = {
|
||||
id: String(r.id),
|
||||
username: String(r.username),
|
||||
passwordHash: String(r.passwordHash),
|
||||
displayName: String(r.displayName),
|
||||
createdAt: Number(r.createdAt),
|
||||
createdAt: Number(r.createdAt)
|
||||
};
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function createUser(user: AuthUser): Promise<void> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt = db!.prepare('INSERT INTO users (id, username, passwordHash, displayName, createdAt) VALUES (?, ?, ?, ?, ?)');
|
||||
|
||||
stmt.bind([user.id, user.username, user.passwordHash, user.displayName, user.createdAt]);
|
||||
stmt.step();
|
||||
stmt.free();
|
||||
@@ -163,39 +185,51 @@ function rowToServer(r: any): ServerInfo {
|
||||
currentUsers: Number(r.currentUsers),
|
||||
tags: JSON.parse(String(r.tags || '[]')),
|
||||
createdAt: Number(r.createdAt),
|
||||
lastSeen: Number(r.lastSeen),
|
||||
lastSeen: Number(r.lastSeen)
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllPublicServers(): Promise<ServerInfo[]> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt: any = db!.prepare('SELECT * FROM servers WHERE isPrivate = 0');
|
||||
const results: ServerInfo[] = [];
|
||||
|
||||
while (stmt.step()) {
|
||||
results.push(rowToServer(stmt.getAsObject()));
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function getServerById(id: string): Promise<ServerInfo | null> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt: any = db!.prepare('SELECT * FROM servers WHERE id = ? LIMIT 1');
|
||||
|
||||
stmt.bind([id]);
|
||||
let row: ServerInfo | null = null;
|
||||
|
||||
if (stmt.step()) {
|
||||
row = rowToServer(stmt.getAsObject());
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function upsertServer(server: ServerInfo): Promise<void> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt = db!.prepare(`
|
||||
INSERT OR REPLACE INTO servers (id, name, description, ownerId, ownerPublicKey, isPrivate, maxUsers, currentUsers, tags, createdAt, lastSeen)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.bind([
|
||||
server.id,
|
||||
server.name,
|
||||
@@ -207,7 +241,7 @@ export async function upsertServer(server: ServerInfo): Promise<void> {
|
||||
server.currentUsers,
|
||||
JSON.stringify(server.tags),
|
||||
server.createdAt,
|
||||
server.lastSeen,
|
||||
server.lastSeen
|
||||
]);
|
||||
stmt.step();
|
||||
stmt.free();
|
||||
@@ -215,13 +249,17 @@ export async function upsertServer(server: ServerInfo): Promise<void> {
|
||||
}
|
||||
|
||||
export async function deleteServer(id: string): Promise<void> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt = db!.prepare('DELETE FROM servers WHERE id = ?');
|
||||
|
||||
stmt.bind([id]);
|
||||
stmt.step();
|
||||
stmt.free();
|
||||
// Also clean up related join requests
|
||||
const jStmt = db!.prepare('DELETE FROM join_requests WHERE serverId = ?');
|
||||
|
||||
jStmt.bind([id]);
|
||||
jStmt.step();
|
||||
jStmt.free();
|
||||
@@ -250,16 +288,19 @@ function rowToJoinRequest(r: any): JoinRequest {
|
||||
userPublicKey: String(r.userPublicKey),
|
||||
displayName: String(r.displayName),
|
||||
status: String(r.status) as JoinRequest['status'],
|
||||
createdAt: Number(r.createdAt),
|
||||
createdAt: Number(r.createdAt)
|
||||
};
|
||||
}
|
||||
|
||||
export async function createJoinRequest(req: JoinRequest): Promise<void> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt = db!.prepare(`
|
||||
INSERT INTO join_requests (id, serverId, userId, userPublicKey, displayName, status, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.bind([req.id, req.serverId, req.userId, req.userPublicKey, req.displayName, req.status, req.createdAt]);
|
||||
stmt.step();
|
||||
stmt.free();
|
||||
@@ -267,32 +308,45 @@ export async function createJoinRequest(req: JoinRequest): Promise<void> {
|
||||
}
|
||||
|
||||
export async function getJoinRequestById(id: string): Promise<JoinRequest | null> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt: any = db!.prepare('SELECT * FROM join_requests WHERE id = ? LIMIT 1');
|
||||
|
||||
stmt.bind([id]);
|
||||
let row: JoinRequest | null = null;
|
||||
|
||||
if (stmt.step()) {
|
||||
row = rowToJoinRequest(stmt.getAsObject());
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function getPendingRequestsForServer(serverId: string): Promise<JoinRequest[]> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt: any = db!.prepare('SELECT * FROM join_requests WHERE serverId = ? AND status = ?');
|
||||
|
||||
stmt.bind([serverId, 'pending']);
|
||||
const results: JoinRequest[] = [];
|
||||
|
||||
while (stmt.step()) {
|
||||
results.push(rowToJoinRequest(stmt.getAsObject()));
|
||||
}
|
||||
|
||||
stmt.free();
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function updateJoinRequestStatus(id: string, status: JoinRequest['status']): Promise<void> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const stmt = db!.prepare('UPDATE join_requests SET status = ? WHERE id = ?');
|
||||
|
||||
stmt.bind([status, id]);
|
||||
stmt.step();
|
||||
stmt.free();
|
||||
@@ -300,9 +354,12 @@ export async function updateJoinRequestStatus(id: string, status: JoinRequest['s
|
||||
}
|
||||
|
||||
export async function deleteStaleJoinRequests(maxAgeMs: number): Promise<void> {
|
||||
if (!db) await initDB();
|
||||
if (!db)
|
||||
await initDB();
|
||||
|
||||
const cutoff = Date.now() - maxAgeMs;
|
||||
const stmt = db!.prepare('DELETE FROM join_requests WHERE createdAt < ?');
|
||||
|
||||
stmt.bind([cutoff]);
|
||||
stmt.step();
|
||||
stmt.free();
|
||||
|
||||
@@ -23,8 +23,8 @@ app.use(express.json());
|
||||
interface ConnectedUser {
|
||||
oderId: string;
|
||||
ws: WebSocket;
|
||||
serverIds: Set<string>; // all servers the user is a member of
|
||||
viewedServerId?: string; // currently viewed/active server
|
||||
serverIds: Set<string>; // all servers the user is a member of
|
||||
viewedServerId?: string; // currently viewed/active server
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
@@ -46,21 +46,23 @@ import {
|
||||
updateJoinRequestStatus,
|
||||
deleteStaleJoinRequests,
|
||||
ServerInfo,
|
||||
JoinRequest,
|
||||
JoinRequest
|
||||
} from './db';
|
||||
|
||||
function hashPassword(pw: string) { return crypto.createHash('sha256').update(pw).digest('hex'); }
|
||||
function hashPassword(pw: string) { return crypto.createHash('sha256').update(pw)
|
||||
.digest('hex'); }
|
||||
|
||||
// REST API Routes
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', async (req, res) => {
|
||||
const allServers = await getAllPublicServers();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: Date.now(),
|
||||
serverCount: allServers.length,
|
||||
connectedUsers: connectedUsers.size,
|
||||
connectedUsers: connectedUsers.size
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,6 +75,7 @@ app.get('/api/time', (req, res) => {
|
||||
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' });
|
||||
}
|
||||
@@ -80,6 +83,7 @@ app.get('/api/image-proxy', async (req, res) => {
|
||||
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) {
|
||||
@@ -87,12 +91,14 @@ app.get('/api/image-proxy', async (req, res) => {
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
@@ -104,6 +110,7 @@ app.get('/api/image-proxy', async (req, res) => {
|
||||
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' });
|
||||
}
|
||||
@@ -112,10 +119,17 @@ app.get('/api/image-proxy', async (req, res) => {
|
||||
// Auth
|
||||
app.post('/api/users/register', async (req, res) => {
|
||||
const { username, password, displayName } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: 'Missing username/password' });
|
||||
|
||||
if (!username || !password)
|
||||
return res.status(400).json({ error: 'Missing username/password' });
|
||||
|
||||
const exists = await getUserByUsername(username);
|
||||
if (exists) return res.status(409).json({ error: 'Username taken' });
|
||||
|
||||
if (exists)
|
||||
return res.status(409).json({ error: 'Username taken' });
|
||||
|
||||
const user = { id: uuidv4(), username, passwordHash: hashPassword(password), displayName: displayName || username, createdAt: Date.now() };
|
||||
|
||||
await createUser(user);
|
||||
res.status(201).json({ id: user.id, username: user.username, displayName: user.displayName });
|
||||
});
|
||||
@@ -123,7 +137,10 @@ app.post('/api/users/register', async (req, res) => {
|
||||
app.post('/api/users/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = await getUserByUsername(username);
|
||||
if (!user || user.passwordHash !== hashPassword(password)) return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
if (!user || user.passwordHash !== hashPassword(password))
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
res.json({ id: user.id, username: user.username, displayName: user.displayName });
|
||||
});
|
||||
|
||||
@@ -137,20 +154,25 @@ app.get('/api/servers', async (req, res) => {
|
||||
.filter(s => {
|
||||
if (q) {
|
||||
const query = String(q).toLowerCase();
|
||||
|
||||
return s.name.toLowerCase().includes(query) ||
|
||||
s.description?.toLowerCase().includes(query);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter(s => {
|
||||
if (tags) {
|
||||
const tagList = String(tags).split(',');
|
||||
|
||||
return tagList.some(t => s.tags.includes(t));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const total = results.length;
|
||||
|
||||
results = results.slice(Number(offset), Number(offset) + Number(limit));
|
||||
|
||||
res.json({ servers: results, total, limit: Number(limit), offset: Number(offset) });
|
||||
@@ -176,7 +198,7 @@ app.post('/api/servers', async (req, res) => {
|
||||
currentUsers: 0,
|
||||
tags: tags ?? [],
|
||||
createdAt: Date.now(),
|
||||
lastSeen: Date.now(),
|
||||
lastSeen: Date.now()
|
||||
};
|
||||
|
||||
await upsertServer(server);
|
||||
@@ -187,8 +209,8 @@ app.post('/api/servers', async (req, res) => {
|
||||
app.put('/api/servers/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, ...updates } = req.body;
|
||||
|
||||
const server = await getServerById(id);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -198,6 +220,7 @@ app.put('/api/servers/:id', async (req, res) => {
|
||||
}
|
||||
|
||||
const updated: ServerInfo = { ...server, ...updates, lastSeen: Date.now() };
|
||||
|
||||
await upsertServer(updated);
|
||||
res.json(updated);
|
||||
});
|
||||
@@ -206,16 +229,18 @@ app.put('/api/servers/:id', async (req, res) => {
|
||||
app.post('/api/servers/:id/heartbeat', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { currentUsers } = req.body;
|
||||
|
||||
const server = await getServerById(id);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
|
||||
server.lastSeen = Date.now();
|
||||
|
||||
if (typeof currentUsers === 'number') {
|
||||
server.currentUsers = currentUsers;
|
||||
}
|
||||
|
||||
await upsertServer(server);
|
||||
|
||||
res.json({ ok: true });
|
||||
@@ -225,8 +250,8 @@ app.post('/api/servers/:id/heartbeat', async (req, res) => {
|
||||
app.delete('/api/servers/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId } = req.body;
|
||||
|
||||
const server = await getServerById(id);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -243,8 +268,8 @@ app.delete('/api/servers/:id', async (req, res) => {
|
||||
app.post('/api/servers/:id/join', async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { userId, userPublicKey, displayName } = req.body;
|
||||
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -257,7 +282,7 @@ app.post('/api/servers/:id/join', async (req, res) => {
|
||||
userPublicKey,
|
||||
displayName,
|
||||
status: server.isPrivate ? 'pending' : 'approved',
|
||||
createdAt: Date.now(),
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
await createJoinRequest(request);
|
||||
@@ -266,7 +291,7 @@ app.post('/api/servers/:id/join', async (req, res) => {
|
||||
if (server.isPrivate) {
|
||||
notifyServerOwner(server.ownerId, {
|
||||
type: 'join_request',
|
||||
request,
|
||||
request
|
||||
});
|
||||
}
|
||||
|
||||
@@ -277,8 +302,8 @@ app.post('/api/servers/:id/join', async (req, res) => {
|
||||
app.get('/api/servers/:id/requests', async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { ownerId } = req.query;
|
||||
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -288,6 +313,7 @@ app.get('/api/servers/:id/requests', async (req, res) => {
|
||||
}
|
||||
|
||||
const requests = await getPendingRequestsForServer(serverId);
|
||||
|
||||
res.json({ requests });
|
||||
});
|
||||
|
||||
@@ -295,13 +321,14 @@ app.get('/api/servers/:id/requests', async (req, res) => {
|
||||
app.put('/api/requests/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, status } = req.body;
|
||||
|
||||
const request = await getJoinRequestById(id);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Request not found' });
|
||||
}
|
||||
|
||||
const server = await getServerById(request.serverId);
|
||||
|
||||
if (!server || server.ownerId !== ownerId) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
@@ -312,7 +339,7 @@ app.put('/api/requests/:id', async (req, res) => {
|
||||
// Notify the requester
|
||||
notifyUser(request.userId, {
|
||||
type: 'request_update',
|
||||
request: updated,
|
||||
request: updated
|
||||
});
|
||||
|
||||
res.json(updated);
|
||||
@@ -325,16 +352,19 @@ function buildServer() {
|
||||
const certDir = path.resolve(__dirname, '..', '..', '.certs');
|
||||
const certFile = path.join(certDir, 'localhost.crt');
|
||||
const keyFile = path.join(certDir, 'localhost.key');
|
||||
|
||||
if (!fs.existsSync(certFile) || !fs.existsSync(keyFile)) {
|
||||
console.error(`SSL=true but certs not found in ${certDir}`);
|
||||
console.error('Run ./generate-cert.sh first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return createHttpsServer(
|
||||
{ cert: fs.readFileSync(certFile), key: fs.readFileSync(keyFile) },
|
||||
app,
|
||||
app
|
||||
);
|
||||
}
|
||||
|
||||
return createHttpServer(app);
|
||||
}
|
||||
|
||||
@@ -343,11 +373,13 @@ const wss = new WebSocketServer({ server });
|
||||
|
||||
wss.on('connection', (ws: WebSocket) => {
|
||||
const connectionId = uuidv4();
|
||||
|
||||
connectedUsers.set(connectionId, { oderId: connectionId, ws, serverIds: new Set() });
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
|
||||
handleWebSocketMessage(connectionId, message);
|
||||
} catch (err) {
|
||||
console.error('Invalid WebSocket message:', err);
|
||||
@@ -356,6 +388,7 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
|
||||
ws.on('close', () => {
|
||||
const user = connectedUsers.get(connectionId);
|
||||
|
||||
if (user) {
|
||||
// Notify all servers the user was a member of
|
||||
user.serverIds.forEach((sid) => {
|
||||
@@ -363,10 +396,11 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
type: 'user_left',
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName,
|
||||
serverId: sid,
|
||||
serverId: sid
|
||||
}, user.oderId);
|
||||
});
|
||||
}
|
||||
|
||||
connectedUsers.delete(connectionId);
|
||||
});
|
||||
|
||||
@@ -376,7 +410,9 @@ wss.on('connection', (ws: WebSocket) => {
|
||||
|
||||
function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
const user = connectedUsers.get(connectionId);
|
||||
if (!user) return;
|
||||
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
switch (message.type) {
|
||||
case 'identify':
|
||||
@@ -391,6 +427,7 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
case 'join_server': {
|
||||
const sid = message.serverId;
|
||||
const isNew = !user.serverIds.has(sid);
|
||||
|
||||
user.serverIds.add(sid);
|
||||
user.viewedServerId = sid;
|
||||
connectedUsers.set(connectionId, user);
|
||||
@@ -405,7 +442,7 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
user.ws.send(JSON.stringify({
|
||||
type: 'server_users',
|
||||
serverId: sid,
|
||||
users: usersInServer,
|
||||
users: usersInServer
|
||||
}));
|
||||
|
||||
// Only broadcast user_joined if this is a brand-new join (not a re-view)
|
||||
@@ -414,15 +451,17 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
type: 'user_joined',
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName || 'Anonymous',
|
||||
serverId: sid,
|
||||
serverId: sid
|
||||
}, user.oderId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'view_server': {
|
||||
// Just switch the viewed server without joining/leaving
|
||||
const viewSid = message.serverId;
|
||||
|
||||
user.viewedServerId = viewSid;
|
||||
connectedUsers.set(connectionId, user);
|
||||
console.log(`User ${user.displayName || 'Anonymous'} (${user.oderId}) viewing server ${viewSid}`);
|
||||
@@ -435,27 +474,31 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
user.ws.send(JSON.stringify({
|
||||
type: 'server_users',
|
||||
serverId: viewSid,
|
||||
users: viewUsers,
|
||||
users: viewUsers
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'leave_server': {
|
||||
const leaveSid = message.serverId || user.viewedServerId;
|
||||
|
||||
if (leaveSid) {
|
||||
user.serverIds.delete(leaveSid);
|
||||
|
||||
if (user.viewedServerId === leaveSid) {
|
||||
user.viewedServerId = undefined;
|
||||
}
|
||||
|
||||
connectedUsers.set(connectionId, user);
|
||||
|
||||
broadcastToServer(leaveSid, {
|
||||
type: 'user_left',
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName || 'Anonymous',
|
||||
serverId: leaveSid,
|
||||
serverId: leaveSid
|
||||
}, user.oderId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -465,21 +508,24 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
// Forward signaling messages to specific peer
|
||||
console.log(`Forwarding ${message.type} from ${user.oderId} to ${message.targetUserId}`);
|
||||
const targetUser = findUserByUserId(message.targetUserId);
|
||||
|
||||
if (targetUser) {
|
||||
targetUser.ws.send(JSON.stringify({
|
||||
...message,
|
||||
fromUserId: user.oderId,
|
||||
fromUserId: user.oderId
|
||||
}));
|
||||
console.log(`Successfully forwarded ${message.type} to ${message.targetUserId}`);
|
||||
} else {
|
||||
console.log(`Target user ${message.targetUserId} not found. Connected users:`,
|
||||
Array.from(connectedUsers.values()).map(u => ({ oderId: u.oderId, displayName: u.displayName })));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'chat_message': {
|
||||
// Broadcast chat message to all users in the server
|
||||
const chatSid = message.serverId || user.viewedServerId;
|
||||
|
||||
if (chatSid && user.serverIds.has(chatSid)) {
|
||||
broadcastToServer(chatSid, {
|
||||
type: 'chat_message',
|
||||
@@ -487,23 +533,26 @@ function handleWebSocketMessage(connectionId: string, message: any): void {
|
||||
message: message.message,
|
||||
senderId: user.oderId,
|
||||
senderName: user.displayName,
|
||||
timestamp: Date.now(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'typing': {
|
||||
// Broadcast typing indicator
|
||||
const typingSid = message.serverId || user.viewedServerId;
|
||||
|
||||
if (typingSid && user.serverIds.has(typingSid)) {
|
||||
broadcastToServer(typingSid, {
|
||||
type: 'user_typing',
|
||||
serverId: typingSid,
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName,
|
||||
displayName: user.displayName
|
||||
}, user.oderId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -524,6 +573,7 @@ function broadcastToServer(serverId: string, message: any, excludeOderId?: strin
|
||||
|
||||
function notifyServerOwner(ownerId: string, message: any): void {
|
||||
const owner = findUserByUserId(ownerId);
|
||||
|
||||
if (owner) {
|
||||
owner.ws.send(JSON.stringify(message));
|
||||
}
|
||||
@@ -531,6 +581,7 @@ function notifyServerOwner(ownerId: string, message: any): void {
|
||||
|
||||
function notifyUser(oderId: string, message: any): void {
|
||||
const user = findUserByUserId(oderId);
|
||||
|
||||
if (user) {
|
||||
user.ws.send(JSON.stringify(message));
|
||||
}
|
||||
@@ -543,7 +594,7 @@ function findUserByUserId(oderId: string): ConnectedUser | undefined {
|
||||
// Cleanup stale join requests periodically (older than 24 h)
|
||||
setInterval(() => {
|
||||
deleteStaleJoinRequests(24 * 60 * 60 * 1000).catch(err =>
|
||||
console.error('Failed to clean up stale join requests:', err),
|
||||
console.error('Failed to clean up stale join requests:', err)
|
||||
);
|
||||
}, 60 * 1000);
|
||||
|
||||
@@ -551,11 +602,13 @@ initDB().then(() => {
|
||||
server.listen(PORT, () => {
|
||||
const proto = USE_SSL ? 'https' : 'http';
|
||||
const wsProto = USE_SSL ? 'wss' : 'ws';
|
||||
|
||||
console.log(`🚀 MetoYou signaling server running on port ${PORT} (SSL=${USE_SSL})`);
|
||||
console.log(` REST API: ${proto}://localhost:${PORT}/api`);
|
||||
console.log(` WebSocket: ${wsProto}://localhost:${PORT}`);
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('Failed to initialize database:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to initialize database:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user