Spaces:
Sleeping
Sleeping
| /** | |
| * 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(); | |