omegga 1.6.0 → 1.7.0

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 (105) hide show
  1. package/.claude/settings.local.json +9 -2
  2. package/CHANGELOG.md +22 -1
  3. package/dist/cli/terminal.js +5 -1
  4. package/dist/cli/terminal.js.map +1 -1
  5. package/dist/db/connection.js +31 -0
  6. package/dist/db/connection.js.map +1 -0
  7. package/dist/db/migrate.js +20 -0
  8. package/dist/db/migrate.js.map +1 -0
  9. package/dist/db/migrations/0000_long_killer_shrike.sql +111 -0
  10. package/dist/db/migrations/meta/0000_snapshot.json +737 -0
  11. package/dist/db/migrations/meta/_journal.json +13 -0
  12. package/dist/db/nedbImport.js +344 -0
  13. package/dist/db/nedbImport.js.map +1 -0
  14. package/dist/db/plugin-migrations/0000_faulty_tyger_tiger.sql +11 -0
  15. package/dist/db/plugin-migrations/meta/0000_snapshot.json +81 -0
  16. package/dist/db/plugin-migrations/meta/_journal.json +13 -0
  17. package/dist/db/pluginSchema.js +19 -0
  18. package/dist/db/pluginSchema.js.map +1 -0
  19. package/dist/db/schema.js +174 -0
  20. package/dist/db/schema.js.map +1 -0
  21. package/dist/omegga/matchers/version.js +30 -13
  22. package/dist/omegga/matchers/version.js.map +1 -1
  23. package/dist/omegga/plugin.js +47 -49
  24. package/dist/omegga/plugin.js.map +1 -1
  25. package/dist/omegga/server.js.map +1 -1
  26. package/dist/softconfig.js +9 -21
  27. package/dist/softconfig.js.map +1 -1
  28. package/dist/updater/steam.js +1 -1
  29. package/dist/updater/steam.js.map +1 -1
  30. package/dist/util/brdb.js +84 -2
  31. package/dist/util/brdb.js.map +1 -1
  32. package/dist/webserver/backend/database.js +533 -533
  33. package/dist/webserver/backend/database.js.map +1 -1
  34. package/dist/webserver/backend/index.js +19 -8
  35. package/dist/webserver/backend/index.js.map +1 -1
  36. package/dist/webserver/backend/metrics.js +7 -5
  37. package/dist/webserver/backend/metrics.js.map +1 -1
  38. package/dist/webserver/backend/permissions.js +69 -20
  39. package/dist/webserver/backend/permissions.js.map +1 -1
  40. package/dist/webserver/backend/roleHierarchy.js +106 -0
  41. package/dist/webserver/backend/roleHierarchy.js.map +1 -0
  42. package/dist/webserver/backend/router/player.js +13 -16
  43. package/dist/webserver/backend/router/player.js.map +1 -1
  44. package/dist/webserver/backend/router/role.js +195 -7
  45. package/dist/webserver/backend/router/role.js.map +1 -1
  46. package/dist/webserver/backend/router/server.js +32 -2
  47. package/dist/webserver/backend/router/server.js.map +1 -1
  48. package/dist/webserver/backend/router/session.js +3 -5
  49. package/dist/webserver/backend/router/session.js.map +1 -1
  50. package/dist/webserver/backend/router/user.js +89 -42
  51. package/dist/webserver/backend/router/user.js.map +1 -1
  52. package/dist/webserver/backend/scopes.js +73 -44
  53. package/dist/webserver/backend/scopes.js.map +1 -1
  54. package/dist/webserver/backend/trpc.js +3 -6
  55. package/dist/webserver/backend/trpc.js.map +1 -1
  56. package/doc/permissions.md +83 -12
  57. package/drizzle.config.ts +7 -0
  58. package/drizzle.plugin.config.ts +7 -0
  59. package/package.json +7 -2
  60. package/public/app.html +415 -32
  61. package/public/assets/IconWorld-CmdphDgp.js +2 -0
  62. package/public/assets/IconWorld-CmdphDgp.js.map +1 -0
  63. package/public/assets/IconWorldPlus-DKSZjbqh.js +2 -0
  64. package/public/assets/IconWorldPlus-DKSZjbqh.js.map +1 -0
  65. package/public/assets/app-BiBsxujC.js +7 -0
  66. package/public/assets/app-BiBsxujC.js.map +1 -0
  67. package/public/assets/{auth-CEYiizOt.js → auth-nn5rDnJ9.js} +2 -2
  68. package/public/assets/{auth-CEYiizOt.js.map → auth-nn5rDnJ9.js.map} +1 -1
  69. package/public/assets/favicon-DYc6c67x.ico +0 -0
  70. package/public/assets/{index-DEL-PD_8.js → index-2cgXLhrt.js} +2 -2
  71. package/public/assets/{index-DEL-PD_8.js.map → index-2cgXLhrt.js.map} +1 -1
  72. package/public/assets/index-5EkjXNMi.js +12 -0
  73. package/public/assets/index-5EkjXNMi.js.map +1 -0
  74. package/public/assets/index-B2DCW1zi.js +2 -0
  75. package/public/assets/index-B2DCW1zi.js.map +1 -0
  76. package/public/assets/logo-Dl-LlTow.webp +0 -0
  77. package/public/assets/shared/@components-VkA0DIz6.js +2 -0
  78. package/public/assets/shared/@components-VkA0DIz6.js.map +1 -0
  79. package/public/assets/shared/@hooks-CiJaUL6_.js +2 -0
  80. package/public/assets/shared/@hooks-CiJaUL6_.js.map +1 -0
  81. package/public/assets/shared/@tabler_icons-react-B4130bhe.js +3 -0
  82. package/public/assets/shared/{@tabler_icons-react-DrVFX3uL.js.map → @tabler_icons-react-B4130bhe.js.map} +1 -1
  83. package/public/assets/shared/{wouter-CNISJ5i6.js → wouter-CUC1caxu.js} +2 -2
  84. package/public/assets/shared/wouter-CUC1caxu.js.map +1 -0
  85. package/public/assets/user-XF1UyJMA.js +2 -0
  86. package/public/assets/user-XF1UyJMA.js.map +1 -0
  87. package/public/auth.html +163 -16
  88. package/public/assets/IconWorld-XmQNg6JI.js +0 -2
  89. package/public/assets/IconWorld-XmQNg6JI.js.map +0 -1
  90. package/public/assets/IconWorldPlus-CxGGv2eD.js +0 -2
  91. package/public/assets/IconWorldPlus-CxGGv2eD.js.map +0 -1
  92. package/public/assets/app-DMr_-wik.js +0 -7
  93. package/public/assets/app-DMr_-wik.js.map +0 -1
  94. package/public/assets/index-D_STNela.js +0 -2
  95. package/public/assets/index-D_STNela.js.map +0 -1
  96. package/public/assets/index-Dkh3HiBz.js +0 -4
  97. package/public/assets/index-Dkh3HiBz.js.map +0 -1
  98. package/public/assets/shared/@components-CgsSYMe6.js +0 -2
  99. package/public/assets/shared/@components-CgsSYMe6.js.map +0 -1
  100. package/public/assets/shared/@hooks-DT93N7wd.js +0 -2
  101. package/public/assets/shared/@hooks-DT93N7wd.js.map +0 -1
  102. package/public/assets/shared/@tabler_icons-react-DrVFX3uL.js +0 -3
  103. package/public/assets/shared/wouter-CNISJ5i6.js.map +0 -1
  104. package/public/assets/useHasScope-BOdFktko.js +0 -2
  105. package/public/assets/useHasScope-BOdFktko.js.map +0 -1
