auth

Main authentication module for my projects
git clone git://git.finwo.net/app/auth
Log | Files | Refs

index.js (4074B)


      1 import bodyParser from 'body-parser';
      2 import cors from 'cors';
      3 import fs from 'node:fs';
      4 import http from 'node:http';
      5 import path from 'node:path';
      6 import { fileURLToPath, pathToFileURL } from 'node:url';
      7 import morgan from 'morgan';
      8 import scandir from '@finwo/scandir';
      9 import serveStatic from 'serve-static';
     10 
     11 import { app } from './app.js';
     12 import { loadConfig } from './lib/config.js';
     13 import { closeRealms, initRealms } from './lib/realm.js';
     14 
     15 const here = path.dirname(fileURLToPath(import.meta.url));
     16 const dist = path.join(here, '..', 'web', 'dist');
     17 
     18 const cfg = loadConfig();
     19 app.config = cfg;
     20 
     21 async function loadModules(dir) {
     22   const files = [];
     23   scandir.sync(dir, (file) => files.push(file), ['js'], false);
     24   files.sort();
     25   const modules = [];
     26   for (const file of files) {
     27     modules.push(await import(pathToFileURL(file).href));
     28   }
     29   return modules;
     30 }
     31 
     32 for (const mod of await loadModules(path.join(here, 'lib', 'http'))) {
     33   Object.assign(app, mod);
     34 }
     35 
     36 app.use(morgan('tiny'));
     37 app.use(cors());
     38 app.use(bodyParser.json({ limit: '16kb' }));
     39 app.use(bodyParser.urlencoded({ extended: false, limit: '16kb' }));
     40 
     41 const routes = [];
     42 for (const mod of await loadModules(path.join(here, 'controller'))) {
     43   const exported = mod.default ?? mod.route ?? mod;
     44   for (const route of Array.isArray(exported) ? exported : [exported]) {
     45     if (!route) continue;
     46     if (route.mount) {
     47       app.use(route.mount, route.handler);
     48       continue;
     49     }
     50     if (!route.method || !route.path) continue;
     51     if (route.name) {
     52       app.manifest.push({
     53         method: route.method,
     54         path: route.path,
     55         name: route.name,
     56       });
     57     }
     58     routes.push(route);
     59   }
     60 }
     61 
     62 routes.sort((A, B) => {
     63   const aparts = A.path.split('/');
     64   const bparts = B.path.split('/');
     65   for (let i = 0; i < Math.min(aparts.length, bparts.length); i += 1) {
     66     if (aparts[i].startsWith(':') && !bparts[i].startsWith(':')) return 1;
     67     if (!aparts[i].startsWith(':') && bparts[i].startsWith(':')) return -1;
     68   }
     69   return bparts.length - aparts.length;
     70 });
     71 
     72 for (const route of routes) {
     73   app[route.method](route.path, async (req, res, ...args) => {
     74     try {
     75       const result = await route.handler(req, res, ...args);
     76       if (result instanceof app.HttpResponse) result.send(res);
     77     } catch (e) {
     78       if (res.headersSent) {
     79         res.end();
     80         return;
     81       }
     82       if (e instanceof app.HttpError) {
     83         e.toResponse().send(res);
     84         return;
     85       }
     86       console.error(e);
     87       new app.HttpError(500, 'internal_error', 'Internal server error').toResponse().send(res);
     88     }
     89   });
     90 }
     91 
     92 const serve = serveStatic(dist, {
     93   index: ['index.htm', 'index.html'],
     94 });
     95 
     96 let indexContent = null;
     97 try {
     98   indexContent = fs.readFileSync(path.join(dist, 'index.html'));
     99 } catch {
    100   indexContent = null;
    101 }
    102 
    103 await initRealms(cfg);
    104 
    105 const server = http.createServer((req, res) => {
    106   app(req, res, (err) => {
    107     if (err) {
    108       const status = err.status || err.statusCode || 500;
    109       const code = err.type === 'entity.parse.failed' ? 'invalid_json' : 'internal_error';
    110       res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
    111       res.end(JSON.stringify({ error: { code, message: err.message || 'Request failed' } }));
    112       return;
    113     }
    114 
    115     serve(req, res, () => {
    116       if (!indexContent) {
    117         res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
    118         res.end('frontend build missing');
    119         return;
    120       }
    121       res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
    122       res.end(indexContent);
    123     });
    124   });
    125 });
    126 
    127 server.listen(cfg.server.port, cfg.server.host, () => {
    128   console.log(`Listening on ${cfg.server.host}:${cfg.server.port}`);
    129 });
    130 
    131 let shuttingDown = false;
    132 
    133 async function shutdown(signal) {
    134   if (shuttingDown) return;
    135   shuttingDown = true;
    136   console.log(`${signal} received, shutting down`);
    137   server.close();
    138   await closeRealms();
    139 }
    140 
    141 for (const signal of ['SIGINT', 'SIGTERM']) {
    142   process.once(signal, () => {
    143     shutdown(signal).finally(() => process.exit(0));
    144   });
    145 }