Spaces:
Sleeping
Sleeping
File size: 6,808 Bytes
839aef2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | /**
* 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();
|