@@ -1,176 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const softconfig = require("../../softconfig.js");
3
+ const schema = require("../../db/schema.js");
4
4
  const pattern = require("../../util/pattern.js");
5
5
  const time = require("../../util/time.js");
6
6
  const bcrypt = require("bcryptjs");
7
7
  const chokidar = require("chokidar");
8
+ const drizzleOrm = require("drizzle-orm");
8
9
  const EventEmitter = require("events");
9
- const Datastore = require("nedb-promises");
10
+ const crypto = require("node:crypto");
10
11
  const path = require("path");
11
12
  const calendar = require("./calendar.js");
12
13
  const events = require("./events.js");
13
14
  const permissions = require("./permissions.js");
14
- let serverInstance;
15
15
  const createPunchcard = () => Array.from({ length: 7 }).map(
16
16
  () => Array.from({ length: 24 }).fill(0)
17
17
  );
18
18
  class Database extends EventEmitter {
19
- constructor(options, omegga) {
19
+ constructor(options, omegga, sqlite, db) {
20
20
  super();
21
21
  this.defaultPermissionsCache = null;
22
+ this.rolesCache = null;
23
+ this.serverInstance = null;
22
24
  this.options = options;
23
25
  this.omegga = omegga;
26
+ this.sqlite = sqlite;
27
+ this.db = db;
24
28
  this.calendar = new calendar.default();
25
- const dbOpts = {
26
- autoload: true,
27
- // case insensitive string comparison
28
- compareStrings: (a, b) => a.localeCompare(b, "en", { ignorePunctuation: true })
29
- };
30
- this.stores = {
31
- users: Datastore.create({
32
- filename: path.join(omegga.dataPath, softconfig.default.USER_STORE),
33
- ...dbOpts
34
- }),
35
- chat: Datastore.create({
36
- filename: path.join(omegga.dataPath, softconfig.default.CHAT_STORE),
37
- ...dbOpts
38
- }),
39
- players: Datastore.create({
40
- filename: path.join(omegga.dataPath, softconfig.default.PLAYER_STORE),
41
- ...dbOpts
42
- }),
43
- status: Datastore.create({
44
- filename: path.join(omegga.dataPath, softconfig.default.STATUS_STORE),
45
- ...dbOpts
46
- }),
47
- server: Datastore.create({
48
- filename: path.join(omegga.dataPath, softconfig.default.SERVER_STORE),
49
- ...dbOpts
50
- })
51
- };
52
- }
53
- // make sure all the databases have valid store versions
54
- async doMigrations() {
55
- const storeVersions = {
56
- users: 2,
57
- chat: 1,
58
- players: 1,
59
- status: 1,
60
- server: 2,
61
- // example version
62
- dummy: 10
63
- };
64
- const migrations = {
65
- users: [
66
- {
67
- version: 1,
68
- upgrade: async (store) => {
69
- const users = await store.find({
70
- type: "user"
71
- });
72
- for (const user of users) {
73
- const p = user.permissions ?? permissions.EMPTY_PERMISSIONS;
74
- const root = p.root === "all" || p.root === "read" ? p.root : "off";
75
- const domains = {};
76
- for (const [k, v] of Object.entries(p.domains ?? {})) {
77
- if (v !== "unset" && v !== "none") domains[k] = v;
78
- }
79
- const scopes = {};
80
- for (const [k, v] of Object.entries(p.scopes ?? {})) {
81
- if (v === "enabled" || v === true) scopes[k] = true;
82
- else if (v === "disabled" || v === false) scopes[k] = false;
83
- }
84
- const encoded = permissions.encodePermissions({ root, domains, scopes });
85
- await store.update(
86
- { _id: user._id },
87
- { $set: { permissions: encoded } }
88
- );
89
- }
90
- }
91
- }
92
- ],
93
- chat: [],
94
- players: [],
95
- status: [],
96
- server: [
97
- {
98
- version: 1,
99
- upgrade: async (store) => {
100
- const doc = await store.findOne({
101
- type: "defaultPermissions"
102
- });
103
- if (!doc) return;
104
- const root = doc.root === "unset" ? "off" : doc.root;
105
- const domains = {};
106
- for (const [k, v] of Object.entries(doc.domains ?? {})) {
107
- if (v !== "unset") domains[k] = v;
108
- }
109
- const scopes = {};
110
- for (const [k, v] of Object.entries(doc.scopes ?? {})) {
111
- if (v === "enabled") scopes[k] = true;
112
- else if (v === "disabled") scopes[k] = false;
113
- }
114
- const encoded = permissions.encodePermissions({
115
- root,
116
- domains,
117
- scopes
118
- });
119
- await store.update(
120
- { _id: doc._id },
121
- {
122
- $set: {
123
- root: encoded.root,
124
- domains: encoded.domains,
125
- scopes: encoded.scopes
126
- }
127
- }
128
- );
129
- }
130
- }
131
- ],
132
- // example migration list (all version upgrades go up by 1)
133
- dummy: [
134
- {
135
- version: 1,
136
- // target version
137
- upgrade: async (_store) => {
138
- }
139
- // upgrade function (does not include updating version)
140
- }
141
- ]
142
- };
143
- await Promise.all(
144
- Object.entries(this.stores).map(async ([name, store]) => {
145
- const versionEntry = await store.findOne({
146
- type: "storeVersion"
147
- });
148
- const expected = storeVersions[name];
149
- if (!versionEntry) {
150
- store.insert({ type: "storeVersion", version: expected });
151
- } else if (versionEntry.version < expected) {
152
- let { version } = versionEntry;
153
- for (; version < expected; version++) {
154
- const migration = migrations[name].find((v) => v.version === version);
155
- if (!migration) break;
156
- migration.upgrade(store);
157
- }
158
- await store.update({ _id: versionEntry._id }, { version });
159
- }
160
- })
161
- );
162
29
  }
163
30
  // get the running instance id of this omegga
164
31
  async getInstanceId() {
165
- if (serverInstance) return serverInstance._id;
166
- const doc = await this.stores.server.insert({
167
- type: "app:start",
168
- date: Date.now()
169
- });
170
- serverInstance = doc;
171
- const dates = (await this.stores.chat.find({ type: "chat" })).map((c) => c.created);
32
+ if (this.serverInstance) return this.serverInstance.id;
33
+ const id = crypto.randomUUID();
34
+ this.db.insert(schema.serverInstances).values({ id, date: Date.now() }).run();
35
+ this.serverInstance = { id, date: Date.now() };
36
+ const dates = this.db.select({ created: schema.chatLogs.created }).from(schema.chatLogs).all();
172
37
  for (const d of dates) {
173
- this.calendar.addDate(d);
38
+ this.calendar.addDate(d.created);
174
39
  }
175
40
  const watcher = chokidar.watch(
176
41
  path.join(this.omegga.configPath, "BanList.json"),
@@ -190,18 +55,12 @@ class Database extends EventEmitter {
190
55
  const now = Date.now();
191
56
  if (now > eventExpire) return;
192
57
  if (leavingPlayer.id === kicked.id) {
193
- const entry = {
194
- type: "kickHistory",
58
+ this.upsertKickHistory({
195
59
  kicked: kicked.id,
196
60
  kickerId: kicker.id,
197
61
  created: now,
198
62
  reason
199
- };
200
- await this.stores.players.update(
201
- entry,
202
- { $set: entry },
203
- { upsert: true }
204
- );
63
+ });
205
64
  }
206
65
  });
207
66
  };
@@ -211,27 +70,29 @@ class Database extends EventEmitter {
211
70
  if (!kickedPlayer || !kickerPlayer) return;
212
71
  handleKick(kickedPlayer, kickerPlayer, reason);
213
72
  });
214
- return doc._id;
73
+ return id;
215
74
  }
