Big commit
This commit is contained in:
305
server/dist/index.js
vendored
305
server/dist/index.js
vendored
@@ -12,71 +12,85 @@ const app = (0, express_1.default)();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.use((0, cors_1.default)());
|
||||
app.use(express_1.default.json());
|
||||
const servers = new Map();
|
||||
const joinRequests = new Map();
|
||||
const connectedUsers = new Map();
|
||||
// Persistence
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const DATA_DIR = path_1.default.join(process.cwd(), 'data');
|
||||
const SERVERS_FILE = path_1.default.join(DATA_DIR, 'servers.json');
|
||||
const USERS_FILE = path_1.default.join(DATA_DIR, 'users.json');
|
||||
function ensureDataDir() {
|
||||
if (!fs_1.default.existsSync(DATA_DIR))
|
||||
fs_1.default.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
function saveServers() {
|
||||
ensureDataDir();
|
||||
fs_1.default.writeFileSync(SERVERS_FILE, JSON.stringify(Array.from(servers.values()), null, 2));
|
||||
}
|
||||
function loadServers() {
|
||||
ensureDataDir();
|
||||
if (fs_1.default.existsSync(SERVERS_FILE)) {
|
||||
const raw = fs_1.default.readFileSync(SERVERS_FILE, 'utf-8');
|
||||
const list = JSON.parse(raw);
|
||||
list.forEach(s => servers.set(s.id, s));
|
||||
}
|
||||
}
|
||||
// Database
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
const db_1 = require("./db");
|
||||
function hashPassword(pw) { return crypto_1.default.createHash('sha256').update(pw).digest('hex'); }
|
||||
// REST API Routes
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
app.get('/api/health', async (req, res) => {
|
||||
const allServers = await (0, db_1.getAllPublicServers)();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: Date.now(),
|
||||
serverCount: servers.size,
|
||||
serverCount: allServers.length,
|
||||
connectedUsers: connectedUsers.size,
|
||||
});
|
||||
});
|
||||
let authUsers = [];
|
||||
function saveUsers() { ensureDataDir(); fs_1.default.writeFileSync(USERS_FILE, JSON.stringify(authUsers, null, 2)); }
|
||||
function loadUsers() { ensureDataDir(); if (fs_1.default.existsSync(USERS_FILE)) {
|
||||
authUsers = JSON.parse(fs_1.default.readFileSync(USERS_FILE, 'utf-8'));
|
||||
} }
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
function hashPassword(pw) { return crypto_1.default.createHash('sha256').update(pw).digest('hex'); }
|
||||
app.post('/api/users/register', (req, res) => {
|
||||
// Time endpoint for clock synchronization
|
||||
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?.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' });
|
||||
}
|
||||
});
|
||||
// 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 (authUsers.find(u => u.username === username))
|
||||
const exists = await (0, db_1.getUserByUsername)(username);
|
||||
if (exists)
|
||||
return res.status(409).json({ error: 'Username taken' });
|
||||
const user = { id: (0, uuid_1.v4)(), username, passwordHash: hashPassword(password), displayName: displayName || username, createdAt: Date.now() };
|
||||
authUsers.push(user);
|
||||
saveUsers();
|
||||
await (0, db_1.createUser)(user);
|
||||
res.status(201).json({ id: user.id, username: user.username, displayName: user.displayName });
|
||||
});
|
||||
app.post('/api/users/login', (req, res) => {
|
||||
app.post('/api/users/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = authUsers.find(u => u.username === username && u.passwordHash === hashPassword(password));
|
||||
if (!user)
|
||||
const user = await (0, db_1.getUserByUsername)(username);
|
||||
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 });
|
||||
});
|
||||
// Search servers
|
||||
app.get('/api/servers', (req, res) => {
|
||||
app.get('/api/servers', async (req, res) => {
|
||||
const { q, tags, limit = 20, offset = 0 } = req.query;
|
||||
let results = Array.from(servers.values())
|
||||
.filter(s => !s.isPrivate)
|
||||
let results = await (0, db_1.getAllPublicServers)();
|
||||
results = results
|
||||
.filter(s => {
|
||||
if (q) {
|
||||
const query = String(q).toLowerCase();
|
||||
@@ -92,18 +106,16 @@ app.get('/api/servers', (req, res) => {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Keep servers visible permanently until deleted; do not filter by lastSeen
|
||||
const total = results.length;
|
||||
results = results.slice(Number(offset), Number(offset) + Number(limit));
|
||||
res.json({ servers: results, total, limit: Number(limit), offset: Number(offset) });
|
||||
});
|
||||
// Register a server
|
||||
app.post('/api/servers', (req, res) => {
|
||||
app.post('/api/servers', async (req, res) => {
|
||||
const { id: clientId, name, description, ownerId, ownerPublicKey, isPrivate, maxUsers, tags } = req.body;
|
||||
if (!name || !ownerId || !ownerPublicKey) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
// Use client-provided ID if available, otherwise generate one
|
||||
const id = clientId || (0, uuid_1.v4)();
|
||||
const server = {
|
||||
id,
|
||||
@@ -118,15 +130,14 @@ app.post('/api/servers', (req, res) => {
|
||||
createdAt: Date.now(),
|
||||
lastSeen: Date.now(),
|
||||
};
|
||||
servers.set(id, server);
|
||||
saveServers();
|
||||
await (0, db_1.upsertServer)(server);
|
||||
res.status(201).json(server);
|
||||
});
|
||||
// Update server
|
||||
app.put('/api/servers/:id', (req, res) => {
|
||||
app.put('/api/servers/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, ...updates } = req.body;
|
||||
const server = servers.get(id);
|
||||
const server = await (0, db_1.getServerById)(id);
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -134,15 +145,14 @@ app.put('/api/servers/:id', (req, res) => {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
const updated = { ...server, ...updates, lastSeen: Date.now() };
|
||||
servers.set(id, updated);
|
||||
saveServers();
|
||||
await (0, db_1.upsertServer)(updated);
|
||||
res.json(updated);
|
||||
});
|
||||
// Heartbeat - keep server alive
|
||||
app.post('/api/servers/:id/heartbeat', (req, res) => {
|
||||
app.post('/api/servers/:id/heartbeat', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { currentUsers } = req.body;
|
||||
const server = servers.get(id);
|
||||
const server = await (0, db_1.getServerById)(id);
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -150,30 +160,28 @@ app.post('/api/servers/:id/heartbeat', (req, res) => {
|
||||
if (typeof currentUsers === 'number') {
|
||||
server.currentUsers = currentUsers;
|
||||
}
|
||||
servers.set(id, server);
|
||||
saveServers();
|
||||
await (0, db_1.upsertServer)(server);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
// Remove server
|
||||
app.delete('/api/servers/:id', (req, res) => {
|
||||
app.delete('/api/servers/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId } = req.body;
|
||||
const server = servers.get(id);
|
||||
const server = await (0, db_1.getServerById)(id);
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
if (server.ownerId !== ownerId) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
servers.delete(id);
|
||||
saveServers();
|
||||
await (0, db_1.deleteServer)(id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
// Request to join a server
|
||||
app.post('/api/servers/:id/join', (req, res) => {
|
||||
app.post('/api/servers/:id/join', async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { userId, userPublicKey, displayName } = req.body;
|
||||
const server = servers.get(serverId);
|
||||
const server = await (0, db_1.getServerById)(serverId);
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
@@ -187,7 +195,7 @@ app.post('/api/servers/:id/join', (req, res) => {
|
||||
status: server.isPrivate ? 'pending' : 'approved',
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
joinRequests.set(requestId, request);
|
||||
await (0, db_1.createJoinRequest)(request);
|
||||
// Notify server owner via WebSocket
|
||||
if (server.isPrivate) {
|
||||
notifyServerOwner(server.ownerId, {
|
||||
@@ -198,70 +206,72 @@ app.post('/api/servers/:id/join', (req, res) => {
|
||||
res.status(201).json(request);
|
||||
});
|
||||
// Get join requests for a server
|
||||
app.get('/api/servers/:id/requests', (req, res) => {
|
||||
app.get('/api/servers/:id/requests', async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { ownerId } = req.query;
|
||||
const server = servers.get(serverId);
|
||||
const server = await (0, db_1.getServerById)(serverId);
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
if (server.ownerId !== ownerId) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
const requests = Array.from(joinRequests.values())
|
||||
.filter(r => r.serverId === serverId && r.status === 'pending');
|
||||
const requests = await (0, db_1.getPendingRequestsForServer)(serverId);
|
||||
res.json({ requests });
|
||||
});
|
||||
// Approve/reject join request
|
||||
app.put('/api/requests/:id', (req, res) => {
|
||||
app.put('/api/requests/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, status } = req.body;
|
||||
const request = joinRequests.get(id);
|
||||
const request = await (0, db_1.getJoinRequestById)(id);
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Request not found' });
|
||||
}
|
||||
const server = servers.get(request.serverId);
|
||||
const server = await (0, db_1.getServerById)(request.serverId);
|
||||
if (!server || server.ownerId !== ownerId) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
request.status = status;
|
||||
joinRequests.set(id, request);
|
||||
await (0, db_1.updateJoinRequestStatus)(id, status);
|
||||
const updated = { ...request, status };
|
||||
// Notify the requester
|
||||
notifyUser(request.userId, {
|
||||
type: 'request_update',
|
||||
request,
|
||||
request: updated,
|
||||
});
|
||||
res.json(request);
|
||||
res.json(updated);
|
||||
});
|
||||
// WebSocket Server for real-time signaling
|
||||
const server = (0, http_1.createServer)(app);
|
||||
const wss = new ws_1.WebSocketServer({ server });
|
||||
wss.on('connection', (ws) => {
|
||||
const oderId = (0, uuid_1.v4)();
|
||||
connectedUsers.set(oderId, { oderId, ws });
|
||||
const connectionId = (0, uuid_1.v4)();
|
||||
connectedUsers.set(connectionId, { oderId: connectionId, ws, serverIds: new Set() });
|
||||
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);
|
||||
if (user?.serverId) {
|
||||
// Notify others in the room
|
||||
broadcastToServer(user.serverId, {
|
||||
type: 'user_left',
|
||||
oderId,
|
||||
displayName: user.displayName,
|
||||
}, oderId);
|
||||
const user = connectedUsers.get(connectionId);
|
||||
if (user) {
|
||||
// Notify all servers the user was a member of
|
||||
user.serverIds.forEach((sid) => {
|
||||
broadcastToServer(sid, {
|
||||
type: 'user_left',
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName,
|
||||
serverId: sid,
|
||||
}, user.oderId);
|
||||
});
|
||||
}
|
||||
connectedUsers.delete(oderId);
|
||||
connectedUsers.delete(connectionId);
|
||||
});
|
||||
// Send connection acknowledgment
|
||||
ws.send(JSON.stringify({ type: 'connected', oderId }));
|
||||
// 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, message) {
|
||||
const user = connectedUsers.get(connectionId);
|
||||
@@ -276,38 +286,68 @@ function handleWebSocketMessage(connectionId, message) {
|
||||
connectedUsers.set(connectionId, user);
|
||||
console.log(`User identified: ${user.displayName} (${user.oderId})`);
|
||||
break;
|
||||
case 'join_server':
|
||||
user.serverId = message.serverId;
|
||||
case 'join_server': {
|
||||
const sid = message.serverId;
|
||||
const isNew = !user.serverIds.has(sid);
|
||||
user.serverIds.add(sid);
|
||||
user.viewedServerId = sid;
|
||||
connectedUsers.set(connectionId, user);
|
||||
console.log(`User ${user.displayName} (${user.oderId}) joined server ${message.serverId}`);
|
||||
// Get list of current users in server (exclude this user by oderId)
|
||||
console.log(`User ${user.displayName || 'Anonymous'} (${user.oderId}) joined server ${sid} (new=${isNew})`);
|
||||
// Always send the current user list for this server
|
||||
const usersInServer = Array.from(connectedUsers.values())
|
||||
.filter(u => u.serverId === message.serverId && u.oderId !== user.oderId)
|
||||
.map(u => ({ oderId: u.oderId, displayName: u.displayName }));
|
||||
console.log(`Sending server_users to ${user.displayName}:`, usersInServer);
|
||||
.filter(u => u.serverIds.has(sid) && u.oderId !== user.oderId && u.displayName)
|
||||
.map(u => ({ oderId: u.oderId, displayName: u.displayName || 'Anonymous' }));
|
||||
console.log(`Sending server_users to ${user.displayName || 'Anonymous'}:`, usersInServer);
|
||||
user.ws.send(JSON.stringify({
|
||||
type: 'server_users',
|
||||
serverId: sid,
|
||||
users: usersInServer,
|
||||
}));
|
||||
// Notify others (exclude by oderId, not connectionId)
|
||||
broadcastToServer(message.serverId, {
|
||||
type: 'user_joined',
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName,
|
||||
}, user.oderId);
|
||||
break;
|
||||
case 'leave_server':
|
||||
const oldServerId = user.serverId;
|
||||
user.serverId = undefined;
|
||||
connectedUsers.set(connectionId, user);
|
||||
if (oldServerId) {
|
||||
broadcastToServer(oldServerId, {
|
||||
type: 'user_left',
|
||||
// Only broadcast user_joined if this is a brand-new join (not a re-view)
|
||||
if (isNew) {
|
||||
broadcastToServer(sid, {
|
||||
type: 'user_joined',
|
||||
oderId: user.oderId,
|
||||
displayName: user.displayName,
|
||||
displayName: user.displayName || 'Anonymous',
|
||||
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}`);
|
||||
// Send current user list for the viewed server
|
||||
const viewUsers = Array.from(connectedUsers.values())
|
||||
.filter(u => u.serverIds.has(viewSid) && u.oderId !== user.oderId && u.displayName)
|
||||
.map(u => ({ oderId: u.oderId, displayName: u.displayName || 'Anonymous' }));
|
||||
user.ws.send(JSON.stringify({
|
||||
type: 'server_users',
|
||||
serverId: viewSid,
|
||||
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,
|
||||
}, user.oderId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'offer':
|
||||
case 'answer':
|
||||
case 'ice_candidate':
|
||||
@@ -325,11 +365,13 @@ function handleWebSocketMessage(connectionId, message) {
|
||||
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':
|
||||
case 'chat_message': {
|
||||
// Broadcast chat message to all users in the server
|
||||
if (user.serverId) {
|
||||
broadcastToServer(user.serverId, {
|
||||
const chatSid = message.serverId || user.viewedServerId;
|
||||
if (chatSid && user.serverIds.has(chatSid)) {
|
||||
broadcastToServer(chatSid, {
|
||||
type: 'chat_message',
|
||||
serverId: chatSid,
|
||||
message: message.message,
|
||||
senderId: user.oderId,
|
||||
senderName: user.displayName,
|
||||
@@ -337,16 +379,20 @@ function handleWebSocketMessage(connectionId, message) {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'typing':
|
||||
}
|
||||
case 'typing': {
|
||||
// Broadcast typing indicator
|
||||
if (user.serverId) {
|
||||
broadcastToServer(user.serverId, {
|
||||
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,
|
||||
}, user.oderId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
@@ -354,7 +400,7 @@ function handleWebSocketMessage(connectionId, message) {
|
||||
function broadcastToServer(serverId, message, excludeOderId) {
|
||||
console.log(`Broadcasting to server ${serverId}, excluding ${excludeOderId}:`, message.type);
|
||||
connectedUsers.forEach((user) => {
|
||||
if (user.serverId === serverId && user.oderId !== excludeOderId) {
|
||||
if (user.serverIds.has(serverId) && user.oderId !== excludeOderId) {
|
||||
console.log(` -> Sending to ${user.displayName} (${user.oderId})`);
|
||||
user.ws.send(JSON.stringify(message));
|
||||
}
|
||||
@@ -375,21 +421,18 @@ function notifyUser(oderId, message) {
|
||||
function findUserByUserId(oderId) {
|
||||
return Array.from(connectedUsers.values()).find(u => u.oderId === oderId);
|
||||
}
|
||||
// Cleanup old data periodically
|
||||
// Simple cleanup only for stale join requests (keep servers permanent)
|
||||
// Cleanup stale join requests periodically (older than 24 h)
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
joinRequests.forEach((request, id) => {
|
||||
if (now - request.createdAt > 24 * 60 * 60 * 1000) {
|
||||
joinRequests.delete(id);
|
||||
}
|
||||
});
|
||||
(0, db_1.deleteStaleJoinRequests)(24 * 60 * 60 * 1000).catch(err => console.error('Failed to clean up stale join requests:', err));
|
||||
}, 60 * 1000);
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🚀 MetoYou signaling server running on port ${PORT}`);
|
||||
console.log(` REST API: http://localhost:${PORT}/api`);
|
||||
console.log(` WebSocket: ws://localhost:${PORT}`);
|
||||
// Load servers on startup
|
||||
loadServers();
|
||||
(0, db_1.initDB)().then(() => {
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🚀 MetoYou signaling server running on port ${PORT}`);
|
||||
console.log(` REST API: http://localhost:${PORT}/api`);
|
||||
console.log(` WebSocket: ws://localhost:${PORT}`);
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('Failed to initialize database:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
Reference in New Issue
Block a user