omegga 1.6.0 → 1.6.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/webserver/backend/database.js +91 -5
  3. package/dist/webserver/backend/database.js.map +1 -1
  4. package/dist/webserver/backend/permissions.js +69 -10
  5. package/dist/webserver/backend/permissions.js.map +1 -1
  6. package/dist/webserver/backend/roleHierarchy.js +108 -0
  7. package/dist/webserver/backend/roleHierarchy.js.map +1 -0
  8. package/dist/webserver/backend/router/player.js +9 -0
  9. package/dist/webserver/backend/router/player.js.map +1 -1
  10. package/dist/webserver/backend/router/role.js +201 -7
  11. package/dist/webserver/backend/router/role.js.map +1 -1
  12. package/dist/webserver/backend/router/session.js +3 -5
  13. package/dist/webserver/backend/router/session.js.map +1 -1
  14. package/dist/webserver/backend/router/user.js +87 -34
  15. package/dist/webserver/backend/router/user.js.map +1 -1
  16. package/dist/webserver/backend/scopes.js +73 -44
  17. package/dist/webserver/backend/scopes.js.map +1 -1
  18. package/dist/webserver/backend/trpc.js +2 -2
  19. package/dist/webserver/backend/trpc.js.map +1 -1
  20. package/doc/permissions.md +83 -12
  21. package/package.json +3 -2
  22. package/public/app.html +103 -24
  23. package/public/assets/IconWorld-Bx3cKMms.js +2 -0
  24. package/public/assets/IconWorld-Bx3cKMms.js.map +1 -0
  25. package/public/assets/IconWorldPlus-BHSmzWsZ.js +2 -0
  26. package/public/assets/IconWorldPlus-BHSmzWsZ.js.map +1 -0
  27. package/public/assets/app-J90GEVdo.js +7 -0
  28. package/public/assets/app-J90GEVdo.js.map +1 -0
  29. package/public/assets/{auth-CEYiizOt.js → auth-nn5rDnJ9.js} +2 -2
  30. package/public/assets/{auth-CEYiizOt.js.map → auth-nn5rDnJ9.js.map} +1 -1
  31. package/public/assets/index-B2DCW1zi.js +2 -0
  32. package/public/assets/index-B2DCW1zi.js.map +1 -0
  33. package/public/assets/index-wYDkzaAO.js +12 -0
  34. package/public/assets/index-wYDkzaAO.js.map +1 -0
  35. package/public/assets/shared/@components-BWkdUJhC.js +2 -0
  36. package/public/assets/shared/@components-BWkdUJhC.js.map +1 -0
  37. package/public/assets/shared/{@hooks-DT93N7wd.js → @hooks-BuRTblPY.js} +2 -2
  38. package/public/assets/shared/{@hooks-DT93N7wd.js.map → @hooks-BuRTblPY.js.map} +1 -1
  39. package/public/assets/shared/@tabler_icons-react-CdTxTJVq.js +3 -0
  40. package/public/assets/shared/{@tabler_icons-react-DrVFX3uL.js.map → @tabler_icons-react-CdTxTJVq.js.map} +1 -1
  41. package/public/assets/useHasScope-DZPCXNQE.js +2 -0
  42. package/public/assets/useHasScope-DZPCXNQE.js.map +1 -0
  43. package/public/auth.html +59 -10
  44. package/public/assets/IconWorld-XmQNg6JI.js +0 -2
  45. package/public/assets/IconWorld-XmQNg6JI.js.map +0 -1
  46. package/public/assets/IconWorldPlus-CxGGv2eD.js +0 -2
  47. package/public/assets/IconWorldPlus-CxGGv2eD.js.map +0 -1
  48. package/public/assets/app-DMr_-wik.js +0 -7
  49. package/public/assets/app-DMr_-wik.js.map +0 -1
  50. package/public/assets/index-D_STNela.js +0 -2
  51. package/public/assets/index-D_STNela.js.map +0 -1
  52. package/public/assets/index-Dkh3HiBz.js +0 -4
  53. package/public/assets/index-Dkh3HiBz.js.map +0 -1
  54. package/public/assets/shared/@components-CgsSYMe6.js +0 -2
  55. package/public/assets/shared/@components-CgsSYMe6.js.map +0 -1
  56. package/public/assets/shared/@tabler_icons-react-DrVFX3uL.js +0 -3
  57. package/public/assets/useHasScope-BOdFktko.js +0 -2
  58. package/public/assets/useHasScope-BOdFktko.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  - Future features go here
6
6
 
7
+ ## 1.6.1 - 2026-05-25
8
+
9
+ - Add role-based access control (RBAC) with hierarchical role management
10
+ - Roles view with create, edit, delete, and drag-reorder (with toggle)
11
+ - Role hierarchy enforcement: users can only manage roles below their own level
12
+ - Default permissions editor moved to the Roles view as the "Everyone" entry
13
+ - Permission editor: scope counts per domain and total, read-only mode when lacking permissions
14
+ - Animated dropdown menus (fade + slide) across all views
15
+ - Show game version on the top right of the web-ui
16
+
7
17
  ## 1.6.0 - 2026-05-24
8
18
 
9
19
  - Add owner-only user disable and delete actions to the web UI