216
75
  // synchronize the ban list with the one in the database
217
76
  syncBanList() {
218
77
  const banList = this.omegga.getBanList()?.banList;
219
78
  if (!banList) return;
220
- for (const banned in banList) {
221
- const entry = {
222
- type: "banHistory",
223
- banned,
224
- bannerId: banList[banned].bannerId,
225
- created: time.parseBrickadiaTime(banList[banned].created),
226
- expires: time.parseBrickadiaTime(banList[banned].expires),
227
- reason: banList[banned].reason
228
- };
229
- this.stores.players.update(entry, { $set: entry }, { upsert: true });
230
- }
79
+ const tx = this.sqlite.transaction(() => {
80
+ for (const banned in banList) {
81
+ this.upsertBanHistory({
82
+ banned,
83
+ bannerId: banList[banned].bannerId ?? "",
84
+ created: time.parseBrickadiaTime(banList[banned].created) ?? 0,
85
+ expires: time.parseBrickadiaTime(banList[banned].expires) ?? 0,
86
+ reason: banList[banned].reason
87
+ });
88
+ }
89
+ });
90
+ tx();
231
91
  }
232
92
  // determine if this user would be the first user (admin user)
233
93
  async isFirstUser() {
234
- return await this.stores.users.count({ type: "user" }) === 0;
94
+ const result = this.db.select({ count: drizzleOrm.count() }).from(schema.users).get();
95
+ return (result?.count ?? 0) === 0;
235
96
  }
236
97
  // hash a password
