omegga 1.19.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -121,6 +121,7 @@ class Webserver {
|
|
|
121
121
|
this.app.use(bodyParser.json());
|
|
122
122
|
const openApi = express.Router();
|
|
123
123
|
const api = express.Router();
|
|
124
|
+
const requireSession = (req, res, next) => req.session ? next() : res.status(400).json({ message: "no session" });
|
|
124
125
|
openApi.get(
|
|
125
126
|
"/first",
|
|
126
127
|
async (_req, res) => res.json(await this.database.isFirstUser())
|
|
@@ -274,6 +275,7 @@ class Webserver {
|
|
|
274
275
|
res.status(e ? 500 : 200).json({});
|
|
275
276
|
});
|
|
276
277
|
});
|
|
278
|
+
this.app.use("/api/v1", requireSession);
|
|
277
279
|
this.app.use("/api/v1", openApi);
|
|
278
280
|
this.app.use("/api/v1", api);
|
|
279
281
|
this.app.use(
|
|
@@ -285,10 +287,10 @@ class Webserver {
|
|
|
285
287
|
);
|
|
286
288
|
metrics.default(this);
|
|
287
289
|
this.app.use(async (req, res) => {
|
|
288
|
-
if (req.session
|
|
290
|
+
if (req.session?.mfaPending) {
|
|
289
291
|
return res.sendFile(path.join(PUBLIC_PATH, "auth.html"));
|
|
290
292
|
}
|
|
291
|
-
const user = await this.database.findUserById(req.session
|
|
293
|
+
const user = await this.database.findUserById(req.session?.userId);
|
|
292
294
|
const isAuth = user && !user.isBanned;
|
|
293
295
|
if (isAuth) {
|
|
294
296
|
res.sendFile(path.join(PUBLIC_PATH, "app.html"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/webserver/backend/index.ts"],"sourcesContent":["/// <reference path=\"./better-sqlite3-session-store.d.ts\" />\nimport Logger from '@/logger';\nimport soft from '@/softconfig';\nimport { openDb } from '@/db/connection';\nimport { runMigrations } from '@/db/migrate';\nimport { importNedbIfNeeded } from '@/db/nedbImport';\nimport { type IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { type IServerStatus } from '@omegga/types';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport bodyParser from 'body-parser';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport express from 'express';\nimport expressSession from 'express-session';\nimport hasbin from 'hasbin';\nimport http from 'http';\nimport https from 'https';\nimport BetterSqlite3SessionStore from 'better-sqlite3-session-store';\nimport path from 'path';\nimport bcrypt from 'bcryptjs';\nimport {\n generateAuthenticationOptions,\n verifyAuthenticationResponse,\n} from '@simplewebauthn/server';\nimport Database from './database';\nimport { verifyToken as verifyTOTP } from './totp';\nimport setupMetrics from './metrics';\nimport { appRouter } from './router';\nimport { setWebserver } from './router/server';\nimport { createContext, setContextDeps } from './trpc';\nimport * as util from './util';\n\n// path to vite-built data\nconst PUBLIC_PATH = path.join(__dirname, '../../../public');\nconst ASSETS_PATH = path.join(__dirname, '../../../public/assets');\n\nconst log = (...args: unknown[]) => Logger.log(...args);\nconst error = (...args: unknown[]) => Logger.error(...args);\n\n// the webserver serves an authenticated web UI\nexport default class Webserver {\n database: Database;\n port: number;\n omegga: Omegga;\n\n app: express.Express;\n // undefined until createServer() picks a protocol (http/https)\n server: http.Server | https.Server | undefined;\n\n options: IServerConfig;\n https: boolean;\n /** host shown in the \"Web UI available at\" log (OMEGGA_UI_HOST) */\n host: string;\n started: boolean;\n created: Promise<boolean>;\n\n // assigned by setupMetrics once the server status loop starts\n serverStatusInterval: ReturnType<typeof setInterval> | undefined;\n lastReportedStatus: IServerStatus | null = null;\n\n dataPath: string;\n\n // create a webserver\n constructor(options: IServerConfig, omegga: Omegga) {\n this.port = Number(\n options.port || process.env.OMEGGA_PORT || soft.DEFAULT_PORT,\n );\n this.host = process.env.OMEGGA_UI_HOST || '127.0.0.1';\n this.options = options;\n this.omegga = omegga;\n this.dataPath = path.join(omegga.path, soft.DATA_PATH);\n\n // create express app (routes are attached in createServer/initWebUI)\n this.app = express();\n\n const mainSqlite = openDb(path.join(this.dataPath, soft.MAIN_DB));\n const mainDb = drizzle(mainSqlite);\n\n runMigrations(mainDb);\n\n // the database provides omegga with metrics, chat logs, and more\n // to help administrators keep track of their users and server\n this.database = new Database(options, omegga, mainSqlite, mainDb);\n\n // https status of the server\n this.https = false;\n // started status of the server\n this.started = false;\n this.created = this.createServer();\n }\n\n // create the webserver\n async createServer() {\n const hasOpenSSL = hasbin.sync('openssl');\n\n await importNedbIfNeeded(\n this.dataPath,\n this.database.sqlite,\n this.database.db,\n );\n\n // create a server with https if applicable\n const pickProtocol = async () => {\n if (this.options.https) {\n if (hasOpenSSL) {\n const certFile = await util.getSSLKeys(this.dataPath);\n if (certFile) {\n // handy notification of generated ssl certs\n if (certFile.new) log('>>'.green, 'Generated new SSL certificate');\n\n this.https = true;\n return https.createServer(\n {\n key: certFile.keys.serviceKey,\n cert: certFile.keys.certificate,\n },\n this.app,\n );\n } else {\n error(\n '!>'.red,\n 'Error generating SSL certificate - falling back to http',\n );\n }\n }\n\n // warn the user\n log(\n ':>'.yellow,\n 'Running web server with http - install',\n 'openssl'.yellow.underline,\n 'for https (more secure)',\n );\n }\n\n // fallback to http\n return new http.Server(this.app);\n };\n\n // create http(s) server based on openssl availability\n this.server = await pickProtocol();\n\n this.app.set('trust proxy', 1);\n const SqliteStore = (\n BetterSqlite3SessionStore['default'] ?? BetterSqlite3SessionStore\n )(expressSession);\n const sessionSqlite = openDb(path.join(this.dataPath, soft.SESSIONS_DB));\n const session = expressSession({\n secret: util.getSessionSecret(this.dataPath),\n resave: false,\n saveUninitialized: false,\n cookie: {\n path: '/',\n secure: this.https,\n maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year\n },\n store: new SqliteStore({\n client: sessionSqlite,\n expired: { clear: true, intervalMs: 900000 },\n }),\n });\n this.app.use(session);\n\n // setup routes and webserver\n await this.initWebUI();\n return true;\n }\n\n // setup the web ui routes\n async initWebUI() {\n await this.database.getInstanceId();\n\n // Initialize tRPC context deps\n setContextDeps({\n database: this.database,\n omegga: this.omegga,\n });\n setWebserver(this);\n\n // provide assets in the /public path\n this.app.use('/assets', express.static(ASSETS_PATH));\n this.app.use(bodyParser.json());\n\n // --- Auth routes (outside tRPC) ---\n const openApi = express.Router();\n const api = express.Router();\n\n // check if this is the first user in the database\n openApi.get('/first', async (_req, res) =>\n res.json(await this.database.isFirstUser()),\n );\n\n // login / create admin user route\n openApi.post('/auth', async (req, res) => {\n if (\n typeof req.body !== 'object' ||\n typeof req.body.username !== 'string' ||\n typeof req.body.password !== 'string'\n ) {\n return res.status(422).json({ message: 'invalid body' });\n }\n const { username, password } = req.body;\n\n if (!username.match(/^\\w{0,32}$/)) {\n return res.status(422).json({ message: 'invalid body' });\n }\n\n const isFirst = await this.database.isFirstUser();\n let user;\n if (isFirst) {\n user = await this.database.createAdminUser(\n username,\n username === '' ? '' : password,\n );\n } else {\n user = await this.database.authUser(username, password);\n }\n\n if (user) {\n req.session.userId = user._id;\n if (user.totpEnabled) {\n req.session.mfaPending = true;\n req.session.save();\n res.status(200).json({ mfaRequired: true });\n } else {\n req.session.save();\n res.status(200).json({});\n }\n } else {\n res.status(404).json({ message: 'no user found' });\n }\n });\n\n // rate limiting for MFA verification (per session)\n const mfaAttempts = new Map<\n string,\n { count: number; lockedUntil: number }\n >();\n\n // TOTP verification during MFA challenge\n openApi.post('/auth/mfa/totp', async (req, res) => {\n if (!req.session.mfaPending || !req.session.userId) {\n return res.status(400).json({ message: 'no pending MFA challenge' });\n }\n\n // rate limit: 5 attempts, then 60s lockout\n const sid = req.sessionID;\n const attempt = mfaAttempts.get(sid) ?? { count: 0, lockedUntil: 0 };\n if (Date.now() < attempt.lockedUntil) {\n return res\n .status(429)\n .json({ message: 'too many attempts, try again later' });\n }\n\n const { code } = req.body ?? {};\n if (typeof code !== 'string') {\n return res.status(422).json({ message: 'code required' });\n }\n const user = await this.database.findUserById(req.session.userId);\n if (!user || !user.totpSecret) {\n return res.status(400).json({ message: 'MFA not configured' });\n }\n\n let verified = false;\n\n if (verifyTOTP(code, user.totpSecret)) {\n verified = true;\n }\n\n // try recovery codes\n if (!verified && user.recoveryCodes?.length) {\n for (const hashedCode of user.recoveryCodes) {\n if (await bcrypt.compare(code, hashedCode)) {\n await this.database.removeRecoveryCode(user.username, hashedCode);\n verified = true;\n break;\n }\n }\n }\n\n if (verified) {\n mfaAttempts.delete(sid);\n delete req.session.mfaPending;\n req.session.save();\n return res.status(200).json({});\n }\n\n attempt.count++;\n if (attempt.count >= 5) {\n attempt.lockedUntil = Date.now() + 60_000;\n attempt.count = 0;\n }\n mfaAttempts.set(sid, attempt);\n res.status(401).json({ message: 'invalid code' });\n });\n\n const getRpID = (req: express.Request) => {\n const origin = req.headers.origin;\n if (origin) {\n try {\n return new URL(origin as string).hostname;\n } catch {}\n }\n return req.hostname;\n };\n\n // WebAuthn authentication options (passwordless login)\n openApi.get('/auth/webauthn/options', async (req, res) => {\n const rpID = getRpID(req);\n const options = await generateAuthenticationOptions({ rpID });\n req.session.mfaChallenge = options.challenge;\n req.session.save();\n res.json(options);\n });\n\n // WebAuthn authentication verification\n openApi.post('/auth/webauthn/verify', async (req, res) => {\n const challenge = req.session.mfaChallenge;\n if (!challenge) {\n return res.status(400).json({ message: 'no pending challenge' });\n }\n // clear challenge immediately to prevent replay\n delete req.session.mfaChallenge;\n req.session.save();\n try {\n const { credential } = req.body;\n const user = await this.database.findUserByPasskeyId(credential?.id);\n if (!user || user.isBanned) {\n return res.status(401).json({ message: 'unknown credential' });\n }\n const passkey = user.passkeys!.find(p => p.id === credential.id)!;\n const verification = await verifyAuthenticationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin:\n req.headers.origin ?? `${req.protocol}://${req.get('host')}`,\n expectedRPID: getRpID(req),\n credential: {\n id: passkey.id,\n publicKey: Buffer.from(passkey.publicKey, 'base64url'),\n counter: passkey.counter,\n transports: passkey.transports,\n },\n });\n if (!verification.verified) {\n return res.status(401).json({ message: 'verification failed' });\n }\n await this.database.updatePasskeyCounter(\n user.username,\n passkey.id,\n verification.authenticationInfo.newCounter,\n );\n req.session.userId = user._id;\n delete req.session.mfaPending;\n req.session.save();\n res.status(200).json({});\n } catch {\n res.status(400).json({ message: 'verification error' });\n }\n });\n\n // authentication middleware for protected api routes\n api.all('*', (req, _res, next) => {\n (async () => {\n if (req.session.mfaPending) return next(new Error('unauthorized'));\n const user = await this.database.findUserById(req.session.userId);\n if (!user || user.isBanned) return next(new Error('unauthorized'));\n next();\n })();\n });\n\n // kill a session\n api.get('/logout', (req, res) => {\n req.session.destroy(e => {\n res.status(e ? 500 : 200).json({});\n });\n });\n\n this.app.use('/api/v1', openApi);\n this.app.use('/api/v1', api);\n\n // --- tRPC ---\n this.app.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext,\n }),\n );\n\n // setup metrics and tracking\n setupMetrics(this);\n\n // every request goes through the index file (frontend handles 404s)\n this.app.use(async (req, res) => {\n if (req.session.mfaPending) {\n return res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n const user = await this.database.findUserById(req.session.userId);\n const isAuth = user && !user.isBanned;\n if (isAuth) {\n res.sendFile(path.join(PUBLIC_PATH, 'app.html'));\n } else {\n res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n });\n }\n\n // start the webserver\n async start() {\n if (this.started) return;\n await this.created;\n const server = this.server;\n if (!server) throw new Error('webserver failed to initialize');\n return await new Promise<void>(resolve => {\n server.listen(this.port, () => {\n log(\n `${'>>'.green} Web UI available at`,\n `http${this.https ? 's' : ''}://${this.host}:${this.port}`.green,\n );\n this.started = true;\n this.database.addChatLog('server', {}, 'Server started');\n resolve();\n });\n });\n }\n\n // stop the webserver\n stop() {\n this.database.addChatLog('server', {}, 'Server stopped');\n this.server?.close();\n this.started = false;\n clearInterval(this.serverStatusInterval);\n }\n}\n"],"names":["Logger","soft","openDb","drizzle","runMigrations","Database","importNedbIfNeeded","util.getSSLKeys","util.getSessionSecret","setContextDeps","setWebserver","verifyTOTP","generateAuthenticationOptions","verifyAuthenticationResponse","createExpressMiddleware","appRouter","createContext","setupMetrics","server"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,cAAc,KAAK,KAAK,WAAW,iBAAiB;AAC1D,MAAM,cAAc,KAAK,KAAK,WAAW,wBAAwB;AAEjE,MAAM,MAAM,IAAI,SAAoBA,OAAAA,QAAO,IAAI,GAAG,IAAI;AACtD,MAAM,QAAQ,IAAI,SAAoBA,OAAAA,QAAO,MAAM,GAAG,IAAI;AAG1D,MAAqB,UAAU;AAAA;AAAA,EAuB7B,YAAY,SAAwB,QAAgB;AALpD,SAAA,qBAA2C;AAMzC,SAAK,OAAO;AAAA,MACV,QAAQ,QAAQ,QAAQ,IAAI,eAAeC,WAAAA,QAAK;AAAA,IAAA;AAElD,SAAK,OAAO,QAAQ,IAAI,kBAAkB;AAC1C,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,WAAW,KAAK,KAAK,OAAO,MAAMA,WAAAA,QAAK,SAAS;AAGrD,SAAK,MAAM,QAAA;AAEX,UAAM,aAAaC,WAAAA,OAAO,KAAK,KAAK,KAAK,UAAUD,mBAAK,OAAO,CAAC;AAChE,UAAM,SAASE,cAAAA,QAAQ,UAAU;AAEjCC,YAAAA,cAAc,MAAM;AAIpB,SAAK,WAAW,IAAIC,SAAAA,QAAS,SAAS,QAAQ,YAAY,MAAM;AAGhE,SAAK,QAAQ;AAEb,SAAK,UAAU;AACf,SAAK,UAAU,KAAK,aAAA;AAAA,EACtB;AAAA;AAAA,EAGA,MAAM,eAAe;AACnB,UAAM,aAAa,OAAO,KAAK,SAAS;AAExC,UAAMC,WAAAA;AAAAA,MACJ,KAAK;AAAA,MACL,KAAK,SAAS;AAAA,MACd,KAAK,SAAS;AAAA,IAAA;AAIhB,UAAM,eAAe,YAAY;AAC/B,UAAI,KAAK,QAAQ,OAAO;AACtB,YAAI,YAAY;AACd,gBAAM,WAAW,MAAMC,gBAAgB,KAAK,QAAQ;AACpD,cAAI,UAAU;AAEZ,gBAAI,SAAS,IAAK,KAAI,KAAK,OAAO,+BAA+B;AAEjE,iBAAK,QAAQ;AACb,mBAAO,MAAM;AAAA,cACX;AAAA,gBACE,KAAK,SAAS,KAAK;AAAA,gBACnB,MAAM,SAAS,KAAK;AAAA,cAAA;AAAA,cAEtB,KAAK;AAAA,YAAA;AAAA,UAET,OAAO;AACL;AAAA,cACE,KAAK;AAAA,cACL;AAAA,YAAA;AAAA,UAEJ;AAAA,QACF;AAGA;AAAA,UACE,KAAK;AAAA,UACL;AAAA,UACA,UAAU,OAAO;AAAA,UACjB;AAAA,QAAA;AAAA,MAEJ;AAGA,aAAO,IAAI,KAAK,OAAO,KAAK,GAAG;AAAA,IACjC;AAGA,SAAK,SAAS,MAAM,aAAA;AAEpB,SAAK,IAAI,IAAI,eAAe,CAAC;AAC7B,UAAM,eACJ,0BAA0B,SAAS,KAAK,2BACxC,cAAc;AAChB,UAAM,gBAAgBL,WAAAA,OAAO,KAAK,KAAK,KAAK,UAAUD,mBAAK,WAAW,CAAC;AACvE,UAAM,UAAU,eAAe;AAAA,MAC7B,QAAQO,KAAAA,iBAAsB,KAAK,QAAQ;AAAA,MAC3C,QAAQ;AAAA,MACR,mBAAmB;AAAA,MACnB,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,QAAQ,KAAK;AAAA,QACb,QAAQ,MAAM,KAAK,KAAK,KAAK;AAAA;AAAA,MAAA;AAAA,MAE/B,OAAO,IAAI,YAAY;AAAA,QACrB,QAAQ;AAAA,QACR,SAAS,EAAE,OAAO,MAAM,YAAY,IAAA;AAAA,MAAO,CAC5C;AAAA,IAAA,CACF;AACD,SAAK,IAAI,IAAI,OAAO;AAGpB,UAAM,KAAK,UAAA;AACX,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,YAAY;AAChB,UAAM,KAAK,SAAS,cAAA;AAGpBC,wBAAe;AAAA,MACb,UAAU,KAAK;AAAA,MACf,QAAQ,KAAK;AAAA,IAAA,CACd;AACDC,WAAAA,aAAa,IAAI;AAGjB,SAAK,IAAI,IAAI,WAAW,QAAQ,OAAO,WAAW,CAAC;AACnD,SAAK,IAAI,IAAI,WAAW,KAAA,CAAM;AAG9B,UAAM,UAAU,QAAQ,OAAA;AACxB,UAAM,MAAM,QAAQ,OAAA;AAGpB,YAAQ;AAAA,MAAI;AAAA,MAAU,OAAO,MAAM,QACjC,IAAI,KAAK,MAAM,KAAK,SAAS,YAAA,CAAa;AAAA,IAAA;AAI5C,YAAQ,KAAK,SAAS,OAAO,KAAK,QAAQ;AACxC,UACE,OAAO,IAAI,SAAS,YACpB,OAAO,IAAI,KAAK,aAAa,YAC7B,OAAO,IAAI,KAAK,aAAa,UAC7B;AACA,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,gBAAgB;AAAA,MACzD;AACA,YAAM,EAAE,UAAU,SAAA,IAAa,IAAI;AAEnC,UAAI,CAAC,SAAS,MAAM,YAAY,GAAG;AACjC,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,gBAAgB;AAAA,MACzD;AAEA,YAAM,UAAU,MAAM,KAAK,SAAS,YAAA;AACpC,UAAI;AACJ,UAAI,SAAS;AACX,eAAO,MAAM,KAAK,SAAS;AAAA,UACzB;AAAA,UACA,aAAa,KAAK,KAAK;AAAA,QAAA;AAAA,MAE3B,OAAO;AACL,eAAO,MAAM,KAAK,SAAS,SAAS,UAAU,QAAQ;AAAA,MACxD;AAEA,UAAI,MAAM;AACR,YAAI,QAAQ,SAAS,KAAK;AAC1B,YAAI,KAAK,aAAa;AACpB,cAAI,QAAQ,aAAa;AACzB,cAAI,QAAQ,KAAA;AACZ,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,aAAa,MAAM;AAAA,QAC5C,OAAO;AACL,cAAI,QAAQ,KAAA;AACZ,cAAI,OAAO,GAAG,EAAE,KAAK,CAAA,CAAE;AAAA,QACzB;AAAA,MACF,OAAO;AACL,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,iBAAiB;AAAA,MACnD;AAAA,IACF,CAAC;AAGD,UAAM,kCAAkB,IAAA;AAMxB,YAAQ,KAAK,kBAAkB,OAAO,KAAK,QAAQ;AACjD,UAAI,CAAC,IAAI,QAAQ,cAAc,CAAC,IAAI,QAAQ,QAAQ;AAClD,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,4BAA4B;AAAA,MACrE;AAGA,YAAM,MAAM,IAAI;AAChB,YAAM,UAAU,YAAY,IAAI,GAAG,KAAK,EAAE,OAAO,GAAG,aAAa,EAAA;AACjE,UAAI,KAAK,QAAQ,QAAQ,aAAa;AACpC,eAAO,IACJ,OAAO,GAAG,EACV,KAAK,EAAE,SAAS,sCAAsC;AAAA,MAC3D;AAEA,YAAM,EAAE,KAAA,IAAS,IAAI,QAAQ,CAAA;AAC7B,UAAI,OAAO,SAAS,UAAU;AAC5B,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,iBAAiB;AAAA,MAC1D;AACA,YAAM,OAAO,MAAM,KAAK,SAAS,aAAa,IAAI,QAAQ,MAAM;AAChE,UAAI,CAAC,QAAQ,CAAC,KAAK,YAAY;AAC7B,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,sBAAsB;AAAA,MAC/D;AAEA,UAAI,WAAW;AAEf,UAAIC,iBAAW,MAAM,KAAK,UAAU,GAAG;AACrC,mBAAW;AAAA,MACb;AAGA,UAAI,CAAC,YAAY,KAAK,eAAe,QAAQ;AAC3C,mBAAW,cAAc,KAAK,eAAe;AAC3C,cAAI,MAAM,OAAO,QAAQ,MAAM,UAAU,GAAG;AAC1C,kBAAM,KAAK,SAAS,mBAAmB,KAAK,UAAU,UAAU;AAChE,uBAAW;AACX;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,UAAI,UAAU;AACZ,oBAAY,OAAO,GAAG;AACtB,eAAO,IAAI,QAAQ;AACnB,YAAI,QAAQ,KAAA;AACZ,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,CAAA,CAAE;AAAA,MAChC;AAEA,cAAQ;AACR,UAAI,QAAQ,SAAS,GAAG;AACtB,gBAAQ,cAAc,KAAK,IAAA,IAAQ;AACnC,gBAAQ,QAAQ;AAAA,MAClB;AACA,kBAAY,IAAI,KAAK,OAAO;AAC5B,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,gBAAgB;AAAA,IAClD,CAAC;AAED,UAAM,UAAU,CAAC,QAAyB;AACxC,YAAM,SAAS,IAAI,QAAQ;AAC3B,UAAI,QAAQ;AACV,YAAI;AACF,iBAAO,IAAI,IAAI,MAAgB,EAAE;AAAA,QACnC,QAAQ;AAAA,QAAC;AAAA,MACX;AACA,aAAO,IAAI;AAAA,IACb;AAGA,YAAQ,IAAI,0BAA0B,OAAO,KAAK,QAAQ;AACxD,YAAM,OAAO,QAAQ,GAAG;AACxB,YAAM,UAAU,MAAMC,uCAA8B,EAAE,MAAM;AAC5D,UAAI,QAAQ,eAAe,QAAQ;AACnC,UAAI,QAAQ,KAAA;AACZ,UAAI,KAAK,OAAO;AAAA,IAClB,CAAC;AAGD,YAAQ,KAAK,yBAAyB,OAAO,KAAK,QAAQ;AACxD,YAAM,YAAY,IAAI,QAAQ;AAC9B,UAAI,CAAC,WAAW;AACd,eAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,wBAAwB;AAAA,MACjE;AAEA,aAAO,IAAI,QAAQ;AACnB,UAAI,QAAQ,KAAA;AACZ,UAAI;AACF,cAAM,EAAE,eAAe,IAAI;AAC3B,cAAM,OAAO,MAAM,KAAK,SAAS,oBAAoB,YAAY,EAAE;AACnE,YAAI,CAAC,QAAQ,KAAK,UAAU;AAC1B,iBAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,sBAAsB;AAAA,QAC/D;AACA,cAAM,UAAU,KAAK,SAAU,KAAK,OAAK,EAAE,OAAO,WAAW,EAAE;AAC/D,cAAM,eAAe,MAAMC,sCAA6B;AAAA,UACtD,UAAU;AAAA,UACV,mBAAmB;AAAA,UACnB,gBACE,IAAI,QAAQ,UAAU,GAAG,IAAI,QAAQ,MAAM,IAAI,IAAI,MAAM,CAAC;AAAA,UAC5D,cAAc,QAAQ,GAAG;AAAA,UACzB,YAAY;AAAA,YACV,IAAI,QAAQ;AAAA,YACZ,WAAW,OAAO,KAAK,QAAQ,WAAW,WAAW;AAAA,YACrD,SAAS,QAAQ;AAAA,YACjB,YAAY,QAAQ;AAAA,UAAA;AAAA,QACtB,CACD;AACD,YAAI,CAAC,aAAa,UAAU;AAC1B,iBAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,uBAAuB;AAAA,QAChE;AACA,cAAM,KAAK,SAAS;AAAA,UAClB,KAAK;AAAA,UACL,QAAQ;AAAA,UACR,aAAa,mBAAmB;AAAA,QAAA;AAElC,YAAI,QAAQ,SAAS,KAAK;AAC1B,eAAO,IAAI,QAAQ;AACnB,YAAI,QAAQ,KAAA;AACZ,YAAI,OAAO,GAAG,EAAE,KAAK,CAAA,CAAE;AAAA,MACzB,QAAQ;AACN,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,sBAAsB;AAAA,MACxD;AAAA,IACF,CAAC;AAGD,QAAI,IAAI,KAAK,CAAC,KAAK,MAAM,SAAS;AAChC,OAAC,YAAY;AACX,YAAI,IAAI,QAAQ,WAAY,QAAO,KAAK,IAAI,MAAM,cAAc,CAAC;AACjE,cAAM,OAAO,MAAM,KAAK,SAAS,aAAa,IAAI,QAAQ,MAAM;AAChE,YAAI,CAAC,QAAQ,KAAK,iBAAiB,KAAK,IAAI,MAAM,cAAc,CAAC;AACjE,aAAA;AAAA,MACF,GAAA;AAAA,IACF,CAAC;AAGD,QAAI,IAAI,WAAW,CAAC,KAAK,QAAQ;AAC/B,UAAI,QAAQ,QAAQ,CAAA,MAAK;AACvB,YAAI,OAAO,IAAI,MAAM,GAAG,EAAE,KAAK,EAAE;AAAA,MACnC,CAAC;AAAA,IACH,CAAC;AAED,SAAK,IAAI,IAAI,WAAW,OAAO;AAC/B,SAAK,IAAI,IAAI,WAAW,GAAG;AAG3B,SAAK,IAAI;AAAA,MACP;AAAA,MACAC,kCAAwB;AAAA,QACtB,QAAQC,MAAAA;AAAAA,QAAA,eACRC,KAAAA;AAAAA,MAAA,CACD;AAAA,IAAA;AAIHC,YAAAA,QAAa,IAAI;AAGjB,SAAK,IAAI,IAAI,OAAO,KAAK,QAAQ;AAC/B,UAAI,IAAI,QAAQ,YAAY;AAC1B,eAAO,IAAI,SAAS,KAAK,KAAK,aAAa,WAAW,CAAC;AAAA,MACzD;AACA,YAAM,OAAO,MAAM,KAAK,SAAS,aAAa,IAAI,QAAQ,MAAM;AAChE,YAAM,SAAS,QAAQ,CAAC,KAAK;AAC7B,UAAI,QAAQ;AACV,YAAI,SAAS,KAAK,KAAK,aAAa,UAAU,CAAC;AAAA,MACjD,OAAO;AACL,YAAI,SAAS,KAAK,KAAK,aAAa,WAAW,CAAC;AAAA,MAClD;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,QAAQ;AACZ,QAAI,KAAK,QAAS;AAClB,UAAM,KAAK;AACX,UAAMC,UAAS,KAAK;AACpB,QAAI,CAACA,QAAQ,OAAM,IAAI,MAAM,gCAAgC;AAC7D,WAAO,MAAM,IAAI,QAAc,CAAA,YAAW;AACxC,MAAAA,QAAO,OAAO,KAAK,MAAM,MAAM;AAC7B;AAAA,UACE,GAAG,KAAK,KAAK;AAAA,UACb,OAAO,KAAK,QAAQ,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG;AAAA,QAAA;AAE7D,aAAK,UAAU;AACf,aAAK,SAAS,WAAW,UAAU,CAAA,GAAI,gBAAgB;AACvD,gBAAA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,OAAO;AACL,SAAK,SAAS,WAAW,UAAU,CAAA,GAAI,gBAAgB;AACvD,SAAK,QAAQ,MAAA;AACb,SAAK,UAAU;AACf,kBAAc,KAAK,oBAAoB;AAAA,EACzC;AACF;;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/webserver/backend/index.ts"],"sourcesContent":["/// <reference path=\"./better-sqlite3-session-store.d.ts\" />\nimport Logger from '@/logger';\nimport soft from '@/softconfig';\nimport { openDb } from '@/db/connection';\nimport { runMigrations } from '@/db/migrate';\nimport { importNedbIfNeeded } from '@/db/nedbImport';\nimport { type IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { type IServerStatus } from '@omegga/types';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport bodyParser from 'body-parser';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport express from 'express';\nimport expressSession from 'express-session';\nimport hasbin from 'hasbin';\nimport http from 'http';\nimport https from 'https';\nimport BetterSqlite3SessionStore from 'better-sqlite3-session-store';\nimport path from 'path';\nimport bcrypt from 'bcryptjs';\nimport {\n generateAuthenticationOptions,\n verifyAuthenticationResponse,\n} from '@simplewebauthn/server';\nimport Database from './database';\nimport { verifyToken as verifyTOTP } from './totp';\nimport setupMetrics from './metrics';\nimport { appRouter } from './router';\nimport { setWebserver } from './router/server';\nimport { createContext, setContextDeps } from './trpc';\nimport * as util from './util';\n\n// path to vite-built data\nconst PUBLIC_PATH = path.join(__dirname, '../../../public');\nconst ASSETS_PATH = path.join(__dirname, '../../../public/assets');\n\nconst log = (...args: unknown[]) => Logger.log(...args);\nconst error = (...args: unknown[]) => Logger.error(...args);\n\n// the webserver serves an authenticated web UI\nexport default class Webserver {\n database: Database;\n port: number;\n omegga: Omegga;\n\n app: express.Express;\n // undefined until createServer() picks a protocol (http/https)\n server: http.Server | https.Server | undefined;\n\n options: IServerConfig;\n https: boolean;\n /** host shown in the \"Web UI available at\" log (OMEGGA_UI_HOST) */\n host: string;\n started: boolean;\n created: Promise<boolean>;\n\n // assigned by setupMetrics once the server status loop starts\n serverStatusInterval: ReturnType<typeof setInterval> | undefined;\n lastReportedStatus: IServerStatus | null = null;\n\n dataPath: string;\n\n // create a webserver\n constructor(options: IServerConfig, omegga: Omegga) {\n this.port = Number(\n options.port || process.env.OMEGGA_PORT || soft.DEFAULT_PORT,\n );\n this.host = process.env.OMEGGA_UI_HOST || '127.0.0.1';\n this.options = options;\n this.omegga = omegga;\n this.dataPath = path.join(omegga.path, soft.DATA_PATH);\n\n // create express app (routes are attached in createServer/initWebUI)\n this.app = express();\n\n const mainSqlite = openDb(path.join(this.dataPath, soft.MAIN_DB));\n const mainDb = drizzle(mainSqlite);\n\n runMigrations(mainDb);\n\n // the database provides omegga with metrics, chat logs, and more\n // to help administrators keep track of their users and server\n this.database = new Database(options, omegga, mainSqlite, mainDb);\n\n // https status of the server\n this.https = false;\n // started status of the server\n this.started = false;\n this.created = this.createServer();\n }\n\n // create the webserver\n async createServer() {\n const hasOpenSSL = hasbin.sync('openssl');\n\n await importNedbIfNeeded(\n this.dataPath,\n this.database.sqlite,\n this.database.db,\n );\n\n // create a server with https if applicable\n const pickProtocol = async () => {\n if (this.options.https) {\n if (hasOpenSSL) {\n const certFile = await util.getSSLKeys(this.dataPath);\n if (certFile) {\n // handy notification of generated ssl certs\n if (certFile.new) log('>>'.green, 'Generated new SSL certificate');\n\n this.https = true;\n return https.createServer(\n {\n key: certFile.keys.serviceKey,\n cert: certFile.keys.certificate,\n },\n this.app,\n );\n } else {\n error(\n '!>'.red,\n 'Error generating SSL certificate - falling back to http',\n );\n }\n }\n\n // warn the user\n log(\n ':>'.yellow,\n 'Running web server with http - install',\n 'openssl'.yellow.underline,\n 'for https (more secure)',\n );\n }\n\n // fallback to http\n return new http.Server(this.app);\n };\n\n // create http(s) server based on openssl availability\n this.server = await pickProtocol();\n\n this.app.set('trust proxy', 1);\n const SqliteStore = (\n BetterSqlite3SessionStore['default'] ?? BetterSqlite3SessionStore\n )(expressSession);\n const sessionSqlite = openDb(path.join(this.dataPath, soft.SESSIONS_DB));\n const session = expressSession({\n secret: util.getSessionSecret(this.dataPath),\n resave: false,\n saveUninitialized: false,\n cookie: {\n path: '/',\n secure: this.https,\n maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year\n },\n store: new SqliteStore({\n client: sessionSqlite,\n expired: { clear: true, intervalMs: 900000 },\n }),\n });\n this.app.use(session);\n\n // setup routes and webserver\n await this.initWebUI();\n return true;\n }\n\n // setup the web ui routes\n async initWebUI() {\n await this.database.getInstanceId();\n\n // Initialize tRPC context deps\n setContextDeps({\n database: this.database,\n omegga: this.omegga,\n });\n setWebserver(this);\n\n // provide assets in the /public path\n this.app.use('/assets', express.static(ASSETS_PATH));\n this.app.use(bodyParser.json());\n\n // --- Auth routes (outside tRPC) ---\n const openApi = express.Router();\n const api = express.Router();\n\n // express-session leaves req.session undefined on requests it declines,\n // like a request target that is not a path (`OPTIONS *`)\n const requireSession: express.RequestHandler = (req, res, next) =>\n req.session ? next() : res.status(400).json({ message: 'no session' });\n\n // check if this is the first user in the database\n openApi.get('/first', async (_req, res) =>\n res.json(await this.database.isFirstUser()),\n );\n\n // login / create admin user route\n openApi.post('/auth', async (req, res) => {\n if (\n typeof req.body !== 'object' ||\n typeof req.body.username !== 'string' ||\n typeof req.body.password !== 'string'\n ) {\n return res.status(422).json({ message: 'invalid body' });\n }\n const { username, password } = req.body;\n\n if (!username.match(/^\\w{0,32}$/)) {\n return res.status(422).json({ message: 'invalid body' });\n }\n\n const isFirst = await this.database.isFirstUser();\n let user;\n if (isFirst) {\n user = await this.database.createAdminUser(\n username,\n username === '' ? '' : password,\n );\n } else {\n user = await this.database.authUser(username, password);\n }\n\n if (user) {\n req.session.userId = user._id;\n if (user.totpEnabled) {\n req.session.mfaPending = true;\n req.session.save();\n res.status(200).json({ mfaRequired: true });\n } else {\n req.session.save();\n res.status(200).json({});\n }\n } else {\n res.status(404).json({ message: 'no user found' });\n }\n });\n\n // rate limiting for MFA verification (per session)\n const mfaAttempts = new Map<\n string,\n { count: number; lockedUntil: number }\n >();\n\n // TOTP verification during MFA challenge\n openApi.post('/auth/mfa/totp', async (req, res) => {\n if (!req.session.mfaPending || !req.session.userId) {\n return res.status(400).json({ message: 'no pending MFA challenge' });\n }\n\n // rate limit: 5 attempts, then 60s lockout\n const sid = req.sessionID;\n const attempt = mfaAttempts.get(sid) ?? { count: 0, lockedUntil: 0 };\n if (Date.now() < attempt.lockedUntil) {\n return res\n .status(429)\n .json({ message: 'too many attempts, try again later' });\n }\n\n const { code } = req.body ?? {};\n if (typeof code !== 'string') {\n return res.status(422).json({ message: 'code required' });\n }\n const user = await this.database.findUserById(req.session.userId);\n if (!user || !user.totpSecret) {\n return res.status(400).json({ message: 'MFA not configured' });\n }\n\n let verified = false;\n\n if (verifyTOTP(code, user.totpSecret)) {\n verified = true;\n }\n\n // try recovery codes\n if (!verified && user.recoveryCodes?.length) {\n for (const hashedCode of user.recoveryCodes) {\n if (await bcrypt.compare(code, hashedCode)) {\n await this.database.removeRecoveryCode(user.username, hashedCode);\n verified = true;\n break;\n }\n }\n }\n\n if (verified) {\n mfaAttempts.delete(sid);\n delete req.session.mfaPending;\n req.session.save();\n return res.status(200).json({});\n }\n\n attempt.count++;\n if (attempt.count >= 5) {\n attempt.lockedUntil = Date.now() + 60_000;\n attempt.count = 0;\n }\n mfaAttempts.set(sid, attempt);\n res.status(401).json({ message: 'invalid code' });\n });\n\n const getRpID = (req: express.Request) => {\n const origin = req.headers.origin;\n if (origin) {\n try {\n return new URL(origin as string).hostname;\n } catch {}\n }\n return req.hostname;\n };\n\n // WebAuthn authentication options (passwordless login)\n openApi.get('/auth/webauthn/options', async (req, res) => {\n const rpID = getRpID(req);\n const options = await generateAuthenticationOptions({ rpID });\n req.session.mfaChallenge = options.challenge;\n req.session.save();\n res.json(options);\n });\n\n // WebAuthn authentication verification\n openApi.post('/auth/webauthn/verify', async (req, res) => {\n const challenge = req.session.mfaChallenge;\n if (!challenge) {\n return res.status(400).json({ message: 'no pending challenge' });\n }\n // clear challenge immediately to prevent replay\n delete req.session.mfaChallenge;\n req.session.save();\n try {\n const { credential } = req.body;\n const user = await this.database.findUserByPasskeyId(credential?.id);\n if (!user || user.isBanned) {\n return res.status(401).json({ message: 'unknown credential' });\n }\n const passkey = user.passkeys!.find(p => p.id === credential.id)!;\n const verification = await verifyAuthenticationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin:\n req.headers.origin ?? `${req.protocol}://${req.get('host')}`,\n expectedRPID: getRpID(req),\n credential: {\n id: passkey.id,\n publicKey: Buffer.from(passkey.publicKey, 'base64url'),\n counter: passkey.counter,\n transports: passkey.transports,\n },\n });\n if (!verification.verified) {\n return res.status(401).json({ message: 'verification failed' });\n }\n await this.database.updatePasskeyCounter(\n user.username,\n passkey.id,\n verification.authenticationInfo.newCounter,\n );\n req.session.userId = user._id;\n delete req.session.mfaPending;\n req.session.save();\n res.status(200).json({});\n } catch {\n res.status(400).json({ message: 'verification error' });\n }\n });\n\n // authentication middleware for protected api routes\n api.all('*', (req, _res, next) => {\n (async () => {\n if (req.session.mfaPending) return next(new Error('unauthorized'));\n const user = await this.database.findUserById(req.session.userId);\n if (!user || user.isBanned) return next(new Error('unauthorized'));\n next();\n })();\n });\n\n // kill a session\n api.get('/logout', (req, res) => {\n req.session.destroy(e => {\n res.status(e ? 500 : 200).json({});\n });\n });\n\n this.app.use('/api/v1', requireSession);\n this.app.use('/api/v1', openApi);\n this.app.use('/api/v1', api);\n\n // --- tRPC ---\n this.app.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext,\n }),\n );\n\n // setup metrics and tracking\n setupMetrics(this);\n\n // every request goes through the index file (frontend handles 404s)\n this.app.use(async (req, res) => {\n if (req.session?.mfaPending) {\n return res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n const user = await this.database.findUserById(req.session?.userId);\n const isAuth = user && !user.isBanned;\n if (isAuth) {\n res.sendFile(path.join(PUBLIC_PATH, 'app.html'));\n } else {\n res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n });\n }\n\n // start the webserver\n async start() {\n if (this.started) return;\n await this.created;\n const server = this.server;\n if (!server) throw new Error('webserver failed to initialize');\n return await new Promise<void>(resolve => {\n server.listen(this.port, () => {\n log(\n `${'>>'.green} Web UI available at`,\n `http${this.https ? 's' : ''}://${this.host}:${this.port}`.green,\n );\n this.started = true;\n this.database.addChatLog('server', {}, 'Server started');\n resolve();\n });\n });\n }\n\n // stop the webserver\n stop() {\n this.database.addChatLog('server', {}, 'Server stopped');\n this.server?.close();\n this.started = false;\n clearInterval(this.serverStatusInterval);\n }\n}\n"],"names":["Logger","soft","openDb","drizzle","runMigrations","Database","importNedbIfNeeded","util.getSSLKeys","util.getSessionSecret","setContextDeps","setWebserver","verifyTOTP","generateAuthenticationOptions","verifyAuthenticationResponse","createExpressMiddleware","appRouter","createContext","setupMetrics","server"],"mappings":"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"}
|