@@ -19,6 +19,7 @@ class Database extends EventEmitter {
19
19
  constructor(options, omegga) {
20
20
  super();
21
21
  this.defaultPermissionsCache = null;
22
+ this.rolesCache = null;
22
23
  this.options = options;
23
24
  this.omegga = omegga;
24
25
  this.calendar = new calendar.default();
@@ -81,7 +82,11 @@ class Database extends EventEmitter {
81
82
  if (v === "enabled" || v === true) scopes[k] = true;
82
83
  else if (v === "disabled" || v === false) scopes[k] = false;
83
84
  }
84
- const encoded = permissions.encodePermissions({ root, domains, scopes });
85
+ const encoded = permissions.encodePermissions({
86
+ root,
87
+ domains,
88
+ scopes
89
+ });
85
90
  await store.update(
86
91
  { _id: user._id },
87
92
  { $set: { permissions: encoded } }
@@ -460,10 +465,91 @@ class Database extends EventEmitter {
460
465
  "passkeys.id": credentialId
461
466
  });
462
467
  }
463
- // get a list of roles
464
- // TODO: actually implement roles
465
- getRoles() {
466
- return this.stores.server.find({ type: "role" });
468
+ async getAllRoles() {
469
+ if (this.rolesCache) return this.rolesCache;
470
+ const roles = await this.stores.server.find({
471
+ type: "webRole"
472
+ });
473
+ roles.sort((a, b) => b.order - a.order);
474
+ this.rolesCache = roles;
475
+ return roles;
476
+ }
477
+ invalidateRolesCache() {
478
+ this.rolesCache = null;
479
+ this.defaultPermissionsCache = null;
480
+ }
481
+ async getRole(id) {
482
+ return await this.stores.server.findOne({
483
+ _id: id,
484
+ type: "webRole"
485
+ });
486
+ }
487
+ async createRole(name, description, permissions$1) {
488
+ await this.stores.server.update(
489
+ { type: "webRole" },
490
+ { $inc: { order: 1 } },
491
+ { multi: true }
492
+ );
493
+ const role = await this.stores.server.insert({
494
+ type: "webRole",
495
+ name,
496
+ description,
497
+ order: 1,
498
+ permissions: permissions.encodePermissions(permissions$1)
499
+ });
500
+ this.invalidateRolesCache();
501
+ return role;
502
+ }
503
+ async updateRole(id, updates) {
504
+ const $set = {};
505
+ if (updates.name !== void 0) $set.name = updates.name;
506
+ if (updates.description !== void 0)
507
+ $set.description = updates.description;
508
+ if (updates.order !== void 0) $set.order = updates.order;
509
+ if (updates.permissions !== void 0)
510
+ $set.permissions = permissions.encodePermissions(updates.permissions);
511
+ await this.stores.server.update({ _id: id, type: "webRole" }, { $set });
512
+ this.invalidateRolesCache();
513
+ }
514
+ async deleteRole(id) {
515
+ const role = await this.getRole(id);
516
+ if (!role) return "role not found";
517
+ await this.stores.server.remove({ _id: id }, {});
518
+ await this.stores.users.update(
519
+ { type: "user", roles: id },
520
+ { $pull: { roles: id } },
521
+ { multi: true }
522
+ );
523
+ this.invalidateRolesCache();
524
+ return "";
525
+ }
526
+ async reorderRoles(orderedIds) {
527
+ const len = orderedIds.length;
528
+ for (let i = 0; i < len; i++) {
529
+ await this.stores.server.update(
530
+ { _id: orderedIds[i], type: "webRole" },
531
+ { $set: { order: len - i } }
532
+ );
533
+ }
534
+ this.invalidateRolesCache();
535
+ }
536
+ async getUserRolePermissions(user) {
537
+ const defaults = await this.getDefaultPermissions();
538
+ const defaultPerms = {
539
+ root: defaults.root,
540
+ domains: defaults.domains,
541
+ scopes: defaults.scopes
542
+ };
543
+ const allRoles = await this.getAllRoles();
544
+ const userRoles = allRoles.filter((r) => user.roles.includes(r._id));
545
+ return [
546
+ defaultPerms,
547
+ ...userRoles.map((r) => permissions.decodePermissions(r.permissions))
548
+ ];
549
+ }
550
+ async getUserAssignedRoles(user) {
551
+ const allRoles = await this.getAllRoles();
552
+ return allRoles.filter((r) => user.roles.includes(r._id));
467
553
  }
468
554
  // add a chat message to the chat log store
469
555
  async addChatLog(action, user, message) {
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sources":["../../../src/webserver/backend/database.ts"],"sourcesContent":["import soft from '@/softconfig';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { explode } from '@util/pattern';\nimport { parseBrickadiaTime } from '@util/time';\nimport bcrypt from 'bcryptjs';\nimport chokidar from 'chokidar';\nimport EventEmitter from 'events';\nimport Datastore from 'nedb-promises';\nimport path from 'path';\nimport Calendar from './calendar';\nimport { serverEvents } from './events';\nimport {\n EMPTY_PERMISSIONS,\n RootLevel,\n decodePermissions,\n encodePermissions,\n type PermissionSet,\n} from './permissions';\nimport {\n IPlayer,\n IPunchcard,\n IStoreAutoRestartConfig,\n IStoreBanHistory,\n IStoreChat,\n IStoreDefaultPermissions,\n IStoreKickHistory,\n IStoreServerInstance,\n IStoreUser,\n IStoreVersion,\n IUserHistory,\n IUserNote,\n IWebAuthnCredential,\n} from './types';\n\n// TODO: online users graph\n// TODO: player online times\n// TODO: bricks over time graph\n// TODO: minute server status for metrics\n// TODO: chat messages per min/hour/day check\n\nlet serverInstance: IStoreServerInstance & { _id: string };\n\ntype FixedSizeArray<N extends number, T, M extends string = '0'> = {\n readonly [k in M]: any;\n} & { length: N } & ReadonlyArray<T>;\n\n// generate a punchcard (days x week)\nconst createPunchcard = (): number[][] =>\n Array.from({ length: 7 }).map(() =>\n Array.from<number>({ length: 24 }).fill(0),\n );\n\n// the database keeps track of metrics for omegga\nexport default class Database extends EventEmitter {\n options: IServerConfig;\n omegga: Omegga;\n stores: {\n users: Datastore;\n chat: Datastore;\n players: Datastore;\n status: Datastore;\n server: Datastore;\n };\n defaultPermissionsCache: IStoreDefaultPermissions | null = null;\n calendar: Calendar;\n\n constructor(options: IServerConfig, omegga: Omegga) {\n super();\n this.options = options;\n this.omegga = omegga;\n this.calendar = new Calendar();\n\n // database\n const dbOpts = {\n autoload: true,\n // case insensitive string comparison\n compareStrings: (a: string, b: string) =>\n a.localeCompare(b, 'en', { ignorePunctuation: true }),\n };\n\n // create all the stores\n this.stores = {\n users: Datastore.create({\n filename: path.join(omegga.dataPath, soft.USER_STORE),\n ...dbOpts,\n }),\n chat: Datastore.create({\n filename: path.join(omegga.dataPath, soft.CHAT_STORE),\n ...dbOpts,\n }),\n players: Datastore.create({\n filename: path.join(omegga.dataPath, soft.PLAYER_STORE),\n ...dbOpts,\n }),\n status: Datastore.create({\n filename: path.join(omegga.dataPath, soft.STATUS_STORE),\n ...dbOpts,\n }),\n server: Datastore.create({\n filename: path.join(omegga.dataPath, soft.SERVER_STORE),\n ...dbOpts,\n }),\n };\n }\n\n // make sure all the databases have valid store versions\n async doMigrations() {\n // current store versions\n const storeVersions: { [key: keyof typeof this.stores]: number } = {\n users: 2,\n chat: 1,\n players: 1,\n status: 1,\n server: 2,\n\n // example version\n dummy: 10,\n };\n\n // migrations to/from certain store versions\n const migrations: {\n [key: keyof typeof this.stores]: {\n version: number;\n upgrade: (store: Datastore) => Promise<void>;\n }[];\n } = {\n users: [\n {\n version: 1,\n upgrade: async (store: Datastore) => {\n const users = await store.find<IStoreUser & { _id: string }>({\n type: 'user',\n });\n for (const user of users) {\n const p = (user.permissions as any) ?? EMPTY_PERMISSIONS;\n const root =\n p.root === 'all' || p.root === 'read' ? p.root : 'off';\n const domains: Record<string, string> = {};\n for (const [k, v] of Object.entries(p.domains ?? {})) {\n if (v !== 'unset' && v !== 'none') domains[k] = v as string;\n }\n const scopes: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(p.scopes ?? {})) {\n if (v === 'enabled' || v === true) scopes[k] = true;\n else if (v === 'disabled' || v === false) scopes[k] = false;\n }\n const encoded = encodePermissions({ root, domains, scopes });\n await store.update(\n { _id: user._id },\n { $set: { permissions: encoded } },\n );\n }\n },\n },\n ],\n chat: [],\n players: [],\n status: [],\n server: [\n {\n version: 1,\n upgrade: async (store: Datastore) => {\n const doc = await store.findOne<any>({\n type: 'defaultPermissions',\n });\n if (!doc) return;\n const root = doc.root === 'unset' ? 'off' : doc.root;\n const domains: Record<string, string> = {};\n for (const [k, v] of Object.entries(doc.domains ?? {})) {\n if (v !== 'unset') domains[k] = v as string;\n }\n const scopes: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(doc.scopes ?? {})) {\n if (v === 'enabled') scopes[k] = true;\n else if (v === 'disabled') scopes[k] = false;\n }\n const encoded = encodePermissions({\n root,\n domains,\n scopes,\n } as PermissionSet);\n await store.update(\n { _id: doc._id },\n {\n $set: {\n root: encoded.root,\n domains: encoded.domains,\n scopes: encoded.scopes,\n },\n },\n );\n },\n },\n ],\n\n // example migration list (all version upgrades go up by 1)\n dummy: [\n {\n version: 1, // target version\n upgrade: async _store => {}, // upgrade function (does not include updating version)\n },\n ],\n };\n\n // loop through all stores to find if any need version upgrades\n await Promise.all(\n Object.entries(this.stores).map(async ([name, store]) => {\n const versionEntry = await store.findOne<IStoreVersion>({\n type: 'storeVersion',\n });\n const expected = storeVersions[name];\n // if the version was not found, insert it\n if (!versionEntry) {\n store.insert({ type: 'storeVersion', version: expected });\n\n // if the version is below the expected version\n } else if (versionEntry.version < expected) {\n // while the version is below the expected version\n let { version } = versionEntry;\n for (; version < expected; version++) {\n // find an available migration\n const migration = migrations[name].find(v => v.version === version);\n\n // no migration found, ignore\n if (!migration) break;\n\n // run the upgrade\n migration.upgrade(store);\n }\n\n // update the stored version\n await store.update({ _id: versionEntry._id }, { version });\n }\n }),\n );\n }\n\n // get the running instance id of this omegga\n async getInstanceId() {\n if (serverInstance) return serverInstance._id;\n\n const doc = await this.stores.server.insert<IStoreServerInstance>({\n type: 'app:start',\n date: Date.now(),\n });\n\n serverInstance = doc;\n\n // create a calendar of all previous chat messages\n const dates = (\n await this.stores.chat.find<IStoreChat>({ type: 'chat' })\n ).map(c => c.created);\n for (const d of dates) {\n this.calendar.addDate(d);\n }\n\n const watcher = chokidar.watch(\n path.join(this.omegga.configPath, 'BanList.json'),\n { persistent: false },\n );\n watcher\n .on('add', () => setTimeout(this.syncBanList.bind(this), 500))\n .on('change', () =>\n setTimeout(() => {\n this.syncBanList();\n this.emit('update.bans');\n }, 500),\n );\n setTimeout(this.syncBanList.bind(this), 500);\n\n const handleKick = (kicked: IPlayer, kicker: IPlayer, reason: string) => {\n // time the event listener should expire\n const eventExpire = Date.now() + 100;\n\n // detect a single leave\n this.omegga.once('leave', async leavingPlayer => {\n const now = Date.now();\n // if a player leaves more than 100ms from the kick command, it's bugged\n if (now > eventExpire) return;\n\n if (leavingPlayer.id === kicked.id) {\n const entry = {\n type: 'kickHistory',\n kicked: kicked.id,\n kickerId: kicker.id,\n created: now,\n reason,\n };\n\n // depending on implementation, this could potentially be a kick event\n await this.stores.players.update(\n entry,\n { $set: entry },\n { upsert: true },\n );\n }\n });\n };\n\n // for a5 is based on the chat event\n this.omegga.on('kick', (kicked, kicker, reason) => {\n const kickedPlayer = this.omegga.findPlayerByName(kicked);\n const kickerPlayer = this.omegga.findPlayerByName(kicker);\n\n // couldn't find kicked or kicker player\n if (!kickedPlayer || !kickerPlayer) return;\n\n handleKick(kickedPlayer, kickerPlayer, reason);\n });\n\n return doc._id;\n }\n\n // synchronize the ban list with the one in the database\n syncBanList() {\n const banList = this.omegga.getBanList()?.banList;\n if (!banList) return;\n\n // upsert all bans\n for (const banned in banList) {\n const entry: IStoreBanHistory = {\n type: 'banHistory',\n banned,\n bannerId: banList[banned].bannerId,\n created: parseBrickadiaTime(banList[banned].created),\n expires: parseBrickadiaTime(banList[banned].expires),\n reason: banList[banned].reason,\n };\n\n this.stores.players.update(entry, { $set: entry }, { upsert: true });\n }\n }\n\n // determine if this user would be the first user (admin user)\n async isFirstUser() {\n return (await this.stores.users.count({ type: 'user' })) === 0;\n }\n\n // hash a password\n async hash(password: string) {\n return await bcrypt.hash(password, 10);\n }\n\n // create an admin user account, username can be blank (no password)\n async createAdminUser(username: string, password: string) {\n const hash = await this.hash(password);\n\n // create an owner user\n const user = this.stores.users.insert({\n // this is a user\n type: 'user',\n created: Date.now(),\n lastOnline: Date.now(),\n\n // credentials\n username,\n hash,\n\n // permissions\n isOwner: true,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n\n // mfa\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n\n // brickadia player uuid\n playerId: '',\n });\n return user;\n }\n\n // determine if a user with this name exists\n async userExists(username: string) {\n return (await this.stores.users.count({ type: 'user', username })) > 0;\n }\n\n // create a regular user account\n async createUser(username: string, password: string) {\n if (await this.userExists(username)) throw new Error('user already exists');\n\n const hash = await this.hash(password);\n // create a regular user\n const user = this.stores.users.insert<IStoreUser>({\n // this is a user\n type: 'user',\n created: Date.now(),\n lastOnline: 0,\n\n // credentials\n username,\n hash,\n\n // permissions\n isOwner: false,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n\n // mfa\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n\n // brickadia player uuid\n playerId: '',\n });\n return user;\n }\n\n // set a user's password\n async userPasswd(username: string, password: string) {\n const hash = await this.hash(password);\n\n if (!(await this.userExists(username)))\n throw new Error('user does not exist');\n\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { hash } },\n );\n }\n\n async banUser(username: string, banned: boolean) {\n const result = await this.stores.users.update(\n { type: 'user', username },\n { $set: { isBanned: banned } },\n );\n if (banned) serverEvents.emit('userInvalidated', username);\n return result;\n }\n\n async deleteUser(username: string) {\n const result = await this.stores.users.remove(\n { type: 'user', username },\n {},\n );\n serverEvents.emit('userInvalidated', username);\n return result;\n }\n\n async setUserPermissions(username: string, permissions: PermissionSet) {\n return await this.stores.users.update(\n { type: 'user', username },\n { $set: { permissions: encodePermissions(permissions) } },\n );\n }\n\n async getDefaultPermissions(): Promise<IStoreDefaultPermissions> {\n if (this.defaultPermissionsCache) return this.defaultPermissionsCache;\n let doc = await this.stores.server.findOne<IStoreDefaultPermissions>({\n type: 'defaultPermissions',\n });\n if (!doc) {\n doc = await this.stores.server.insert<IStoreDefaultPermissions>({\n type: 'defaultPermissions',\n root: RootLevel.Read,\n domains: {},\n scopes: {},\n });\n }\n const decoded = decodePermissions(doc);\n const result = { ...doc, ...decoded };\n this.defaultPermissionsCache = result;\n return result;\n }\n\n async setDefaultPermissions(\n permissions: Omit<IStoreDefaultPermissions, 'type'>,\n ) {\n const encoded = encodePermissions(permissions as PermissionSet);\n await this.stores.server.update(\n { type: 'defaultPermissions' },\n {\n $set: {\n root: encoded.root,\n domains: encoded.domains,\n scopes: encoded.scopes,\n },\n },\n { upsert: true },\n );\n this.defaultPermissionsCache = null;\n }\n\n // get a user from credentials\n async authUser(username: string, password: string) {\n const user = await this.stores.users.findOne<IStoreUser>({ username });\n // user not found\n if (!user || user.isBanned) return null;\n\n // make sure the user's password hash is valid\n if (await bcrypt.compare(password, user.hash)) {\n // update last online status\n await this.stores.users.update(\n { _id: user._id },\n { $set: { lastOnline: Date.now() } },\n );\n return user;\n }\n\n // wrong password\n return null;\n }\n\n // find a user by object id\n async findUserById(id: string) {\n const user = await this.stores.users.findOne<IStoreUser>({\n $or: [\n // the owner has no username, so everyone is the owner\n { type: 'user', username: '', isOwner: true },\n\n // the user exists and has an id\n { type: 'user', _id: id },\n ],\n });\n if (user) {\n user.permissions = decodePermissions(user.permissions);\n if (!user.totpEnabled) user.totpEnabled = false;\n if (!user.passkeys) user.passkeys = [];\n }\n return user;\n }\n\n async setUserTotp(username: string, secret: string, enabled: boolean) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { totpSecret: secret, totpEnabled: enabled } },\n );\n }\n\n async disableUserTotp(username: string) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { totpEnabled: false }, $unset: { totpSecret: true } },\n );\n }\n\n async addPasskey(username: string, credential: IWebAuthnCredential) {\n await this.stores.users.update(\n { type: 'user', username },\n { $push: { passkeys: credential } },\n );\n }\n\n async removePasskey(username: string, credentialId: string) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const passkeys = (user.passkeys ?? []).filter(p => p.id !== credentialId);\n await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });\n }\n\n async updatePasskeyCounter(\n username: string,\n credentialId: string,\n counter: number,\n ) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const passkeys = (user.passkeys ?? []).map(p =>\n p.id === credentialId ? { ...p, counter, lastUsed: Date.now() } : p,\n );\n await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });\n }\n\n async setRecoveryCodes(username: string, hashedCodes: string[]) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { recoveryCodes: hashedCodes } },\n );\n }\n\n async removeRecoveryCode(username: string, hashedCode: string) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const codes = (user.recoveryCodes ?? []).filter(c => c !== hashedCode);\n await this.stores.users.update(\n { _id: user._id },\n { $set: { recoveryCodes: codes } },\n );\n }\n\n async resetUserMfa(username: string) {\n await this.stores.users.update(\n { type: 'user', username },\n {\n $set: {\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n },\n $unset: { totpSecret: true },\n },\n );\n }\n\n async findUserByPasskeyId(credentialId: string) {\n return await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n 'passkeys.id': credentialId,\n });\n }\n\n // get a list of roles\n // TODO: actually implement roles\n getRoles(): Promise<{ type: 'role'; name: 'string' }[]> {\n return this.stores.server.find({ type: 'role' });\n }\n\n // add a chat message to the chat log store\n async addChatLog(\n action: 'msg' | 'server' | 'leave' | 'join',\n user: IPlayer,\n message?: string,\n ) {\n this.calendar.addDate(Date.now());\n return await this.stores.chat.insert<IStoreChat>({\n type: 'chat',\n created: Date.now(),\n instanceId: await this.getInstanceId(),\n action,\n user,\n ...(message ? { message } : {}),\n });\n }\n\n // get recent chat activity\n async getChats({\n count = 50,\n sameServer,\n before,\n after,\n }: {\n count?: number;\n sameServer?: boolean;\n before?: number;\n after?: number;\n } = {}) {\n return await this.stores.chat\n .find<IStoreChat>({\n type: 'chat',\n ...(sameServer ? { instanceId: await this.getInstanceId() } : {}),\n created: before\n ? { $lt: before }\n : after\n ? { $gt: after }\n : { $lt: Date.now() },\n })\n .sort({ created: !before && after ? 1 : -1 })\n .limit(count)\n .exec();\n }\n\n // get paginated players\n async getPlayers({\n count = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n limitId = undefined,\n }: {\n count?: number;\n search?: string;\n page?: number;\n sort?: string;\n direction?: number;\n limitId?: string[];\n } = {}) {\n const pattern = explode(search);\n\n // the query used for finding which players are available\n const query = {\n type: 'userHistory',\n // only filter if there's a query\n $and: [\n // if a limitId is passed in, the id must be in it\n ...(limitId ? [{ id: { $in: limitId } }] : []),\n\n // if there's a search, the search matches...\n ...(search.length > 0\n ? [\n {\n $or: [\n // id was pasted in\n { id: search },\n // user's current name\n { name: { $regex: pattern } },\n { displayName: { $regex: pattern } },\n // user's past name\n {\n nameHistory: { $elemMatch: { name: { $regex: pattern } } },\n },\n {\n nameHistory: {\n $elemMatch: { displayName: { $regex: pattern } },\n },\n },\n ],\n },\n ]\n : []),\n ],\n };\n\n // the query used for finding a specific player\n const exactQuery = {\n type: 'userHistory',\n // only filter if there's a query\n $or: [\n // id was pasted in\n { id: search },\n // user's current name\n { name: search },\n // user's current display name\n { displayName: search },\n ],\n };\n\n // count players and query players\n const [total, players, exactResult] = await Promise.all([\n this.stores.players.count(query),\n this.stores.players\n .find<IUserHistory>(query)\n // TODO: add other sorts and sort directions\n .sort({ [sort]: direction })\n .skip(count * page)\n .limit(count)\n .exec(),\n\n // exact match detection\n search.length > 0 && page === 0\n ? this.stores.players.find(exactQuery)\n : [],\n ]);\n\n // add exact matches to the beginning\n if (exactResult.length > 0) {\n // remove duplicates\n for (const res of exactResult) {\n const index = players.findIndex(p => p._id === res._id);\n players.splice(index, 1);\n }\n\n // add results to the beginning\n players.splice(0, 0, ...exactResult);\n }\n\n return {\n pages: Math.ceil(total / count),\n total,\n players,\n };\n }\n\n // get paginated users\n async getUsers({\n count = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n } = {}) {\n const pattern = explode(search);\n\n // the query used for finding which players are available\n const query = {\n type: 'user',\n // only filter if there's a query\n ...(search.length > 0\n ? {\n $or: [\n // id was pasted in\n { playerId: search },\n // user's current name\n { name: { $regex: pattern } },\n ],\n }\n : {}),\n };\n\n // count players and query players\n const [total, users] = await Promise.all([\n this.stores.users.count(query),\n this.stores.users\n .find<IStoreUser>(query)\n // TODO: add other sorts and sort directions\n .sort({ [sort]: direction })\n .skip(count * page)\n .limit(count)\n .exec(),\n ]);\n\n return {\n pages: Math.ceil(total / count),\n total,\n users,\n };\n }\n\n // get an individual player, ban and kick history, and notes\n async getPlayer(id: string) {\n const [player, banHistory, kickHistory, notes] = await Promise.all([\n this.stores.players.findOne<IUserHistory>({ type: 'userHistory', id }),\n this.stores.players\n .find<IStoreBanHistory>({ type: 'banHistory', banned: id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n this.stores.players\n .find<IStoreKickHistory>({ type: 'kickHistory', kicked: id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n this.stores.players\n .find<IUserNote>({ type: 'note', id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n ]);\n\n if (!player) return null;\n\n return { ...player, banHistory, kickHistory, notes };\n }\n\n // add a user to the visit history, returns true if this is a first visit\n async addVisit(user: Required<IPlayer>) {\n const existing = await this.stores.players.findOne<IUserHistory>({\n type: 'userHistory',\n id: user.id,\n });\n const now = Date.now();\n const instanceId = await this.getInstanceId();\n if (!existing) {\n // create the player\n // TODO: add some form of admin note tool for moderators to leave notes\n // on troublesome players + warnings\n await this.stores.players.insert<IUserHistory>({\n type: 'userHistory',\n // base brickadia user info\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n // list of names and when they were first used\n nameHistory: [\n { name: user.name, displayName: user.displayName, date: now },\n ],\n // list of ips this player has been on\n ips: [],\n // first time player was seen\n created: now,\n // last time player was seen\n lastSeen: now,\n // last server this player has been on\n lastInstanceId: instanceId,\n // number of status checks this player has observed\n heartbeats: 0,\n // number of sessions (at least an hour apart) this player has had\n sessions: 1,\n // number of instances (times omegga was started) this player has joined\n instances: 1,\n });\n return true;\n } else {\n await this.stores.players.update<IUserHistory>(\n { _id: existing._id },\n {\n $inc: {\n // increment number of sessions if the player hasn't been seen for an hour\n sessions: existing.lastSeen < now - 60 * 60 * 1000 ? 1 : 0,\n // increment the number of instances this player has joined\n instances: existing.lastInstanceId === instanceId ? 1 : 0,\n },\n\n $set: {\n // update last seen time and user name\n lastSeen: now,\n name: user.name,\n displayName: user.displayName,\n\n // set the last joined instance\n lastInstanceId: instanceId,\n },\n\n // add the name to the history if it was not already in it\n ...(existing.nameHistory.some(\n h => h.name === user.name && h.displayName === user.displayName,\n )\n ? {}\n : {\n $addToSet: {\n nameHistory: {\n name: user.name,\n displayName: user.displayName,\n date: now,\n },\n },\n }),\n },\n );\n return false;\n }\n }\n\n // use data from minutely heartbeats to fuel metrics\n async addHeartbeat(data: {\n bricks: number;\n players: string[];\n ips: Record<string, string>;\n }) {\n const now = Date.now();\n\n // add the heartbeat to the server status store\n await this.stores.status.insert({\n type: 'heartbeat',\n created: now,\n bricks: data.bricks,\n players: data.players,\n });\n\n // update player heartbeats and last seens\n await this.stores.players.update(\n {\n // all players in the status update\n id: { $in: data.players },\n },\n {\n // update last seen\n $set: { lastSeen: now },\n // increment heartbeats\n $inc: { heartbeats: 1 },\n },\n { multi: true },\n );\n\n // get all players in the status update\n const players = await this.stores.players.find<IUserHistory>({\n id: { $in: data.players },\n });\n\n await Promise.all(\n players\n // find players without the discovered ip\n .filter(p => !(p.ips || []).includes(data.ips[p.id]))\n // insert the IP into the player's ip list\n .map(p =>\n this.stores.players.update(\n { _id: p._id },\n {\n $addToSet: { ips: data.ips[p.id] },\n },\n ),\n ),\n );\n }\n\n async getAutoRestartConfig(): Promise<IStoreAutoRestartConfig> {\n let config = await this.stores.server.findOne<IStoreAutoRestartConfig>({\n type: 'autoRestartConfig',\n });\n if (!config) {\n config = await this.stores.server.insert({\n type: 'autoRestartConfig',\n maxUptime: 48,\n maxUptimeEnabled: false,\n emptyUptime: 24,\n emptyUptimeEnabled: false,\n dailyHour: 2,\n dailyHourEnabled: false,\n autoUpdateEnabled: true,\n autoUpdateIntervalMins: 60,\n announcementEnabled: true,\n playersEnabled: true,\n saveWorld: true,\n crashRestartEnabled: true,\n });\n }\n\n config.autoUpdateEnabled ??= true;\n config.autoUpdateIntervalMins ??= 60;\n config.saveWorld ??= true;\n config.crashRestartEnabled ??= true;\n\n return config;\n }\n\n async setAutoRestartConfig(config: IStoreAutoRestartConfig): Promise<void> {\n config.maxUptime = Math.round(Math.max(1, Math.min(config.maxUptime, 168)));\n config.emptyUptime = Math.round(\n Math.max(1, Math.min(config.emptyUptime, 168)),\n );\n config.dailyHour = Math.round(Math.max(0, Math.min(config.dailyHour, 23)));\n config.autoUpdateIntervalMins = Math.round(\n Math.max(10, Math.min(config.autoUpdateIntervalMins ?? 60, Infinity)),\n );\n config.autoUpdateEnabled ??= true;\n await this.stores.server.update(\n { type: 'autoRestartConfig' },\n {\n $set: {\n ...config,\n },\n },\n { upsert: false },\n );\n }\n\n // update player online-time punchcard\n async updatePlayerPunchcard(numNewPlayers: number) {\n // get date, utc hour, and utc day\n const now = new Date();\n const time = now.getTime();\n const hour = now.getUTCHours();\n const day = now.getUTCDay();\n const month = now.getUTCMonth();\n const year = now.getUTCFullYear();\n\n // find the punchcard\n const card = await this.stores.status.findOne<IPunchcard>({\n type: 'punchcard',\n kind: 'playerCount',\n month,\n year,\n });\n\n // create a new punchcard if one does not exist for this month\n if (!card) {\n const punchcard = createPunchcard();\n punchcard[day][hour] = numNewPlayers;\n // insert it\n await this.stores.status.insert<IPunchcard>({\n type: 'punchcard',\n kind: 'playerCount',\n created: time,\n updated: time,\n month,\n year,\n punchcard,\n });\n\n // if the card exists, check if it was already updated this hour\n } else {\n // add the players to this slot\n card.punchcard[day][hour] += numNewPlayers;\n // update the punchcard\n await this.stores.status.update(\n { _id: card._id },\n {\n $set: {\n punchcard: card.punchcard,\n updated: time,\n },\n },\n );\n }\n }\n}\n"],"names":["Calendar","soft","EMPTY_PERMISSIONS","encodePermissions","parseBrickadiaTime","serverEvents","permissions","RootLevel","decodePermissions","pattern","explode","time"],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sources":["../../../src/webserver/backend/database.ts"],"sourcesContent":["import soft from '@/softconfig';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { explode } from '@util/pattern';\nimport { parseBrickadiaTime } from '@util/time';\nimport bcrypt from 'bcryptjs';\nimport chokidar from 'chokidar';\nimport EventEmitter from 'events';\nimport Datastore from 'nedb-promises';\nimport path from 'path';\nimport Calendar from './calendar';\nimport { serverEvents } from './events';\nimport {\n EMPTY_PERMISSIONS,\n RootLevel,\n decodePermissions,\n encodePermissions,\n type PermissionSet,\n} from './permissions';\nimport {\n IPlayer,\n IPunchcard,\n IStoreAutoRestartConfig,\n IStoreBanHistory,\n IStoreChat,\n IStoreDefaultPermissions,\n IStoreKickHistory,\n IStoreRole,\n IStoreServerInstance,\n IStoreUser,\n IStoreVersion,\n IUserHistory,\n IUserNote,\n IWebAuthnCredential,\n} from './types';\n\n// TODO: online users graph\n// TODO: player online times\n// TODO: bricks over time graph\n// TODO: minute server status for metrics\n// TODO: chat messages per min/hour/day check\n\nlet serverInstance: IStoreServerInstance & { _id: string };\n\ntype FixedSizeArray<N extends number, T, M extends string = '0'> = {\n readonly [k in M]: any;\n} & { length: N } & ReadonlyArray<T>;\n\n// generate a punchcard (days x week)\nconst createPunchcard = (): number[][] =>\n Array.from({ length: 7 }).map(() =>\n Array.from<number>({ length: 24 }).fill(0),\n );\n\n// the database keeps track of metrics for omegga\nexport default class Database extends EventEmitter {\n options: IServerConfig;\n omegga: Omegga;\n stores: {\n users: Datastore;\n chat: Datastore;\n players: Datastore;\n status: Datastore;\n server: Datastore;\n };\n defaultPermissionsCache: IStoreDefaultPermissions | null = null;\n rolesCache: (IStoreRole & { _id: string })[] | null = null;\n calendar: Calendar;\n\n constructor(options: IServerConfig, omegga: Omegga) {\n super();\n this.options = options;\n this.omegga = omegga;\n this.calendar = new Calendar();\n\n // database\n const dbOpts = {\n autoload: true,\n // case insensitive string comparison\n compareStrings: (a: string, b: string) =>\n a.localeCompare(b, 'en', { ignorePunctuation: true }),\n };\n\n // create all the stores\n this.stores = {\n users: Datastore.create({\n filename: path.join(omegga.dataPath, soft.USER_STORE),\n ...dbOpts,\n }),\n chat: Datastore.create({\n filename: path.join(omegga.dataPath, soft.CHAT_STORE),\n ...dbOpts,\n }),\n players: Datastore.create({\n filename: path.join(omegga.dataPath, soft.PLAYER_STORE),\n ...dbOpts,\n }),\n status: Datastore.create({\n filename: path.join(omegga.dataPath, soft.STATUS_STORE),\n ...dbOpts,\n }),\n server: Datastore.create({\n filename: path.join(omegga.dataPath, soft.SERVER_STORE),\n ...dbOpts,\n }),\n };\n }\n\n // make sure all the databases have valid store versions\n async doMigrations() {\n // current store versions\n const storeVersions: { [key: keyof typeof this.stores]: number } = {\n users: 2,\n chat: 1,\n players: 1,\n status: 1,\n server: 2,\n\n // example version\n dummy: 10,\n };\n\n // migrations to/from certain store versions\n const migrations: {\n [key: keyof typeof this.stores]: {\n version: number;\n upgrade: (store: Datastore) => Promise<void>;\n }[];\n } = {\n users: [\n {\n version: 1,\n upgrade: async (store: Datastore) => {\n const users = await store.find<IStoreUser & { _id: string }>({\n type: 'user',\n });\n for (const user of users) {\n const p = (user.permissions as any) ?? EMPTY_PERMISSIONS;\n const root =\n p.root === 'all' || p.root === 'read' ? p.root : 'off';\n const domains: Record<string, string> = {};\n for (const [k, v] of Object.entries(p.domains ?? {})) {\n if (v !== 'unset' && v !== 'none') domains[k] = v as string;\n }\n const scopes: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(p.scopes ?? {})) {\n if (v === 'enabled' || v === true) scopes[k] = true;\n else if (v === 'disabled' || v === false) scopes[k] = false;\n }\n const encoded = encodePermissions({\n root,\n domains,\n scopes,\n } as PermissionSet);\n await store.update(\n { _id: user._id },\n { $set: { permissions: encoded } },\n );\n }\n },\n },\n ],\n chat: [],\n players: [],\n status: [],\n server: [\n {\n version: 1,\n upgrade: async (store: Datastore) => {\n const doc = await store.findOne<any>({\n type: 'defaultPermissions',\n });\n if (!doc) return;\n const root = doc.root === 'unset' ? 'off' : doc.root;\n const domains: Record<string, string> = {};\n for (const [k, v] of Object.entries(doc.domains ?? {})) {\n if (v !== 'unset') domains[k] = v as string;\n }\n const scopes: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(doc.scopes ?? {})) {\n if (v === 'enabled') scopes[k] = true;\n else if (v === 'disabled') scopes[k] = false;\n }\n const encoded = encodePermissions({\n root,\n domains,\n scopes,\n } as PermissionSet);\n await store.update(\n { _id: doc._id },\n {\n $set: {\n root: encoded.root,\n domains: encoded.domains,\n scopes: encoded.scopes,\n },\n },\n );\n },\n },\n ],\n\n // example migration list (all version upgrades go up by 1)\n dummy: [\n {\n version: 1, // target version\n upgrade: async _store => {}, // upgrade function (does not include updating version)\n },\n ],\n };\n\n // loop through all stores to find if any need version upgrades\n await Promise.all(\n Object.entries(this.stores).map(async ([name, store]) => {\n const versionEntry = await store.findOne<IStoreVersion>({\n type: 'storeVersion',\n });\n const expected = storeVersions[name];\n // if the version was not found, insert it\n if (!versionEntry) {\n store.insert({ type: 'storeVersion', version: expected });\n\n // if the version is below the expected version\n } else if (versionEntry.version < expected) {\n // while the version is below the expected version\n let { version } = versionEntry;\n for (; version < expected; version++) {\n // find an available migration\n const migration = migrations[name].find(v => v.version === version);\n\n // no migration found, ignore\n if (!migration) break;\n\n // run the upgrade\n migration.upgrade(store);\n }\n\n // update the stored version\n await store.update({ _id: versionEntry._id }, { version });\n }\n }),\n );\n }\n\n // get the running instance id of this omegga\n async getInstanceId() {\n if (serverInstance) return serverInstance._id;\n\n const doc = await this.stores.server.insert<IStoreServerInstance>({\n type: 'app:start',\n date: Date.now(),\n });\n\n serverInstance = doc;\n\n // create a calendar of all previous chat messages\n const dates = (\n await this.stores.chat.find<IStoreChat>({ type: 'chat' })\n ).map(c => c.created);\n for (const d of dates) {\n this.calendar.addDate(d);\n }\n\n const watcher = chokidar.watch(\n path.join(this.omegga.configPath, 'BanList.json'),\n { persistent: false },\n );\n watcher\n .on('add', () => setTimeout(this.syncBanList.bind(this), 500))\n .on('change', () =>\n setTimeout(() => {\n this.syncBanList();\n this.emit('update.bans');\n }, 500),\n );\n setTimeout(this.syncBanList.bind(this), 500);\n\n const handleKick = (kicked: IPlayer, kicker: IPlayer, reason: string) => {\n // time the event listener should expire\n const eventExpire = Date.now() + 100;\n\n // detect a single leave\n this.omegga.once('leave', async leavingPlayer => {\n const now = Date.now();\n // if a player leaves more than 100ms from the kick command, it's bugged\n if (now > eventExpire) return;\n\n if (leavingPlayer.id === kicked.id) {\n const entry = {\n type: 'kickHistory',\n kicked: kicked.id,\n kickerId: kicker.id,\n created: now,\n reason,\n };\n\n // depending on implementation, this could potentially be a kick event\n await this.stores.players.update(\n entry,\n { $set: entry },\n { upsert: true },\n );\n }\n });\n };\n\n // for a5 is based on the chat event\n this.omegga.on('kick', (kicked, kicker, reason) => {\n const kickedPlayer = this.omegga.findPlayerByName(kicked);\n const kickerPlayer = this.omegga.findPlayerByName(kicker);\n\n // couldn't find kicked or kicker player\n if (!kickedPlayer || !kickerPlayer) return;\n\n handleKick(kickedPlayer, kickerPlayer, reason);\n });\n\n return doc._id;\n }\n\n // synchronize the ban list with the one in the database\n syncBanList() {\n const banList = this.omegga.getBanList()?.banList;\n if (!banList) return;\n\n // upsert all bans\n for (const banned in banList) {\n const entry: IStoreBanHistory = {\n type: 'banHistory',\n banned,\n bannerId: banList[banned].bannerId,\n created: parseBrickadiaTime(banList[banned].created),\n expires: parseBrickadiaTime(banList[banned].expires),\n reason: banList[banned].reason,\n };\n\n this.stores.players.update(entry, { $set: entry }, { upsert: true });\n }\n }\n\n // determine if this user would be the first user (admin user)\n async isFirstUser() {\n return (await this.stores.users.count({ type: 'user' })) === 0;\n }\n\n // hash a password\n async hash(password: string) {\n return await bcrypt.hash(password, 10);\n }\n\n // create an admin user account, username can be blank (no password)\n async createAdminUser(username: string, password: string) {\n const hash = await this.hash(password);\n\n // create an owner user\n const user = this.stores.users.insert({\n // this is a user\n type: 'user',\n created: Date.now(),\n lastOnline: Date.now(),\n\n // credentials\n username,\n hash,\n\n // permissions\n isOwner: true,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n\n // mfa\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n\n // brickadia player uuid\n playerId: '',\n });\n return user;\n }\n\n // determine if a user with this name exists\n async userExists(username: string) {\n return (await this.stores.users.count({ type: 'user', username })) > 0;\n }\n\n // create a regular user account\n async createUser(username: string, password: string) {\n if (await this.userExists(username)) throw new Error('user already exists');\n\n const hash = await this.hash(password);\n // create a regular user\n const user = this.stores.users.insert<IStoreUser>({\n // this is a user\n type: 'user',\n created: Date.now(),\n lastOnline: 0,\n\n // credentials\n username,\n hash,\n\n // permissions\n isOwner: false,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n\n // mfa\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n\n // brickadia player uuid\n playerId: '',\n });\n return user;\n }\n\n // set a user's password\n async userPasswd(username: string, password: string) {\n const hash = await this.hash(password);\n\n if (!(await this.userExists(username)))\n throw new Error('user does not exist');\n\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { hash } },\n );\n }\n\n async banUser(username: string, banned: boolean) {\n const result = await this.stores.users.update(\n { type: 'user', username },\n { $set: { isBanned: banned } },\n );\n if (banned) serverEvents.emit('userInvalidated', username);\n return result;\n }\n\n async deleteUser(username: string) {\n const result = await this.stores.users.remove(\n { type: 'user', username },\n {},\n );\n serverEvents.emit('userInvalidated', username);\n return result;\n }\n\n async setUserPermissions(username: string, permissions: PermissionSet) {\n return await this.stores.users.update(\n { type: 'user', username },\n { $set: { permissions: encodePermissions(permissions) } },\n );\n }\n\n async getDefaultPermissions(): Promise<IStoreDefaultPermissions> {\n if (this.defaultPermissionsCache) return this.defaultPermissionsCache;\n let doc = await this.stores.server.findOne<IStoreDefaultPermissions>({\n type: 'defaultPermissions',\n });\n if (!doc) {\n doc = await this.stores.server.insert<IStoreDefaultPermissions>({\n type: 'defaultPermissions',\n root: RootLevel.Read,\n domains: {},\n scopes: {},\n });\n }\n const decoded = decodePermissions(doc);\n const result = { ...doc, ...decoded };\n this.defaultPermissionsCache = result;\n return result;\n }\n\n async setDefaultPermissions(\n permissions: Omit<IStoreDefaultPermissions, 'type'>,\n ) {\n const encoded = encodePermissions(permissions as PermissionSet);\n await this.stores.server.update(\n { type: 'defaultPermissions' },\n {\n $set: {\n root: encoded.root,\n domains: encoded.domains,\n scopes: encoded.scopes,\n },\n },\n { upsert: true },\n );\n this.defaultPermissionsCache = null;\n }\n\n // get a user from credentials\n async authUser(username: string, password: string) {\n const user = await this.stores.users.findOne<IStoreUser>({ username });\n // user not found\n if (!user || user.isBanned) return null;\n\n // make sure the user's password hash is valid\n if (await bcrypt.compare(password, user.hash)) {\n // update last online status\n await this.stores.users.update(\n { _id: user._id },\n { $set: { lastOnline: Date.now() } },\n );\n return user;\n }\n\n // wrong password\n return null;\n }\n\n // find a user by object id\n async findUserById(id: string) {\n const user = await this.stores.users.findOne<IStoreUser>({\n $or: [\n // the owner has no username, so everyone is the owner\n { type: 'user', username: '', isOwner: true },\n\n // the user exists and has an id\n { type: 'user', _id: id },\n ],\n });\n if (user) {\n user.permissions = decodePermissions(user.permissions);\n if (!user.totpEnabled) user.totpEnabled = false;\n if (!user.passkeys) user.passkeys = [];\n }\n return user;\n }\n\n async setUserTotp(username: string, secret: string, enabled: boolean) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { totpSecret: secret, totpEnabled: enabled } },\n );\n }\n\n async disableUserTotp(username: string) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { totpEnabled: false }, $unset: { totpSecret: true } },\n );\n }\n\n async addPasskey(username: string, credential: IWebAuthnCredential) {\n await this.stores.users.update(\n { type: 'user', username },\n { $push: { passkeys: credential } },\n );\n }\n\n async removePasskey(username: string, credentialId: string) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const passkeys = (user.passkeys ?? []).filter(p => p.id !== credentialId);\n await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });\n }\n\n async updatePasskeyCounter(\n username: string,\n credentialId: string,\n counter: number,\n ) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const passkeys = (user.passkeys ?? []).map(p =>\n p.id === credentialId ? { ...p, counter, lastUsed: Date.now() } : p,\n );\n await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });\n }\n\n async setRecoveryCodes(username: string, hashedCodes: string[]) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { recoveryCodes: hashedCodes } },\n );\n }\n\n async removeRecoveryCode(username: string, hashedCode: string) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const codes = (user.recoveryCodes ?? []).filter(c => c !== hashedCode);\n await this.stores.users.update(\n { _id: user._id },\n { $set: { recoveryCodes: codes } },\n );\n }\n\n async resetUserMfa(username: string) {\n await this.stores.users.update(\n { type: 'user', username },\n {\n $set: {\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n },\n $unset: { totpSecret: true },\n },\n );\n }\n\n async findUserByPasskeyId(credentialId: string) {\n return await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n 'passkeys.id': credentialId,\n });\n }\n\n async getAllRoles(): Promise<(IStoreRole & { _id: string })[]> {\n if (this.rolesCache) return this.rolesCache;\n const roles = await this.stores.server.find<IStoreRole & { _id: string }>({\n type: 'webRole',\n });\n roles.sort((a, b) => b.order - a.order);\n this.rolesCache = roles;\n return roles;\n }\n\n invalidateRolesCache() {\n this.rolesCache = null;\n this.defaultPermissionsCache = null;\n }\n\n async getRole(id: string): Promise<(IStoreRole & { _id: string }) | null> {\n return await this.stores.server.findOne<IStoreRole & { _id: string }>({\n _id: id,\n type: 'webRole',\n });\n }\n\n async createRole(\n name: string,\n description: string,\n permissions: PermissionSet,\n ): Promise<IStoreRole & { _id: string }> {\n await this.stores.server.update(\n { type: 'webRole' },\n { $inc: { order: 1 } },\n { multi: true },\n );\n const role = await this.stores.server.insert<IStoreRole>({\n type: 'webRole',\n name,\n description,\n order: 1,\n permissions: encodePermissions(permissions),\n });\n this.invalidateRolesCache();\n return role;\n }\n\n async updateRole(\n id: string,\n updates: Partial<Pick<IStoreRole, 'name' | 'description' | 'order'>> & {\n permissions?: PermissionSet;\n },\n ): Promise<void> {\n const $set: Record<string, any> = {};\n if (updates.name !== undefined) $set.name = updates.name;\n if (updates.description !== undefined)\n $set.description = updates.description;\n if (updates.order !== undefined) $set.order = updates.order;\n if (updates.permissions !== undefined)\n $set.permissions = encodePermissions(updates.permissions);\n await this.stores.server.update({ _id: id, type: 'webRole' }, { $set });\n this.invalidateRolesCache();\n }\n\n async deleteRole(id: string): Promise<string> {\n const role = await this.getRole(id);\n if (!role) return 'role not found';\n await this.stores.server.remove({ _id: id }, {});\n await this.stores.users.update(\n { type: 'user', roles: id },\n { $pull: { roles: id } },\n { multi: true },\n );\n this.invalidateRolesCache();\n return '';\n }\n\n async reorderRoles(orderedIds: string[]): Promise<void> {\n const len = orderedIds.length;\n for (let i = 0; i < len; i++) {\n await this.stores.server.update(\n { _id: orderedIds[i], type: 'webRole' },\n { $set: { order: len - i } },\n );\n }\n this.invalidateRolesCache();\n }\n\n async getUserRolePermissions(user: IStoreUser): Promise<PermissionSet[]> {\n const defaults = await this.getDefaultPermissions();\n const defaultPerms: PermissionSet = {\n root: defaults.root,\n domains: defaults.domains,\n scopes: defaults.scopes,\n };\n const allRoles = await this.getAllRoles();\n const userRoles = allRoles.filter(r => user.roles.includes(r._id));\n return [\n defaultPerms,\n ...userRoles.map(r => decodePermissions(r.permissions)),\n ];\n }\n\n async getUserAssignedRoles(\n user: IStoreUser,\n ): Promise<(IStoreRole & { _id: string })[]> {\n const allRoles = await this.getAllRoles();\n return allRoles.filter(r => user.roles.includes(r._id));\n }\n\n // add a chat message to the chat log store\n async addChatLog(\n action: 'msg' | 'server' | 'leave' | 'join',\n user: IPlayer,\n message?: string,\n ) {\n this.calendar.addDate(Date.now());\n return await this.stores.chat.insert<IStoreChat>({\n type: 'chat',\n created: Date.now(),\n instanceId: await this.getInstanceId(),\n action,\n user,\n ...(message ? { message } : {}),\n });\n }\n\n // get recent chat activity\n async getChats({\n count = 50,\n sameServer,\n before,\n after,\n }: {\n count?: number;\n sameServer?: boolean;\n before?: number;\n after?: number;\n } = {}) {\n return await this.stores.chat\n .find<IStoreChat>({\n type: 'chat',\n ...(sameServer ? { instanceId: await this.getInstanceId() } : {}),\n created: before\n ? { $lt: before }\n : after\n ? { $gt: after }\n : { $lt: Date.now() },\n })\n .sort({ created: !before && after ? 1 : -1 })\n .limit(count)\n .exec();\n }\n\n // get paginated players\n async getPlayers({\n count = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n limitId = undefined,\n }: {\n count?: number;\n search?: string;\n page?: number;\n sort?: string;\n direction?: number;\n limitId?: string[];\n } = {}) {\n const pattern = explode(search);\n\n // the query used for finding which players are available\n const query = {\n type: 'userHistory',\n // only filter if there's a query\n $and: [\n // if a limitId is passed in, the id must be in it\n ...(limitId ? [{ id: { $in: limitId } }] : []),\n\n // if there's a search, the search matches...\n ...(search.length > 0\n ? [\n {\n $or: [\n // id was pasted in\n { id: search },\n // user's current name\n { name: { $regex: pattern } },\n { displayName: { $regex: pattern } },\n // user's past name\n {\n nameHistory: { $elemMatch: { name: { $regex: pattern } } },\n },\n {\n nameHistory: {\n $elemMatch: { displayName: { $regex: pattern } },\n },\n },\n ],\n },\n ]\n : []),\n ],\n };\n\n // the query used for finding a specific player\n const exactQuery = {\n type: 'userHistory',\n // only filter if there's a query\n $or: [\n // id was pasted in\n { id: search },\n // user's current name\n { name: search },\n // user's current display name\n { displayName: search },\n ],\n };\n\n // count players and query players\n const [total, players, exactResult] = await Promise.all([\n this.stores.players.count(query),\n this.stores.players\n .find<IUserHistory>(query)\n // TODO: add other sorts and sort directions\n .sort({ [sort]: direction })\n .skip(count * page)\n .limit(count)\n .exec(),\n\n // exact match detection\n search.length > 0 && page === 0\n ? this.stores.players.find(exactQuery)\n : [],\n ]);\n\n // add exact matches to the beginning\n if (exactResult.length > 0) {\n // remove duplicates\n for (const res of exactResult) {\n const index = players.findIndex(p => p._id === res._id);\n players.splice(index, 1);\n }\n\n // add results to the beginning\n players.splice(0, 0, ...exactResult);\n }\n\n return {\n pages: Math.ceil(total / count),\n total,\n players,\n };\n }\n\n // get paginated users\n async getUsers({\n count = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n } = {}) {\n const pattern = explode(search);\n\n // the query used for finding which players are available\n const query = {\n type: 'user',\n // only filter if there's a query\n ...(search.length > 0\n ? {\n $or: [\n // id was pasted in\n { playerId: search },\n // user's current name\n { name: { $regex: pattern } },\n ],\n }\n : {}),\n };\n\n // count players and query players\n const [total, users] = await Promise.all([\n this.stores.users.count(query),\n this.stores.users\n .find<IStoreUser>(query)\n // TODO: add other sorts and sort directions\n .sort({ [sort]: direction })\n .skip(count * page)\n .limit(count)\n .exec(),\n ]);\n\n return {\n pages: Math.ceil(total / count),\n total,\n users,\n };\n }\n\n // get an individual player, ban and kick history, and notes\n async getPlayer(id: string) {\n const [player, banHistory, kickHistory, notes] = await Promise.all([\n this.stores.players.findOne<IUserHistory>({ type: 'userHistory', id }),\n this.stores.players\n .find<IStoreBanHistory>({ type: 'banHistory', banned: id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n this.stores.players\n .find<IStoreKickHistory>({ type: 'kickHistory', kicked: id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n this.stores.players\n .find<IUserNote>({ type: 'note', id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n ]);\n\n if (!player) return null;\n\n return { ...player, banHistory, kickHistory, notes };\n }\n\n // add a user to the visit history, returns true if this is a first visit\n async addVisit(user: Required<IPlayer>) {\n const existing = await this.stores.players.findOne<IUserHistory>({\n type: 'userHistory',\n id: user.id,\n });\n const now = Date.now();\n const instanceId = await this.getInstanceId();\n if (!existing) {\n // create the player\n // TODO: add some form of admin note tool for moderators to leave notes\n // on troublesome players + warnings\n await this.stores.players.insert<IUserHistory>({\n type: 'userHistory',\n // base brickadia user info\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n // list of names and when they were first used\n nameHistory: [\n { name: user.name, displayName: user.displayName, date: now },\n ],\n // list of ips this player has been on\n ips: [],\n // first time player was seen\n created: now,\n // last time player was seen\n lastSeen: now,\n // last server this player has been on\n lastInstanceId: instanceId,\n // number of status checks this player has observed\n heartbeats: 0,\n // number of sessions (at least an hour apart) this player has had\n sessions: 1,\n // number of instances (times omegga was started) this player has joined\n instances: 1,\n });\n return true;\n } else {\n await this.stores.players.update<IUserHistory>(\n { _id: existing._id },\n {\n $inc: {\n // increment number of sessions if the player hasn't been seen for an hour\n sessions: existing.lastSeen < now - 60 * 60 * 1000 ? 1 : 0,\n // increment the number of instances this player has joined\n instances: existing.lastInstanceId === instanceId ? 1 : 0,\n },\n\n $set: {\n // update last seen time and user name\n lastSeen: now,\n name: user.name,\n displayName: user.displayName,\n\n // set the last joined instance\n lastInstanceId: instanceId,\n },\n\n // add the name to the history if it was not already in it\n ...(existing.nameHistory.some(\n h => h.name === user.name && h.displayName === user.displayName,\n )\n ? {}\n : {\n $addToSet: {\n nameHistory: {\n name: user.name,\n displayName: user.displayName,\n date: now,\n },\n },\n }),\n },\n );\n return false;\n }\n }\n\n // use data from minutely heartbeats to fuel metrics\n async addHeartbeat(data: {\n bricks: number;\n players: string[];\n ips: Record<string, string>;\n }) {\n const now = Date.now();\n\n // add the heartbeat to the server status store\n await this.stores.status.insert({\n type: 'heartbeat',\n created: now,\n bricks: data.bricks,\n players: data.players,\n });\n\n // update player heartbeats and last seens\n await this.stores.players.update(\n {\n // all players in the status update\n id: { $in: data.players },\n },\n {\n // update last seen\n $set: { lastSeen: now },\n // increment heartbeats\n $inc: { heartbeats: 1 },\n },\n { multi: true },\n );\n\n // get all players in the status update\n const players = await this.stores.players.find<IUserHistory>({\n id: { $in: data.players },\n });\n\n await Promise.all(\n players\n // find players without the discovered ip\n .filter(p => !(p.ips || []).includes(data.ips[p.id]))\n // insert the IP into the player's ip list\n .map(p =>\n this.stores.players.update(\n { _id: p._id },\n {\n $addToSet: { ips: data.ips[p.id] },\n },\n ),\n ),\n );\n }\n\n async getAutoRestartConfig(): Promise<IStoreAutoRestartConfig> {\n let config = await this.stores.server.findOne<IStoreAutoRestartConfig>({\n type: 'autoRestartConfig',\n });\n if (!config) {\n config = await this.stores.server.insert({\n type: 'autoRestartConfig',\n maxUptime: 48,\n maxUptimeEnabled: false,\n emptyUptime: 24,\n emptyUptimeEnabled: false,\n dailyHour: 2,\n dailyHourEnabled: false,\n autoUpdateEnabled: true,\n autoUpdateIntervalMins: 60,\n announcementEnabled: true,\n playersEnabled: true,\n saveWorld: true,\n crashRestartEnabled: true,\n });\n }\n\n config.autoUpdateEnabled ??= true;\n config.autoUpdateIntervalMins ??= 60;\n config.saveWorld ??= true;\n config.crashRestartEnabled ??= true;\n\n return config;\n }\n\n async setAutoRestartConfig(config: IStoreAutoRestartConfig): Promise<void> {\n config.maxUptime = Math.round(Math.max(1, Math.min(config.maxUptime, 168)));\n config.emptyUptime = Math.round(\n Math.max(1, Math.min(config.emptyUptime, 168)),\n );\n config.dailyHour = Math.round(Math.max(0, Math.min(config.dailyHour, 23)));\n config.autoUpdateIntervalMins = Math.round(\n Math.max(10, Math.min(config.autoUpdateIntervalMins ?? 60, Infinity)),\n );\n config.autoUpdateEnabled ??= true;\n await this.stores.server.update(\n { type: 'autoRestartConfig' },\n {\n $set: {\n ...config,\n },\n },\n { upsert: false },\n );\n }\n\n // update player online-time punchcard\n async updatePlayerPunchcard(numNewPlayers: number) {\n // get date, utc hour, and utc day\n const now = new Date();\n const time = now.getTime();\n const hour = now.getUTCHours();\n const day = now.getUTCDay();\n const month = now.getUTCMonth();\n const year = now.getUTCFullYear();\n\n // find the punchcard\n const card = await this.stores.status.findOne<IPunchcard>({\n type: 'punchcard',\n kind: 'playerCount',\n month,\n year,\n });\n\n // create a new punchcard if one does not exist for this month\n if (!card) {\n const punchcard = createPunchcard();\n punchcard[day][hour] = numNewPlayers;\n // insert it\n await this.stores.status.insert<IPunchcard>({\n type: 'punchcard',\n kind: 'playerCount',\n created: time,\n updated: time,\n month,\n year,\n punchcard,\n });\n\n // if the card exists, check if it was already updated this hour\n } else {\n // add the players to this slot\n card.punchcard[day][hour] += numNewPlayers;\n // update the punchcard\n await this.stores.status.update(\n { _id: card._id },\n {\n $set: {\n punchcard: card.punchcard,\n updated: time,\n },\n },\n );\n }\n }\n}\n"],"names":["Calendar","soft","EMPTY_PERMISSIONS","encodePermissions","parseBrickadiaTime","serverEvents","permissions","RootLevel","decodePermissions","pattern","explode","time"],"mappings":"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"}
@@ -36,31 +36,89 @@ function decodePermissions(p) {
36
36
  };
37
37
  }
38
38
  const ALL_SCOPES = Object.keys(scopes.SCOPES);
39
- function resolveScope(userPerms, defaultPerms, scopeName) {
39
+ const ROOT_RANK = {
40
+ [
41
+ "off"
42
+ /* Off */
43
+ ]: 0,
44
+ [
45
+ "read"
46
+ /* Read */
47
+ ]: 1,
48
+ [
49
+ "all"
50
+ /* All */
51
+ ]: 2
52
+ };
53
+ const DOMAIN_RANK = {
54
+ [
55
+ "none"
56
+ /* None */
57
+ ]: 0,
58
+ [
59
+ "read"
60
+ /* Read */
61
+ ]: 1,
62
+ [
63
+ "all"
64
+ /* All */
65
+ ]: 2
66
+ };
67
+ const ROOT_BY_RANK = [
68
+ "off",
69
+ "read",
70
+ "all"
71
+ /* All */
72
+ ];
73
+ function mergePermissionSets(...sets) {
74
+ let rootRank = 0;
75
+ const domains = {};
76
+ const scopes2 = {};
77
+ for (const s of sets) {
78
+ rootRank = Math.max(rootRank, ROOT_RANK[s.root] ?? 0);
79
+ for (const [d, level] of Object.entries(s.domains)) {
80
+ const cur = domains[d];
81
+ const curRank = cur ? DOMAIN_RANK[cur] : 0;
82
+ const newRank = DOMAIN_RANK[level] ?? 0;
83
+ if (newRank > curRank) domains[d] = level;
84
+ }
85
+ for (const [sc, val] of Object.entries(s.scopes)) {
86
+ if (val) scopes2[sc] = true;
87
+ }
88
+ }
89
+ return {
90
+ root: ROOT_BY_RANK[rootRank],
91
+ domains,
92
+ scopes: scopes2
93
+ };
94
+ }
95
+ function resolveScope(userPerms, rolePermissions, scopeName) {
40
96
  if (scopeName === scopes.ScopeName.SessionInfo) return true;
41
97
  const scopeDef = scopes.SCOPES[scopeName];
42
98
  if (userPerms.root === "all") return true;
43
- if (userPerms.root === "read") return scopeDef.readOnly;
99
+ if (userPerms.root === "read" && scopeDef.readOnly) return true;
44
100
  const domain = userPerms.domains[scopeDef.domain];
45
101
  if (domain === "all") return true;
46
- if (domain === "read") return scopeDef.readOnly;
47
- const scopeVal = userPerms.scopes[scopeName];
48
- if (scopeVal === true) return true;
49
- if (defaultPerms) return resolveScope(defaultPerms, null, scopeName);
102
+ if (domain === "read" && scopeDef.readOnly) return true;
103
+ if (userPerms.scopes[scopeName] === true) return true;
104
+ if (rolePermissions.length > 0) {
105
+ const merged = mergePermissionSets(...rolePermissions);
106
+ return resolveScope(merged, [], scopeName);
107
+ }
50
108
  return false;
51
109
  }
52
- function resolveAllScopes(userPerms, defaultPerms) {
110
+ function resolveAllScopes(userPerms, rolePermissions) {
53
111
  const result = {};
54
112
  for (const scope of ALL_SCOPES) {
55
- result[scope] = resolveScope(userPerms, defaultPerms, scope);
113
+ result[scope] = resolveScope(userPerms, rolePermissions, scope);
56
114
  }
57
115
  return result;
58
116
  }
59
- function userHasScope(user, scope, defaultPerms) {
117
+ function userHasScope(user, scope, rolePermissions) {
60
118
  if (user.isOwner) return true;
61
119
  return resolveScope(
62
120
  user.permissions ?? EMPTY_PERMISSIONS,
63
- defaultPerms,
121
+ rolePermissions,
64
122
  scope
65
123
  );
66
124
  }
@@ -68,6 +126,7 @@ exports.EMPTY_PERMISSIONS = EMPTY_PERMISSIONS;
68
126
  exports.RootLevel = RootLevel;
69
127
  exports.decodePermissions = decodePermissions;
70
128
  exports.encodePermissions = encodePermissions;
129
+ exports.mergePermissionSets = mergePermissionSets;
71
130
  exports.resolveAllScopes = resolveAllScopes;
72
131
  exports.resolveScope = resolveScope;
73
132
  exports.userHasScope = userHasScope;
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.js","sources":["../../../src/webserver/backend/permissions.ts"],"sourcesContent":["import { SCOPES, ScopeName, type Domain, type Scope } from './scopes';\n\nexport enum RootLevel {\n All = 'all',\n Read = 'read',\n Off = 'off',\n}\n\nexport enum DomainLevel {\n All = 'all',\n Read = 'read',\n None = 'none',\n}\n\nexport interface PermissionSet {\n root: RootLevel;\n domains: Partial<Record<Domain, DomainLevel>>;\n scopes: Partial<Record<Scope, boolean>>;\n}\n\nexport const EMPTY_PERMISSIONS: PermissionSet = {\n root: RootLevel.Off,\n domains: {},\n scopes: {},\n};\n\n// NeDB rejects field names containing dots, so we encode/decode\n// scope keys (e.g. \"server.status\") for storage\nconst DOT_ENCODE = /\\./g;\nconst DOT_DECODE = /:/g;\n\nfunction encodeKeys<V>(obj: Record<string, V>): Record<string, V> {\n const out: Record<string, V> = {};\n for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_ENCODE, ':')] = v;\n return out;\n}\n\nfunction decodeKeys<V>(obj: Record<string, V>): Record<string, V> {\n const out: Record<string, V> = {};\n for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_DECODE, '.')] = v;\n return out;\n}\n\nexport interface StoredPermissionSet {\n root: RootLevel;\n domains: Partial<Record<string, DomainLevel>>;\n scopes: Record<string, boolean>;\n}\n\nexport function encodePermissions(p: PermissionSet): StoredPermissionSet {\n return { root: p.root, domains: p.domains, scopes: encodeKeys(p.scopes) };\n}\n\nexport function decodePermissions(\n p: StoredPermissionSet | null | undefined,\n): PermissionSet {\n if (!p) return EMPTY_PERMISSIONS;\n return {\n root: p.root ?? RootLevel.Off,\n domains: p.domains ?? {},\n scopes: decodeKeys(p.scopes ?? {}),\n };\n}\n\nconst ALL_SCOPES = Object.keys(SCOPES) as Scope[];\n\nexport function resolveScope(\n userPerms: PermissionSet,\n defaultPerms: PermissionSet | null,\n scopeName: Scope,\n): boolean {\n if (scopeName === ScopeName.SessionInfo) return true;\n\n const scopeDef = SCOPES[scopeName];\n\n // 1. Root\n if (userPerms.root === RootLevel.All) return true;\n if (userPerms.root === RootLevel.Read) return scopeDef.readOnly;\n\n // 2. Domain\n const domain = userPerms.domains[scopeDef.domain as Domain];\n if (domain === DomainLevel.All) return true;\n if (domain === DomainLevel.Read) return scopeDef.readOnly;\n\n // 3. Scope (additive only - explicit true grants, false/absent falls through)\n const scopeVal = userPerms.scopes[scopeName];\n if (scopeVal === true) return true;\n\n // 4. Fall back to defaults\n if (defaultPerms) return resolveScope(defaultPerms, null, scopeName);\n\n return false;\n}\n\nexport function resolveAllScopes(\n userPerms: PermissionSet,\n defaultPerms: PermissionSet | null,\n): Record<Scope, boolean> {\n const result = {} as Record<Scope, boolean>;\n for (const scope of ALL_SCOPES) {\n result[scope] = resolveScope(userPerms, defaultPerms, scope);\n }\n return result;\n}\n\nexport function userHasScope(\n user: { isOwner: boolean; permissions?: PermissionSet },\n scope: Scope,\n defaultPerms: PermissionSet | null,\n): boolean {\n if (user.isOwner) return true;\n return resolveScope(\n user.permissions ?? EMPTY_PERMISSIONS,\n defaultPerms,\n scope,\n );\n}\n"],"names":["RootLevel","SCOPES","ScopeName"],"mappings":";;;AAEO,IAAK,8BAAAA,eAAL;AACLA,aAAA,KAAA,IAAM;AACNA,aAAA,MAAA,IAAO;AACPA,aAAA,KAAA,IAAM;AAHI,SAAAA;AAAA,GAAA,aAAA,CAAA,CAAA;AAkBL,MAAM,oBAAmC;AAAA,EAC9C,MAAM;AAAA,EACN,SAAS,CAAA;AAAA,EACT,QAAQ,CAAA;AACV;AAIA,MAAM,aAAa;AACnB,MAAM,aAAa;AAEnB,SAAS,WAAc,KAA2C;AAChE,QAAM,MAAyB,CAAA;AAC/B,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,EAAG,KAAI,EAAE,QAAQ,YAAY,GAAG,CAAC,IAAI;AAC5E,SAAO;AACT;AAEA,SAAS,WAAc,KAA2C;AAChE,QAAM,MAAyB,CAAA;AAC/B,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,EAAG,KAAI,EAAE,QAAQ,YAAY,GAAG,CAAC,IAAI;AAC5E,SAAO;AACT;AAQO,SAAS,kBAAkB,GAAuC;AACvE,SAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,SAAS,QAAQ,WAAW,EAAE,MAAM,EAAA;AACxE;AAEO,SAAS,kBACd,GACe;AACf,MAAI,CAAC,EAAG,QAAO;AACf,SAAO;AAAA,IACL,MAAM,EAAE,QAAQ;AAAA,IAChB,SAAS,EAAE,WAAW,CAAA;AAAA,IACtB,QAAQ,WAAW,EAAE,UAAU,CAAA,CAAE;AAAA,EAAA;AAErC;AAEA,MAAM,aAAa,OAAO,KAAKC,aAAM;AAE9B,SAAS,aACd,WACA,cACA,WACS;AACT,MAAI,cAAcC,OAAAA,UAAU,YAAa,QAAO;AAEhD,QAAM,WAAWD,OAAAA,OAAO,SAAS;AAGjC,MAAI,UAAU,SAAS,MAAe,QAAO;AAC7C,MAAI,UAAU,SAAS,OAAgB,QAAO,SAAS;AAGvD,QAAM,SAAS,UAAU,QAAQ,SAAS,MAAgB;AAC1D,MAAI,WAAW,MAAiB,QAAO;AACvC,MAAI,WAAW,OAAkB,QAAO,SAAS;AAGjD,QAAM,WAAW,UAAU,OAAO,SAAS;AAC3C,MAAI,aAAa,KAAM,QAAO;AAG9B,MAAI,aAAc,QAAO,aAAa,cAAc,MAAM,SAAS;AAEnE,SAAO;AACT;AAEO,SAAS,iBACd,WACA,cACwB;AACxB,QAAM,SAAS,CAAA;AACf,aAAW,SAAS,YAAY;AAC9B,WAAO,KAAK,IAAI,aAAa,WAAW,cAAc,KAAK;AAAA,EAC7D;AACA,SAAO;AACT;AAEO,SAAS,aACd,MACA,OACA,cACS;AACT,MAAI,KAAK,QAAS,QAAO;AACzB,SAAO;AAAA,IACL,KAAK,eAAe;AAAA,IACpB;AAAA,IACA;AAAA,EAAA;AAEJ;;;;;;;;"}
1
+ {"version":3,"file":"permissions.js","sources":["../../../src/webserver/backend/permissions.ts"],"sourcesContent":["import { SCOPES, ScopeName, type Domain, type Scope } from './scopes';\n\nexport enum RootLevel {\n All = 'all',\n Read = 'read',\n Off = 'off',\n}\n\nexport enum DomainLevel {\n All = 'all',\n Read = 'read',\n None = 'none',\n}\n\nexport interface PermissionSet {\n root: RootLevel;\n domains: Partial<Record<Domain, DomainLevel>>;\n scopes: Partial<Record<Scope, boolean>>;\n}\n\nexport const EMPTY_PERMISSIONS: PermissionSet = {\n root: RootLevel.Off,\n domains: {},\n scopes: {},\n};\n\n// NeDB rejects field names containing dots, so we encode/decode\n// scope keys (e.g. \"server.status\") for storage\nconst DOT_ENCODE = /\\./g;\nconst DOT_DECODE = /:/g;\n\nfunction encodeKeys<V>(obj: Record<string, V>): Record<string, V> {\n const out: Record<string, V> = {};\n for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_ENCODE, ':')] = v;\n return out;\n}\n\nfunction decodeKeys<V>(obj: Record<string, V>): Record<string, V> {\n const out: Record<string, V> = {};\n for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_DECODE, '.')] = v;\n return out;\n}\n\nexport interface StoredPermissionSet {\n root: RootLevel;\n domains: Partial<Record<string, DomainLevel>>;\n scopes: Record<string, boolean>;\n}\n\nexport function encodePermissions(p: PermissionSet): StoredPermissionSet {\n return { root: p.root, domains: p.domains, scopes: encodeKeys(p.scopes) };\n}\n\nexport function decodePermissions(\n p: StoredPermissionSet | null | undefined,\n): PermissionSet {\n if (!p) return EMPTY_PERMISSIONS;\n return {\n root: p.root ?? RootLevel.Off,\n domains: p.domains ?? {},\n scopes: decodeKeys(p.scopes ?? {}),\n };\n}\n\nconst ALL_SCOPES = Object.keys(SCOPES) as Scope[];\n\nconst ROOT_RANK: Record<RootLevel, number> = {\n [RootLevel.Off]: 0,\n [RootLevel.Read]: 1,\n [RootLevel.All]: 2,\n};\n\nconst DOMAIN_RANK: Record<DomainLevel, number> = {\n [DomainLevel.None]: 0,\n [DomainLevel.Read]: 1,\n [DomainLevel.All]: 2,\n};\n\nconst ROOT_BY_RANK: RootLevel[] = [\n RootLevel.Off,\n RootLevel.Read,\n RootLevel.All,\n];\nconst DOMAIN_BY_RANK: DomainLevel[] = [\n DomainLevel.None,\n DomainLevel.Read,\n DomainLevel.All,\n];\n\nexport function mergePermissionSets(...sets: PermissionSet[]): PermissionSet {\n let rootRank = 0;\n const domains: Partial<Record<Domain, DomainLevel>> = {};\n const scopes: Partial<Record<Scope, boolean>> = {};\n\n for (const s of sets) {\n rootRank = Math.max(rootRank, ROOT_RANK[s.root] ?? 0);\n for (const [d, level] of Object.entries(s.domains) as [\n Domain,\n DomainLevel,\n ][]) {\n const cur = domains[d];\n const curRank = cur ? DOMAIN_RANK[cur] : 0;\n const newRank = DOMAIN_RANK[level] ?? 0;\n if (newRank > curRank) domains[d] = level;\n }\n for (const [sc, val] of Object.entries(s.scopes) as [Scope, boolean][]) {\n if (val) scopes[sc] = true;\n }\n }\n\n return {\n root: ROOT_BY_RANK[rootRank],\n domains,\n scopes,\n };\n}\n\nexport function resolveScope(\n userPerms: PermissionSet,\n rolePermissions: PermissionSet[],\n scopeName: Scope,\n): boolean {\n if (scopeName === ScopeName.SessionInfo) return true;\n\n const scopeDef = SCOPES[scopeName];\n\n // 1. Root\n if (userPerms.root === RootLevel.All) return true;\n if (userPerms.root === RootLevel.Read && scopeDef.readOnly) return true;\n\n // 2. Domain\n const domain = userPerms.domains[scopeDef.domain as Domain];\n if (domain === DomainLevel.All) return true;\n if (domain === DomainLevel.Read && scopeDef.readOnly) return true;\n\n // 3. Scope (additive only - explicit true grants, false/absent falls through)\n if (userPerms.scopes[scopeName] === true) return true;\n\n // 4. Fall back to merged role permissions\n if (rolePermissions.length > 0) {\n const merged = mergePermissionSets(...rolePermissions);\n return resolveScope(merged, [], scopeName);\n }\n\n return false;\n}\n\nexport function resolveAllScopes(\n userPerms: PermissionSet,\n rolePermissions: PermissionSet[],\n): Record<Scope, boolean> {\n const result = {} as Record<Scope, boolean>;\n for (const scope of ALL_SCOPES) {\n result[scope] = resolveScope(userPerms, rolePermissions, scope);\n }\n return result;\n}\n\nexport function userHasScope(\n user: { isOwner: boolean; permissions?: PermissionSet },\n scope: Scope,\n rolePermissions: PermissionSet[],\n): boolean {\n if (user.isOwner) return true;\n return resolveScope(\n user.permissions ?? EMPTY_PERMISSIONS,\n rolePermissions,\n scope,\n );\n}\n"],"names":["RootLevel","SCOPES","scopes","ScopeName"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const permissions = require("./permissions.js");
4
+ const trpc = require("./trpc.js");
5
+ function getActorHighestOrder(actor, actorRoles, scope, allRolePermissions) {
6
+ let highest = -1;
7
+ for (const role of actorRoles) {
8
+ const perms = permissions.decodePermissions(role.permissions);
9
+ const resolved = permissions.resolveAllScopes(perms, []);
10
+ if (resolved[scope]) {
11
+ highest = Math.max(highest, role.order);
12
+ }
13
+ }
14
+ if (highest < 0) {
15
+ const userPerms = actor.permissions ?? permissions.EMPTY_PERMISSIONS;
16
+ if (permissions.resolveScope(userPerms, allRolePermissions ?? [], scope))
17
+ return Infinity;
18
+ }
19
+ return highest;
20
+ }
21
+ function canManageRole(actorOrder, targetRole) {
22
+ return actorOrder > targetRole.order;
23
+ }
24
+ function actorHasAllPermissions(actorEffective, granting) {
25
+ const actorScopes = permissions.resolveAllScopes(actorEffective, []);
26
+ const grantingScopes = permissions.resolveAllScopes(granting, []);
27
+ for (const [scope, granted] of Object.entries(grantingScopes)) {
28
+ if (granted && !actorScopes[scope]) return false;
29
+ }
30
+ return true;
31
+ }
32
+ function checkPermissionEscalation(actor, actorRolePermissions, proposed) {
33
+ if (actor.isOwner) return null;
34
+ const effective = getActorEffectivePermissions(actor, actorRolePermissions);
35
+ const myScopes = permissions.resolveAllScopes(effective, []);
36
+ const grantedScopes = permissions.resolveAllScopes(proposed, []);
37
+ for (const [scope, granted] of Object.entries(grantedScopes)) {
38
+ if (granted && !myScopes[scope])
39
+ return `cannot grant permission you do not have: ${scope}`;
40
+ }
41
+ return null;
42
+ }
43
+ function checkPermissionRevocation(current, proposed) {
44
+ const currentScopes = permissions.resolveAllScopes(current, []);
45
+ const proposedScopes = permissions.resolveAllScopes(proposed, []);
46
+ for (const [scope, granted] of Object.entries(currentScopes)) {
47
+ if (granted && !proposedScopes[scope])
48
+ return `cannot revoke permission: ${scope}`;
49
+ }
50
+ return null;
51
+ }
52
+ function getGrantablePermissions(actorRoles, minOrder) {
53
+ const qualifying = minOrder !== void 0 ? actorRoles.filter((r) => r.order > minOrder) : actorRoles;
54
+ if (qualifying.length === 0) return permissions.EMPTY_PERMISSIONS;
55
+ return permissions.mergePermissionSets(
56
+ ...qualifying.map((r) => permissions.decodePermissions(r.permissions))
57
+ );
58
+ }
59
+ function getActorEffectivePermissions(user, rolePermissions) {
60
+ return permissions.mergePermissionSets(
61
+ user.permissions ?? permissions.EMPTY_PERMISSIONS,
62
+ ...rolePermissions
63
+ );
64
+ }
65
+ function validateHierarchy(actor, actorRoles, targetRole, requiredScope, allRolePermissions) {
66
+ if (actor.isOwner) return null;
67
+ const order = getActorHighestOrder(
68
+ actor,
69
+ actorRoles,
70
+ requiredScope,
71
+ allRolePermissions
72
+ );
73
+ if (order < 0) return `missing permission: ${requiredScope}`;
74
+ if (!canManageRole(order, targetRole))
75
+ return `cannot manage role "${targetRole.name}" (order ${targetRole.order}) from order ${order}`;
76
+ return null;
77
+ }
78
+ async function checkUserHierarchy(actor, target) {
79
+ if (actor.isOwner) return null;
80
+ if (target.isOwner) return "cannot modify the owner";
81
+ const { database } = trpc.getContextDeps();
82
+ const targetRoles = await database.getUserAssignedRoles(target);
83
+ if (targetRoles.length === 0) return null;
84
+ const targetMaxOrder = Math.max(...targetRoles.map((r) => r.order));
85
+ const actorRoles = await database.getUserAssignedRoles(actor);
86
+ const actorMaxOrder = Math.max(0, ...actorRoles.map((r) => r.order));
87
+ if (actorMaxOrder <= targetMaxOrder)
88
+ return "cannot modify a user with equal or higher role";
89
+ return null;
90
+ }
91
+ async function checkRoleHierarchy(user, role, scope) {
92
+ if (user.isOwner) return null;
93
+ const { database } = trpc.getContextDeps();
94
+ const actorRoles = await database.getUserAssignedRoles(user);
95
+ const rolePerms = await database.getUserRolePermissions(user);
96
+ return validateHierarchy(user, actorRoles, role, scope, rolePerms);
97
+ }
98
+ exports.actorHasAllPermissions = actorHasAllPermissions;
99
+ exports.canManageRole = canManageRole;
100
+ exports.checkPermissionEscalation = checkPermissionEscalation;
101
+ exports.checkPermissionRevocation = checkPermissionRevocation;
102
+ exports.checkRoleHierarchy = checkRoleHierarchy;
103
+ exports.checkUserHierarchy = checkUserHierarchy;
104
+ exports.getActorEffectivePermissions = getActorEffectivePermissions;
105
+ exports.getActorHighestOrder = getActorHighestOrder;
106
+ exports.getGrantablePermissions = getGrantablePermissions;
107
+ exports.validateHierarchy = validateHierarchy;
108
+ //# sourceMappingURL=roleHierarchy.js.map