237
98
  async hash(password) {
@@ -239,481 +100,590 @@ class Database extends EventEmitter {
239
100
  }
240
101
  // create an admin user account, username can be blank (no password)
241
102
  async createAdminUser(username, password) {
242
- const hash = await this.hash(password);
243
- const user = this.stores.users.insert({
244
- // this is a user
245
- type: "user",
103
+ const h = await this.hash(password);
104
+ const id = crypto.randomUUID();
105
+ this.db.insert(schema.users).values({
106
+ id,
246
107
  created: Date.now(),
247
108
  lastOnline: Date.now(),
248
- // credentials
249
109
  username,
250
- hash,
251
- // permissions
110
+ hash: h,
252
111
  isOwner: true,
253
112
  roles: [],
254
113
  permissions: permissions.EMPTY_PERMISSIONS,
255
- // mfa
256
114
  totpEnabled: false,
257
115
  passkeys: [],
258
116
  recoveryCodes: [],
259
- // brickadia player uuid
260
117
  playerId: ""
261
- });
262
- return user;
118
+ }).run();
119
+ return this.findUserById(id);
263
120
  }
264
121
  // determine if a user with this name exists
265
122
  async userExists(username) {
266
- return await this.stores.users.count({ type: "user", username }) > 0;
123
+ const result = this.db.select({ count: drizzleOrm.count() }).from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
124
+ return (result?.count ?? 0) > 0;
267
125
  }
268
- // create a regular user account
126
+ // create a regular user account
269
127
  async createUser(username, password) {
270
128
  if (await this.userExists(username)) throw new Error("user already exists");
271
- const hash = await this.hash(password);
272
- const user = this.stores.users.insert({
273
- // this is a user
274
- type: "user",
129
+ const h = await this.hash(password);
130
+ const id = crypto.randomUUID();
131
+ this.db.insert(schema.users).values({
132
+ id,
275
133
  created: Date.now(),
276
134
  lastOnline: 0,
277
- // credentials
278
135
  username,
279
- hash,
280
- // permissions
136
+ hash: h,
281
137
  isOwner: false,
282
138
  roles: [],
283
139
  permissions: permissions.EMPTY_PERMISSIONS,
284
- // mfa
285
140
  totpEnabled: false,
286
141
  passkeys: [],
287
142
  recoveryCodes: [],
288
- // brickadia player uuid
289
143
  playerId: ""
290
- });
291
- return user;
144
+ }).run();
145
+ return this.findUserById(id);
292
146
  }
293
147
  // set a user's password
294
148
  async userPasswd(username, password) {
295
- const hash = await this.hash(password);
149
+ const h = await this.hash(password);
296
150
  if (!await this.userExists(username))
297
151
  throw new Error("user does not exist");
298
- await this.stores.users.update(
299
- { type: "user", username },
300
- { $set: { hash } }
301
- );
152
+ this.db.update(schema.users).set({ hash: h }).where(drizzleOrm.eq(schema.users.username, username)).run();
302
153
  }
303
154
  async banUser(username, banned) {
304
- const result = await this.stores.users.update(
305
- { type: "user", username },
306
- { $set: { isBanned: banned } }
307
- );
155
+ this.db.update(schema.users).set({ isBanned: banned }).where(drizzleOrm.eq(schema.users.username, username)).run();
308
156
  if (banned) events.serverEvents.emit("userInvalidated", username);
309
- return result;
157
+ return 1;
310
158
  }
311
159
  async deleteUser(username) {
312
- const result = await this.stores.users.remove(
313
- { type: "user", username },
314
- {}
315
- );
160
+ this.db.delete(schema.users).where(drizzleOrm.eq(schema.users.username, username)).run();
316
161
  events.serverEvents.emit("userInvalidated", username);
317
- return result;
162
+ return 1;
318
163
  }
319
- async setUserPermissions(username, permissions$1) {
320
- return await this.stores.users.update(
321
- { type: "user", username },
322
- { $set: { permissions: permissions.encodePermissions(permissions$1) } }
323
- );
164
+ async setUserPermissions(username, permissions2) {
165
+ this.db.update(schema.users).set({ permissions: permissions2 }).where(drizzleOrm.eq(schema.users.username, username)).run();
324
166
  }
325
167
  async getDefaultPermissions() {
326
168
  if (this.defaultPermissionsCache) return this.defaultPermissionsCache;
327
- let doc = await this.stores.server.findOne({
328
- type: "defaultPermissions"
329
- });
330
- if (!doc) {
331
- doc = await this.stores.server.insert({
169
+ const row = this.db.select().from(schema.serverConfig).where(drizzleOrm.eq(schema.serverConfig.key, "defaultPermissions")).get();
170
+ if (!row) {
171
+ const defaults = {
332
172
  type: "defaultPermissions",
333
173
  root: permissions.RootLevel.Read,
334
174
  domains: {},
335
175
  scopes: {}
336
- });
176
+ };
177
+ this.db.insert(schema.serverConfig).values({ key: "defaultPermissions", value: defaults }).run();
178
+ this.defaultPermissionsCache = defaults;
179
+ return defaults;
337
180
  }
338
- const decoded = permissions.decodePermissions(doc);
339
- const result = { ...doc, ...decoded };
181
+ const val = row.value;
182
+ const result = {
183
+ type: "defaultPermissions",
184
+ root: val.root ?? permissions.RootLevel.Off,
185
+ domains: val.domains ?? {},
186
+ scopes: val.scopes ?? {}
187
+ };
340
188
  this.defaultPermissionsCache = result;
341
189
  return result;
342
190
  }
343
- async setDefaultPermissions(permissions$1) {
344
- const encoded = permissions.encodePermissions(permissions$1);
345
- await this.stores.server.update(
346
- { type: "defaultPermissions" },
347
- {
348
- $set: {
349
- root: encoded.root,
350
- domains: encoded.domains,
351
- scopes: encoded.scopes
352
- }
353
- },
354
- { upsert: true }
355
- );
191
+ async setDefaultPermissions(permissions2) {
192
+ const value = {
193
+ root: permissions2.root,
194
+ domains: permissions2.domains,
195
+ scopes: permissions2.scopes
196
+ };
197
+ this.db.insert(schema.serverConfig).values({ key: "defaultPermissions", value }).onConflictDoUpdate({
198
+ target: schema.serverConfig.key,
199
+ set: { value }
200
+ }).run();
356
201
  this.defaultPermissionsCache = null;
357
202
  }
358
203
  // get a user from credentials
359
204
  async authUser(username, password) {
360
- const user = await this.stores.users.findOne({ username });
361
- if (!user || user.isBanned) return null;
362
- if (await bcrypt.compare(password, user.hash)) {
363
- await this.stores.users.update(
364
- { _id: user._id },
365
- { $set: { lastOnline: Date.now() } }
366
- );
367
- return user;
205
+ const row = this.db.select().from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
206
+ if (!row || row.isBanned) return null;
207
+ if (await bcrypt.compare(password, row.hash)) {
208
+ this.db.update(schema.users).set({ lastOnline: Date.now() }).where(drizzleOrm.eq(schema.users.id, row.id)).run();
209
+ return this._toStoreUser(row);
368
210
  }
369
211
  return null;
370
212
  }
371
213
  // find a user by object id
372
214
  async findUserById(id) {
373
- const user = await this.stores.users.findOne({
374
- $or: [
215
+ const row = this.db.select().from(schema.users).where(
216
+ drizzleOrm.or(
375
217
  // the owner has no username, so everyone is the owner
376
- { type: "user", username: "", isOwner: true },
218
+ drizzleOrm.and(drizzleOrm.eq(schema.users.username, ""), drizzleOrm.eq(schema.users.isOwner, true)),
377
219
  // the user exists and has an id
378
- { type: "user", _id: id }
379
- ]
380
- });
381
- if (user) {
382
- user.permissions = permissions.decodePermissions(user.permissions);
383
- if (!user.totpEnabled) user.totpEnabled = false;
384
- if (!user.passkeys) user.passkeys = [];
220
+ drizzleOrm.eq(schema.users.id, id)
221
+ )
222
+ ).get();
223
+ if (!row) return null;
224
+ return this._toStoreUser(row);
225
+ }
226
+ async findUserByUsername(username) {
227
+ const row = this.db.select().from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
228
+ if (!row) return null;
229
+ return this._toStoreUser(row);
230
+ }
231
+ async findUserByPasskeyId(credentialId) {
232
+ const rows = this.db.select().from(schema.users).where(
233
+ drizzleOrm.sql`${schema.users.passkeys} LIKE ${'%"id":"' + credentialId + '"%'}`
234
+ ).all();
235
+ for (const row of rows) {
236
+ const passkeys = row.passkeys ?? [];
237
+ if (passkeys.some((p) => p.id === credentialId)) {
238
+ return this._toStoreUser(row);
239
+ }
385
240
  }
386
- return user;
241
+ return null;
242
+ }
243
+ updateUserLastOnline(id) {
244
+ this.db.update(schema.users).set({ lastOnline: Date.now() }).where(drizzleOrm.eq(schema.users.id, id)).run();
245
+ }
246
+ updateOwnerCredentials(id, username, hash) {
247
+ this.db.update(schema.users).set({ username, hash }).where(drizzleOrm.eq(schema.users.id, id)).run();
248
+ }
249
+ addUserRole(userId, roleId) {
250
+ const row = this.db.select({ roles: schema.users.roles }).from(schema.users).where(drizzleOrm.eq(schema.users.id, userId)).get();
251
+ if (!row) return;
252
+ const roles = row.roles ?? [];
253
+ if (!roles.includes(roleId)) {
254
+ roles.push(roleId);
255
+ this.db.update(schema.users).set({ roles }).where(drizzleOrm.eq(schema.users.id, userId)).run();
256
+ }
257
+ }
258
+ removeUserRole(userId, roleId) {
259
+ const row = this.db.select({ roles: schema.users.roles }).from(schema.users).where(drizzleOrm.eq(schema.users.id, userId)).get();
260
+ if (!row) return;
261
+ const roles = (row.roles ?? []).filter((r) => r !== roleId);
262
+ this.db.update(schema.users).set({ roles }).where(drizzleOrm.eq(schema.users.id, userId)).run();
387
263
  }
388
264
  async setUserTotp(username, secret, enabled) {
389
- await this.stores.users.update(
390
- { type: "user", username },
391
- { $set: { totpSecret: secret, totpEnabled: enabled } }
392
- );
265
+ this.db.update(schema.users).set({ totpSecret: secret, totpEnabled: enabled }).where(drizzleOrm.eq(schema.users.username, username)).run();
393
266
  }
394
267
  async disableUserTotp(username) {
395
- await this.stores.users.update(
396
- { type: "user", username },
397
- { $set: { totpEnabled: false }, $unset: { totpSecret: true } }
398
- );
268
+ this.db.update(schema.users).set({ totpEnabled: false, totpSecret: null }).where(drizzleOrm.eq(schema.users.username, username)).run();
399
269
  }
400
270
  async addPasskey(username, credential) {
401
- await this.stores.users.update(
402
- { type: "user", username },
403
- { $push: { passkeys: credential } }
404
- );
271
+ const row = this.db.select({ id: schema.users.id, passkeys: schema.users.passkeys }).from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
272
+ if (!row) return;
273
+ const passkeys = [
274
+ ...row.passkeys ?? [],
275
+ credential
276
+ ];
277
+ this.db.update(schema.users).set({ passkeys }).where(drizzleOrm.eq(schema.users.id, row.id)).run();
405
278
  }
406
279
  async removePasskey(username, credentialId) {
407
- const user = await this.stores.users.findOne({
408
- type: "user",
409
- username
410
- });
411
- if (!user) return;
412
- const passkeys = (user.passkeys ?? []).filter((p) => p.id !== credentialId);
413
- await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });
280
+ const row = this.db.select({ id: schema.users.id, passkeys: schema.users.passkeys }).from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
281
+ if (!row) return;
282
+ const passkeys = (row.passkeys ?? []).filter(
283
+ (p) => p.id !== credentialId
284
+ );
285
+ this.db.update(schema.users).set({ passkeys }).where(drizzleOrm.eq(schema.users.id, row.id)).run();
414
286
  }
415
287
  async updatePasskeyCounter(username, credentialId, counter) {
416
- const user = await this.stores.users.findOne({
417
- type: "user",
418
- username
419
- });
420
- if (!user) return;
421
- const passkeys = (user.passkeys ?? []).map(
288
+ const row = this.db.select({ id: schema.users.id, passkeys: schema.users.passkeys }).from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
289
+ if (!row) return;
290
+ const passkeys = (row.passkeys ?? []).map(
422
291
  (p) => p.id === credentialId ? { ...p, counter, lastUsed: Date.now() } : p
423
292
  );
424
- await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });
293
+ this.db.update(schema.users).set({ passkeys }).where(drizzleOrm.eq(schema.users.id, row.id)).run();
425
294
  }
426
295
  async setRecoveryCodes(username, hashedCodes) {
427
- await this.stores.users.update(
428
- { type: "user", username },
429
- { $set: { recoveryCodes: hashedCodes } }
430
- );
296
+ this.db.update(schema.users).set({ recoveryCodes: hashedCodes }).where(drizzleOrm.eq(schema.users.username, username)).run();
431
297
  }
432
298
  async removeRecoveryCode(username, hashedCode) {
433
- const user = await this.stores.users.findOne({
434
- type: "user",
435
- username
436
- });
437
- if (!user) return;
438
- const codes = (user.recoveryCodes ?? []).filter((c) => c !== hashedCode);
439
- await this.stores.users.update(
440
- { _id: user._id },
441
- { $set: { recoveryCodes: codes } }
299
+ const row = this.db.select({
300
+ id: schema.users.id,
301
+ recoveryCodes: schema.users.recoveryCodes
302
+ }).from(schema.users).where(drizzleOrm.eq(schema.users.username, username)).get();
303
+ if (!row) return;
304
+ const codes = (row.recoveryCodes ?? []).filter(
305
+ (c) => c !== hashedCode
442
306
  );
307
+ this.db.update(schema.users).set({ recoveryCodes: codes }).where(drizzleOrm.eq(schema.users.id, row.id)).run();
443
308
  }
444
309
  async resetUserMfa(username) {
445
- await this.stores.users.update(
446
- { type: "user", username },
447
- {
448
- $set: {
449
- totpEnabled: false,
450
- passkeys: [],
451
- recoveryCodes: []
452
- },
453
- $unset: { totpSecret: true }
454
- }
455
- );
310
+ this.db.update(schema.users).set({
311
+ totpEnabled: false,
312
+ totpSecret: null,
313
+ passkeys: [],
314
+ recoveryCodes: []
315
+ }).where(drizzleOrm.eq(schema.users.username, username)).run();
316
+ }
317
+ async getAllRoles() {
318
+ if (this.rolesCache) return this.rolesCache;
319
+ const rows = this.db.select().from(schema.webRoles).orderBy(drizzleOrm.desc(schema.webRoles.order)).all();
320
+ const roles = rows.map((r) => ({
321
+ type: "webRole",
322
+ id: r.id,
323
+ name: r.name,
324
+ description: r.description,
325
+ order: r.order,
326
+ permissions: r.permissions
327
+ }));
328
+ this.rolesCache = roles;
329
+ return roles;
330
+ }
331
+ invalidateRolesCache() {
332
+ this.rolesCache = null;
333
+ this.defaultPermissionsCache = null;
456
334
  }
457
- async findUserByPasskeyId(credentialId) {
458
- return await this.stores.users.findOne({
459
- type: "user",
460
- "passkeys.id": credentialId
335
+ async getRole(id) {
336
+ const row = this.db.select().from(schema.webRoles).where(drizzleOrm.eq(schema.webRoles.id, id)).get();
337
+ if (!row) return null;
338
+ return {
339
+ type: "webRole",
340
+ id: row.id,
341
+ name: row.name,
342
+ description: row.description,
343
+ order: row.order,
344
+ permissions: row.permissions
345
+ };
346
+ }
347
+ async createRole(name, description, permissions2) {
348
+ const id = crypto.randomUUID();
349
+ const tx = this.sqlite.transaction(() => {
350
+ this.db.update(schema.webRoles).set({ order: drizzleOrm.sql`${schema.webRoles.order} + 1` }).run();
351
+ this.db.insert(schema.webRoles).values({
352
+ id,
353
+ name,
354
+ description,
355
+ order: 1,
356
+ permissions: permissions2
357
+ }).run();
358
+ });
359
+ tx();
360
+ this.invalidateRolesCache();
361
+ return {
362
+ type: "webRole",
363
+ id,
364
+ name,
365
+ description,
366
+ order: 1,
367
+ permissions: permissions2
368
+ };
369
+ }
370
+ async updateRole(id, updates) {
371
+ const $set = {};
372
+ if (updates.name !== void 0) $set.name = updates.name;
373
+ if (updates.description !== void 0)
374
+ $set.description = updates.description;
375
+ if (updates.order !== void 0) $set.order = updates.order;
376
+ if (updates.permissions !== void 0)
377
+ $set.permissions = updates.permissions;
378
+ if (Object.keys($set).length > 0) {
379
+ this.db.update(schema.webRoles).set($set).where(drizzleOrm.eq(schema.webRoles.id, id)).run();
380
+ }
381
+ this.invalidateRolesCache();
382
+ }
383
+ async deleteRole(id) {
384
+ const role = await this.getRole(id);
385
+ if (!role) return "role not found";
386
+ const tx = this.sqlite.transaction(() => {
387
+ this.db.delete(schema.webRoles).where(drizzleOrm.eq(schema.webRoles.id, id)).run();
388
+ const usersWithRole = this.db.select({ id: schema.users.id, roles: schema.users.roles }).from(schema.users).all();
389
+ for (const u of usersWithRole) {
390
+ const roles = (u.roles ?? []).filter((r) => r !== id);
391
+ if (roles.length !== (u.roles ?? []).length) {
392
+ this.db.update(schema.users).set({ roles }).where(drizzleOrm.eq(schema.users.id, u.id)).run();
393
+ }
394
+ }
395
+ });
396
+ tx();
397
+ this.invalidateRolesCache();
398
+ return "";
399
+ }
400
+ async reorderRoles(entries) {
401
+ const tx = this.sqlite.transaction(() => {
402
+ for (const { id, order } of entries) {
403
+ this.db.update(schema.webRoles).set({ order }).where(drizzleOrm.eq(schema.webRoles.id, id)).run();
404
+ }
461
405
  });
406
+ tx();
407
+ this.invalidateRolesCache();
408
+ }
409
+ async getUserRolePermissions(user) {
410
+ const defaults = await this.getDefaultPermissions();
411
+ const defaultPerms = {
412
+ root: defaults.root,
413
+ domains: defaults.domains,
414
+ scopes: defaults.scopes
415
+ };
416
+ const allRoles = await this.getAllRoles();
417
+ const userRoles = allRoles.filter((r) => user.roles.includes(r.id));
418
+ return [defaultPerms, ...userRoles.map((r) => r.permissions)];
462
419
  }
463
- // get a list of roles
464
- // TODO: actually implement roles
465
- getRoles() {
466
- return this.stores.server.find({ type: "role" });
420
+ async getUserAssignedRoles(user) {
421
+ const allRoles = await this.getAllRoles();
422
+ return allRoles.filter((r) => user.roles.includes(r.id));
467
423
  }
468
424
  // add a chat message to the chat log store
469
425
  async addChatLog(action, user, message) {
470
426
  this.calendar.addDate(Date.now());
471
- return await this.stores.chat.insert({
472
- type: "chat",
427
+ const row = this.db.insert(schema.chatLogs).values({
473
428
  created: Date.now(),
474
429
  instanceId: await this.getInstanceId(),
475
430
  action,
476
431
  user,
477
432
  ...message ? { message } : {}
478
- });
433
+ }).returning().get();
434
+ return {
435
+ type: "chat",
436
+ _id: String(row.id),
437
+ id: row.id,
438
+ created: row.created,
439
+ instanceId: row.instanceId,
440
+ action: row.action,
441
+ user: row.user,
442
+ message: row.message
443
+ };
479
444
  }
480
445
  // get recent chat activity
481
446
  async getChats({
482
- count = 50,
447
+ count: limit = 50,
483
448
  sameServer,
484
449
  before,
485
450
  after
486
451
  } = {}) {
487
- return await this.stores.chat.find({
452
+ const conditions = [];
453
+ if (sameServer) {
454
+ conditions.push(
455
+ drizzleOrm.eq(schema.chatLogs.instanceId, await this.getInstanceId())
456
+ );
457
+ }
458
+ if (before) {
459
+ conditions.push(drizzleOrm.lt(schema.chatLogs.created, before));
460
+ } else if (after) {
461
+ conditions.push(drizzleOrm.gt(schema.chatLogs.created, after));
462
+ } else {
463
+ conditions.push(drizzleOrm.lt(schema.chatLogs.created, Date.now()));
464
+ }
465
+ const orderDir = !before && after ? drizzleOrm.asc : drizzleOrm.desc;
466
+ const rows = this.db.select().from(schema.chatLogs).where(conditions.length > 1 ? drizzleOrm.and(...conditions) : conditions[0]).orderBy(orderDir(schema.chatLogs.created)).limit(limit).all();
467
+ return rows.map((r) => ({
488
468
  type: "chat",
489
- ...sameServer ? { instanceId: await this.getInstanceId() } : {},
490
- created: before ? { $lt: before } : after ? { $gt: after } : { $lt: Date.now() }
491
- }).sort({ created: !before && after ? 1 : -1 }).limit(count).exec();
469
+ _id: String(r.id),
470
+ created: r.created,
471
+ instanceId: r.instanceId,
472
+ action: r.action,
473
+ user: r.user,
474
+ message: r.message
475
+ }));
492
476
  }
493
477
  // get paginated players
494
478
  async getPlayers({
495
- count = 50,
479
+ count: limit = 50,
496
480
  search = "",
497
481
  page = 0,
498
482
  sort = "name",
499
483
  direction = 1,
500
484
  limitId = void 0
501
485
  } = {}) {
502
- const pattern$1 = pattern.explode(search);
503
- const query = {
504
- type: "userHistory",
505
- // only filter if there's a query
506
- $and: [
507
- // if a limitId is passed in, the id must be in it
508
- ...limitId ? [{ id: { $in: limitId } }] : [],
509
- // if there's a search, the search matches...
510
- ...search.length > 0 ? [
511
- {
512
- $or: [
513
- // id was pasted in
514
- { id: search },
515
- // user's current name
516
- { name: { $regex: pattern$1 } },
517
- { displayName: { $regex: pattern$1 } },
518
- // user's past name
519
- {
520
- nameHistory: { $elemMatch: { name: { $regex: pattern$1 } } }
521
- },
522
- {
523
- nameHistory: {
524
- $elemMatch: { displayName: { $regex: pattern$1 } }
525
- }
526
- }
527
- ]
528
- }
529
- ] : []
530
- ]
531
- };
532
- const exactQuery = {
533
- type: "userHistory",
534
- // only filter if there's a query
535
- $or: [
536
- // id was pasted in
537
- { id: search },
538
- // user's current name
539
- { name: search },
540
- // user's current display name
541
- { displayName: search }
542
- ]
543
- };
544
- const [total, players, exactResult] = await Promise.all([
545
- this.stores.players.count(query),
546
- this.stores.players.find(query).sort({ [sort]: direction }).skip(count * page).limit(count).exec(),
547
- // exact match detection
548
- search.length > 0 && page === 0 ? this.stores.players.find(exactQuery) : []
549
- ]);
550
- if (exactResult.length > 0) {
551
- for (const res of exactResult) {
552
- const index = players.findIndex((p) => p._id === res._id);
553
- players.splice(index, 1);
486
+ const pattern$1 = search.length > 0 ? pattern.explode(search) : null;
487
+ const patternSource = pattern$1?.source;
488
+ if (limitId && limitId.length === 0)
489
+ return { pages: 0, total: 0, players: [] };
490
+ const conditions = [];
491
+ if (limitId) {
492
+ conditions.push(drizzleOrm.inArray(schema.playerHistory.id, limitId));
493
+ }
494
+ if (patternSource) {
495
+ conditions.push(drizzleOrm.sql`(
496
+ ${schema.playerHistory.id} = ${search}
497
+ OR ${schema.playerHistory.name} REGEXP ${patternSource}
498
+ OR ${schema.playerHistory.displayName} REGEXP ${patternSource}
499
+ OR EXISTS (
500
+ SELECT 1 FROM json_each(${schema.playerHistory.nameHistory})
501
+ WHERE json_each.type = 'object'
502
+ AND (json_extract(json_each.value, '$.name') REGEXP ${patternSource}
503
+ OR json_extract(json_each.value, '$.displayName') REGEXP ${patternSource})
504
+ )
505
+ )`);
506
+ }
507
+ const baseWhere = conditions.length > 0 ? drizzleOrm.and(...conditions) : void 0;
508
+ const sortCol = sort === "lastSeen" ? schema.playerHistory.lastSeen : sort === "created" ? schema.playerHistory.created : sort === "heartbeats" ? schema.playerHistory.heartbeats : sort === "sessions" ? schema.playerHistory.sessions : drizzleOrm.sql`${schema.playerHistory.name} COLLATE NOCASE`;
509
+ const orderDir = direction === 1 ? drizzleOrm.asc : drizzleOrm.desc;
510
+ const totalResult = this.db.select({ count: drizzleOrm.count() }).from(schema.playerHistory).where(baseWhere).get();
511
+ const players = this.db.select().from(schema.playerHistory).where(baseWhere).orderBy(orderDir(sortCol)).limit(limit).offset(limit * page).all();
512
+ const total = totalResult?.count ?? 0;
513
+ const result = players.map((p) => this._toUserHistory(p));
514
+ if (search.length > 0 && page === 0) {
515
+ const exactRows = this.db.select().from(schema.playerHistory).where(
516
+ drizzleOrm.or(
517
+ drizzleOrm.eq(schema.playerHistory.id, search),
518
+ drizzleOrm.eq(schema.playerHistory.name, search),
519
+ drizzleOrm.eq(schema.playerHistory.displayName, search)
520
+ )
521
+ ).all();
522
+ if (exactRows.length > 0) {
523
+ for (const res of exactRows) {
524
+ const idx = result.findIndex((p) => p.id === res.id);
525
+ if (idx >= 0) result.splice(idx, 1);
526
+ }
527
+ result.splice(0, 0, ...exactRows.map((r) => this._toUserHistory(r)));
554
528
  }
555
- players.splice(0, 0, ...exactResult);
556
529
  }
557
- return {
558
- pages: Math.ceil(total / count),
559
- total,
560
- players
561
- };
530
+ return { pages: Math.ceil(total / limit), total, players: result };
562
531
  }
563
532
  // get paginated users
564
533
  async getUsers({
565
- count = 50,
534
+ count: limit = 50,
566
535
  search = "",
567
536
  page = 0,
568
537
  sort = "name",
569
538
  direction = 1
570
539
  } = {}) {
571
- const pattern$1 = pattern.explode(search);
572
- const query = {
573
- type: "user",
574
- // only filter if there's a query
575
- ...search.length > 0 ? {
576
- $or: [
577
- // id was pasted in
578
- { playerId: search },
579
- // user's current name
580
- { name: { $regex: pattern$1 } }
581
- ]
582
- } : {}
583
- };
584
- const [total, users] = await Promise.all([
585
- this.stores.users.count(query),
586
- this.stores.users.find(query).sort({ [sort]: direction }).skip(count * page).limit(count).exec()
587
- ]);
540
+ const pattern$1 = search.length > 0 ? pattern.explode(search) : null;
541
+ let baseWhere = drizzleOrm.sql`1=1`;
542
+ if (pattern$1) {
543
+ baseWhere = drizzleOrm.sql`(
544
+ ${schema.users.playerId} = ${search}
545
+ OR ${schema.users.username} REGEXP ${pattern$1.source}
546
+ )`;
547
+ }
548
+ const sortCol = sort === "lastOnline" ? schema.users.lastOnline : sort === "created" ? schema.users.created : drizzleOrm.sql`${schema.users.username} COLLATE NOCASE`;
549
+ const orderDir = direction === 1 ? drizzleOrm.asc : drizzleOrm.desc;
550
+ const totalResult = this.db.select({ count: drizzleOrm.count() }).from(schema.users).where(baseWhere).get();
551
+ const users = this.db.select().from(schema.users).where(baseWhere).orderBy(orderDir(sortCol)).limit(limit).offset(limit * page).all();
588
552
  return {
589
- pages: Math.ceil(total / count),
590
- total,
591
- users
553
+ pages: Math.ceil((totalResult?.count ?? 0) / limit),
554
+ total: totalResult?.count ?? 0,
555
+ users: users.map((u) => this._toStoreUser(u))
592
556
  };
593
557
  }
594
558
  // get an individual player, ban and kick history, and notes
595
559
  async getPlayer(id) {
596
- const [player, banHistory, kickHistory, notes] = await Promise.all([
597
- this.stores.players.findOne({ type: "userHistory", id }),
598
- this.stores.players.find({ type: "banHistory", banned: id }).sort({ created: -1 }).limit(25).exec(),
599
- this.stores.players.find({ type: "kickHistory", kicked: id }).sort({ created: -1 }).limit(25).exec(),
600
- this.stores.players.find({ type: "note", id }).sort({ created: -1 }).limit(25).exec()
601
- ]);
560
+ const player = this.db.select().from(schema.playerHistory).where(drizzleOrm.eq(schema.playerHistory.id, id)).get();
602
561
  if (!player) return null;
603
- return { ...player, banHistory, kickHistory, notes };
562
+ const banHist = this.db.select().from(schema.banHistory).where(drizzleOrm.eq(schema.banHistory.banned, id)).orderBy(drizzleOrm.desc(schema.banHistory.created)).limit(25).all();
563
+ const kickHist = this.db.select().from(schema.kickHistory).where(drizzleOrm.eq(schema.kickHistory.kicked, id)).orderBy(drizzleOrm.desc(schema.kickHistory.created)).limit(25).all();
564
+ const noteRows = this.db.select().from(schema.playerNotes).where(drizzleOrm.eq(schema.playerNotes.playerId, id)).limit(25).all();
565
+ return {
566
+ ...this._toUserHistory(player),
567
+ banHistory: banHist.map((b) => ({
568
+ type: "banHistory",
569
+ _id: String(b.id),
570
+ banned: b.banned,
571
+ bannerId: b.bannerId,
572
+ created: b.created,
573
+ expires: b.expires,
574
+ reason: b.reason
575
+ })),
576
+ kickHistory: kickHist.map((k) => ({
577
+ type: "kickHistory",
578
+ _id: String(k.id),
579
+ kicked: k.kicked,
580
+ kickerId: k.kickerId,
581
+ created: k.created,
582
+ reason: k.reason
583
+ })),
584
+ notes: noteRows.map((n) => ({
585
+ type: "note",
586
+ _id: String(n.id),
587
+ id: n.playerId,
588
+ note: n.note
589
+ }))
590
+ };
604
591
  }
605
592
  // add a user to the visit history, returns true if this is a first visit
606
593
  async addVisit(user) {
607
- const existing = await this.stores.players.findOne({
608
- type: "userHistory",
609
- id: user.id
610
- });
611
594
  const now = Date.now();
612
595
  const instanceId = await this.getInstanceId();
613
- if (!existing) {
614
- await this.stores.players.insert({
615
- type: "userHistory",
616
- // base brickadia user info
617
- id: user.id,
596
+ const visit = this.sqlite.transaction(() => {
597
+ const existing = this.db.select().from(schema.playerHistory).where(drizzleOrm.eq(schema.playerHistory.id, user.id)).get();
598
+ if (!existing) {
599
+ this.db.insert(schema.playerHistory).values({
600
+ id: user.id,
601
+ name: user.name,
602
+ displayName: user.displayName,
603
+ nameHistory: [
604
+ { name: user.name, displayName: user.displayName, date: now }
605
+ ],
606
+ ips: [],
607
+ created: now,
608
+ lastSeen: now,
609
+ lastInstanceId: instanceId,
610
+ heartbeats: 0,
611
+ sessions: 1,
612
+ instances: 1
613
+ }).run();
614
+ return true;
615
+ }
616
+ const nameHistory = existing.nameHistory ?? [];
617
+ const hasName = nameHistory.some(
618
+ (h) => h.name === user.name && h.displayName === user.displayName
619
+ );
620
+ if (!hasName) {
621
+ nameHistory.push({
622
+ name: user.name,
623
+ displayName: user.displayName,
624
+ date: now
625
+ });
626
+ }
627
+ this.db.update(schema.playerHistory).set({
628
+ lastSeen: now,
618
629
  name: user.name,
619
630
  displayName: user.displayName,
620
- // list of names and when they were first used
621
- nameHistory: [
622
- { name: user.name, displayName: user.displayName, date: now }
623
- ],
624
- // list of ips this player has been on
625
- ips: [],
626
- // first time player was seen
627
- created: now,
628
- // last time player was seen
629
- lastSeen: now,
630
- // last server this player has been on
631
631
  lastInstanceId: instanceId,
632
- // number of status checks this player has observed
633
- heartbeats: 0,
634
- // number of sessions (at least an hour apart) this player has had
635
- sessions: 1,
636
- // number of instances (times omegga was started) this player has joined
637
- instances: 1
638
- });
639
- return true;
640
- } else {
641
- await this.stores.players.update(
642
- { _id: existing._id },
643
- {
644
- $inc: {
645
- // increment number of sessions if the player hasn't been seen for an hour
646
- sessions: existing.lastSeen < now - 60 * 60 * 1e3 ? 1 : 0,
647
- // increment the number of instances this player has joined
648
- instances: existing.lastInstanceId === instanceId ? 1 : 0
649
- },
650
- $set: {
651
- // update last seen time and user name
652
- lastSeen: now,
653
- name: user.name,
654
- displayName: user.displayName,
655
- // set the last joined instance
656
- lastInstanceId: instanceId
657
- },
658
- // add the name to the history if it was not already in it
659
- ...existing.nameHistory.some(
660
- (h) => h.name === user.name && h.displayName === user.displayName
661
- ) ? {} : {
662
- $addToSet: {
663
- nameHistory: {
664
- name: user.name,
665
- displayName: user.displayName,
666
- date: now
667
- }
668
- }
669
- }
670
- }
671
- );
632
+ nameHistory,
633
+ sessions: drizzleOrm.sql`${schema.playerHistory.sessions} + ${existing.lastSeen < now - 60 * 60 * 1e3 ? 1 : 0}`,
634
+ instances: drizzleOrm.sql`${schema.playerHistory.instances} + ${existing.lastInstanceId !== instanceId ? 1 : 0}`
635
+ }).where(drizzleOrm.eq(schema.playerHistory.id, existing.id)).run();
672
636
  return false;
673
- }
637
+ });
638
+ return visit();
674
639
  }
675
640
  // use data from minutely heartbeats to fuel metrics
676
641
  async addHeartbeat(data) {
677
642
  const now = Date.now();
678
- await this.stores.status.insert({
679
- type: "heartbeat",
680
- created: now,
681
- bricks: data.bricks,
682
- players: data.players
683
- });
684
- await this.stores.players.update(
685
- {
686
- // all players in the status update
687
- id: { $in: data.players }
688
- },
689
- {
690
- // update last seen
691
- $set: { lastSeen: now },
692
- // increment heartbeats
693
- $inc: { heartbeats: 1 }
694
- },
695
- { multi: true }
696
- );
697
- const players = await this.stores.players.find({
698
- id: { $in: data.players }
699
- });
700
- await Promise.all(
701
- players.filter((p) => !(p.ips || []).includes(data.ips[p.id])).map(
702
- (p) => this.stores.players.update(
703
- { _id: p._id },
704
- {
705
- $addToSet: { ips: data.ips[p.id] }
643
+ const tx = this.sqlite.transaction(() => {
644
+ this.db.insert(schema.heartbeats).values({
645
+ created: now,
646
+ bricks: data.bricks,
647
+ players: data.players
648
+ }).run();
649
+ if (data.players.length > 0) {
650
+ this.db.update(schema.playerHistory).set({
651
+ lastSeen: now,
652
+ heartbeats: drizzleOrm.sql`${schema.playerHistory.heartbeats} + 1`
653
+ }).where(drizzleOrm.inArray(schema.playerHistory.id, data.players)).run();
654
+ const players = this.db.select().from(schema.playerHistory).where(drizzleOrm.inArray(schema.playerHistory.id, data.players)).all();
655
+ for (const p of players) {
656
+ const ip = data.ips[p.id];
657
+ if (ip && !(p.ips ?? []).includes(ip)) {
658
+ const ips = [...p.ips ?? [], ip];
659
+ this.db.update(schema.playerHistory).set({ ips }).where(drizzleOrm.eq(schema.playerHistory.id, p.id)).run();
706
660
  }
707
- )
708
- )
709
- );
661
+ }
662
+ }
663
+ });
664
+ tx();
665
+ }
666
+ upsertBanHistory(entry) {
667
+ this.db.insert(schema.banHistory).values({
668
+ banned: entry.banned,
669
+ bannerId: entry.bannerId ?? "",
670
+ created: typeof entry.created === "number" ? entry.created : 0,
671
+ expires: typeof entry.expires === "number" ? entry.expires : 0,
672
+ reason: entry.reason ?? ""
673
+ }).onConflictDoNothing().run();
674
+ }
675
+ upsertKickHistory(entry) {
676
+ this.db.insert(schema.kickHistory).values({
677
+ kicked: entry.kicked,
678
+ kickerId: entry.kickerId ?? "",
679
+ created: entry.created ?? 0,
680
+ reason: entry.reason ?? ""
681
+ }).onConflictDoNothing().run();
710
682
  }
711
683
  async getAutoRestartConfig() {
712
- let config = await this.stores.server.findOne({
713
- type: "autoRestartConfig"
714
- });
715
- if (!config) {
716
- config = await this.stores.server.insert({
684
+ const row = this.db.select().from(schema.serverConfig).where(drizzleOrm.eq(schema.serverConfig.key, "autoRestartConfig")).get();
685
+ if (!row) {
686
+ const config2 = {
717
687
  type: "autoRestartConfig",
718
688
  maxUptime: 48,
719
689
  maxUptimeEnabled: false,
@@ -727,8 +697,12 @@ class Database extends EventEmitter {
727
697
  playersEnabled: true,
728
698
  saveWorld: true,
729
699
  crashRestartEnabled: true
730
- });
700
+ };
701
+ this.db.insert(schema.serverConfig).values({ key: "autoRestartConfig", value: config2 }).run();
702
+ return config2;
731
703
  }
704
+ const config = row.value;
705
+ config.type = "autoRestartConfig";
732
706
  config.autoUpdateEnabled ??= true;
733
707
  config.autoUpdateIntervalMins ??= 60;
734
708
  config.saveWorld ??= true;
@@ -745,15 +719,10 @@ class Database extends EventEmitter {
745
719
  Math.max(10, Math.min(config.autoUpdateIntervalMins ?? 60, Infinity))
746
720
  );
747
721
  config.autoUpdateEnabled ??= true;
748
- await this.stores.server.update(
749
- { type: "autoRestartConfig" },
750
- {
751
- $set: {
752
- ...config
753
- }
754
- },
755
- { upsert: false }
756
- );
722
+ this.db.insert(schema.serverConfig).values({ key: "autoRestartConfig", value: config }).onConflictDoUpdate({
723
+ target: schema.serverConfig.key,
724
+ set: { value: config }
725
+ }).run();
757
726
  }
758
727
  // update player online-time punchcard
759
728
  async updatePlayerPunchcard(numNewPlayers) {
@@ -763,37 +732,68 @@ class Database extends EventEmitter {
763
732
  const day = now.getUTCDay();
764
733
  const month = now.getUTCMonth();
765
734
  const year = now.getUTCFullYear();
766
- const card = await this.stores.status.findOne({
767
- type: "punchcard",
768
- kind: "playerCount",
769
- month,
770
- year
771
- });
735
+ const card = this.db.select().from(schema.punchcards).where(
736
+ drizzleOrm.and(
737
+ drizzleOrm.eq(schema.punchcards.kind, "playerCount"),
738
+ drizzleOrm.eq(schema.punchcards.month, month),
739
+ drizzleOrm.eq(schema.punchcards.year, year)
740
+ )
741
+ ).get();
772
742
  if (!card) {
773
743
  const punchcard = createPunchcard();
774
744
  punchcard[day][hour] = numNewPlayers;
775
- await this.stores.status.insert({
776
- type: "punchcard",
745
+ this.db.insert(schema.punchcards).values({
777
746
  kind: "playerCount",
778
747
  created: time2,
779
748
  updated: time2,
780
749
  month,
781
750
  year,
782
751
  punchcard
783
- });
752
+ }).run();
784
753
  } else {
785
- card.punchcard[day][hour] += numNewPlayers;
786
- await this.stores.status.update(
787
- { _id: card._id },
788
- {
789
- $set: {
790
- punchcard: card.punchcard,
791
- updated: time2
792
- }
793
- }
794
- );
754
+ const punchcard = card.punchcard;
755
+ punchcard[day][hour] += numNewPlayers;
756
+ this.db.update(schema.punchcards).set({ punchcard, updated: time2 }).where(drizzleOrm.eq(schema.punchcards.id, card.id)).run();
795
757
  }
796
758
  }
759
+ _toStoreUser(row) {
760
+ const user = {
761
+ type: "user",
762
+ id: row.id,
763
+ _id: row.id,
764
+ created: row.created,
765
+ lastOnline: row.lastOnline,
766
+ username: row.username,
767
+ hash: row.hash,
768
+ isOwner: row.isOwner,
769
+ roles: row.roles ?? [],
770
+ playerId: row.playerId,
771
+ isBanned: row.isBanned,
772
+ permissions: row.permissions ?? permissions.EMPTY_PERMISSIONS,
773
+ totpSecret: row.totpSecret ?? void 0,
774
+ totpEnabled: row.totpEnabled,
775
+ passkeys: row.passkeys ?? [],
776
+ recoveryCodes: row.recoveryCodes ?? []
777
+ };
778
+ return user;
779
+ }
780
+ _toUserHistory(row) {
781
+ return {
782
+ type: "userHistory",
783
+ _id: row.id,
784
+ id: row.id,
785
+ name: row.name,
786
+ displayName: row.displayName,
787
+ nameHistory: row.nameHistory ?? [],
788
+ ips: row.ips ?? [],
789
+ created: row.created,
790
+ lastSeen: row.lastSeen,
791
+ lastInstanceId: row.lastInstanceId,
792
+ heartbeats: row.heartbeats,
793
+ sessions: row.sessions,
794
+ instances: row.instances
795
+ };
796
+ }
797
797
  }
798
798
  exports.default = Database;
799
799
  //# sourceMappingURL=database.js.map