rr-locker-api / src /server.js
MD.Riad Rayhan
Node.js + Socket.IO backend (migrated from FastAPI)
839aef2 verified
Raw
History Blame Contribute Delete
6.81 kB
/**
* RR Locker API β€” Node.js entry point.
*
* Express REST API (ported from FastAPI) + Socket.IO realtime layer
* (replaces the old WebSocket hubs) + WebRTC signaling for live screen/audio.
* Serves the built Vite dashboard as a SPA and exposes the same /api/v1 surface
* the Android app and dashboard already use.
*/
process.env.TZ = 'UTC';
import http from 'http';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import express from 'express';
import cors from 'cors';
import morgan from 'morgan';
import { settings } from './config.js';
import { sequelize, syncTables, User } from './models/index.js';
import { getPasswordHash } from './utils/security.js';
import { initFirebase } from './services/fcmService.js';
import { initRealtime } from './services/realtime.js';
import { initStreamBridge } from './services/streamBridge.js';
import { startCron } from './services/cron.js';
import registerAuthRoutes from './routes/auth.js';
import registerDeviceRoutes from './routes/devices.js';
import registerCustomerRoutes from './routes/customers.js';
import registerEmiRoutes from './routes/emi.js';
import registerCommandRoutes from './routes/commands.js';
import registerUserRoutes from './routes/users.js';
import registerEnrollmentRoutes from './routes/enrollment.js';
import registerAppDistributionRoutes from './routes/appDistribution.js';
import registerAmapiRoutes from './routes/amapi.js';
import registerMetadataRoutes from './routes/metadata.js';
import registerScreenRoutes from './routes/screen.js';
import registerFileRoutes from './routes/files.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DASHBOARD_DIR = path.resolve(__dirname, '..', 'static');
const app = express();
// ── Middleware ───────────────────────────────────────────
app.use(cors({ origin: settings.CORS_ORIGINS, credentials: true }));
app.use(express.json({ limit: '25mb' }));
app.use(express.urlencoded({ extended: true, limit: '25mb' }));
if (settings.DEBUG) app.use(morgan('dev'));
// ── API router (/api/v1) ─────────────────────────────────
const api = express.Router();
registerAuthRoutes(api);
registerDeviceRoutes(api);
registerCustomerRoutes(api);
registerEmiRoutes(api);
registerCommandRoutes(api);
registerUserRoutes(api);
registerEnrollmentRoutes(api);
registerAppDistributionRoutes(api);
registerAmapiRoutes(api);
registerMetadataRoutes(api);
registerScreenRoutes(api);
registerFileRoutes(api);
app.use('/api/v1', api);
// ── Health ───────────────────────────────────────────────
app.get('/health', (req, res) => res.json({ status: 'healthy' }));
app.get('/api/v1/health', (req, res) => res.json({ status: 'healthy' }));
// ── Dashboard SPA ────────────────────────────────────────
const NO_CACHE = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
};
function serveIndex(res) {
res.set(NO_CACHE);
res.sendFile(path.join(DASHBOARD_DIR, 'index.html'));
}
if (fs.existsSync(DASHBOARD_DIR)) {
app.use('/assets', express.static(path.join(DASHBOARD_DIR, 'assets')));
app.get('/', (req, res) => serveIndex(res));
// SPA catch-all (Express 4 wildcard) β€” serve real files, else index.html.
app.get(/^\/(?!api\/|assets\/|health$).*/, (req, res) => {
const filePath = path.join(DASHBOARD_DIR, req.path);
if (
fs.existsSync(filePath)
&& fs.statSync(filePath).isFile()
&& path.basename(filePath) !== 'index.html'
) {
return res.sendFile(filePath);
}
return serveIndex(res);
});
} else {
app.get('/', (req, res) => res.json({
app: settings.APP_NAME,
version: settings.VERSION,
status: 'running',
}));
console.warn(`Dashboard static files not found at ${DASHBOARD_DIR}`);
}
// ── Error handler (mirrors FastAPI {detail} bodies) ──────
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
const status = err.status || err.statusCode || 500;
if (status >= 500) console.error(err);
res.status(status).json({ detail: err.detail ?? err.message ?? 'Internal Server Error' });
});
// ── Startup helpers ──────────────────────────────────────
async function createDefaultAdmin() {
try {
const existing = await User.findOne({ where: { username: 'admin' } });
if (!existing) {
await User.create({
email: 'admin@emilocker.com',
username: 'admin',
hashed_password: getPasswordHash('admin123'),
full_name: 'Administrator',
role: 'admin',
is_active: true,
});
console.log('Default admin user created: admin / admin123');
} else {
console.log('Admin user already exists');
}
} catch (e) {
console.error('Error creating admin user:', e.message);
}
}
function startKeepAlive() {
const externalUrl = settings.APP_EXTERNAL_URL;
if (!externalUrl) {
console.log('APP_EXTERNAL_URL not set β€” keep-alive disabled');
return;
}
const healthUrl = `${externalUrl}/health`;
console.log(`Keep-alive started: pinging ${healthUrl} every 12 minutes`);
const ping = () => {
try {
const mod = healthUrl.startsWith('https') ? 'https' : 'http';
import(mod).then((m) => {
m.get(healthUrl, (r) => { r.resume(); }).on('error', () => {});
});
} catch { /* ignore */ }
};
setTimeout(() => { ping(); setInterval(ping, 12 * 60 * 1000); }, 120 * 1000);
}
async function main() {
console.log('Starting RR Locker API...');
try {
await sequelize.authenticate();
await syncTables();
console.log('Database tables created/verified');
} catch (e) {
console.error('Database initialization error:', e.message);
}
await createDefaultAdmin();
try {
initFirebase();
} catch (e) {
console.warn('Firebase initialization warning:', e.message);
}
const server = http.createServer(app);
const io = initRealtime(server);
initStreamBridge(server, io);
try {
startCron();
} catch (e) {
console.warn('Cron scheduler warning:', e.message);
}
startKeepAlive();
server.listen(settings.PORT, '0.0.0.0', () => {
console.log(`RR Locker API listening on :${settings.PORT}`);
});
}
main();