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