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.
- package/.claude/settings.local.json +9 -2
- package/CHANGELOG.md +12 -1
- package/dist/cli/terminal.js +5 -1
- package/dist/cli/terminal.js.map +1 -1
- package/dist/db/connection.js +31 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.js +20 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0000_long_killer_shrike.sql +111 -0
- package/dist/db/migrations/meta/0000_snapshot.json +737 -0
- package/dist/db/migrations/meta/_journal.json +13 -0
- package/dist/db/nedbImport.js +344 -0
- package/dist/db/nedbImport.js.map +1 -0
- package/dist/db/plugin-migrations/0000_faulty_tyger_tiger.sql +11 -0
- package/dist/db/plugin-migrations/meta/0000_snapshot.json +81 -0
- package/dist/db/plugin-migrations/meta/_journal.json +13 -0
- package/dist/db/pluginSchema.js +19 -0
- package/dist/db/pluginSchema.js.map +1 -0
- package/dist/db/schema.js +174 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/omegga/matchers/version.js +30 -13
- package/dist/omegga/matchers/version.js.map +1 -1
- package/dist/omegga/plugin.js +47 -49
- package/dist/omegga/plugin.js.map +1 -1
- package/dist/omegga/server.js.map +1 -1
- package/dist/softconfig.js +9 -21
- package/dist/softconfig.js.map +1 -1
- package/dist/updater/steam.js +1 -1
- package/dist/updater/steam.js.map +1 -1
- package/dist/util/brdb.js +84 -2
- package/dist/util/brdb.js.map +1 -1
- package/dist/webserver/backend/database.js +490 -576
- package/dist/webserver/backend/database.js.map +1 -1
- package/dist/webserver/backend/index.js +19 -8
- package/dist/webserver/backend/index.js.map +1 -1
- package/dist/webserver/backend/metrics.js +7 -5
- package/dist/webserver/backend/metrics.js.map +1 -1
- package/dist/webserver/backend/permissions.js +0 -10
- package/dist/webserver/backend/permissions.js.map +1 -1
- package/dist/webserver/backend/roleHierarchy.js +2 -4
- package/dist/webserver/backend/roleHierarchy.js.map +1 -1
- package/dist/webserver/backend/router/player.js +4 -16
- package/dist/webserver/backend/router/player.js.map +1 -1
- package/dist/webserver/backend/router/role.js +12 -18
- package/dist/webserver/backend/router/role.js.map +1 -1
- package/dist/webserver/backend/router/server.js +32 -2
- package/dist/webserver/backend/router/server.js.map +1 -1
- package/dist/webserver/backend/router/user.js +19 -25
- package/dist/webserver/backend/router/user.js.map +1 -1
- package/dist/webserver/backend/trpc.js +1 -4
- package/dist/webserver/backend/trpc.js.map +1 -1
- package/drizzle.config.ts +7 -0
- package/drizzle.plugin.config.ts +7 -0
- package/package.json +7 -3
- package/public/app.html +319 -15
- package/public/assets/IconWorld-CmdphDgp.js +2 -0
- package/public/assets/IconWorld-CmdphDgp.js.map +1 -0
- package/public/assets/IconWorldPlus-DKSZjbqh.js +2 -0
- package/public/assets/IconWorldPlus-DKSZjbqh.js.map +1 -0
- package/public/assets/app-BiBsxujC.js +7 -0
- package/public/assets/app-BiBsxujC.js.map +1 -0
- package/public/assets/favicon-DYc6c67x.ico +0 -0
- package/public/assets/{index-DEL-PD_8.js → index-2cgXLhrt.js} +2 -2
- package/public/assets/{index-DEL-PD_8.js.map → index-2cgXLhrt.js.map} +1 -1
- package/public/assets/index-5EkjXNMi.js +12 -0
- package/public/assets/index-5EkjXNMi.js.map +1 -0
- package/public/assets/logo-Dl-LlTow.webp +0 -0
- package/public/assets/shared/@components-VkA0DIz6.js +2 -0
- package/public/assets/shared/@components-VkA0DIz6.js.map +1 -0
- package/public/assets/shared/@hooks-CiJaUL6_.js +2 -0
- package/public/assets/shared/@hooks-CiJaUL6_.js.map +1 -0
- package/public/assets/shared/{@tabler_icons-react-CdTxTJVq.js → @tabler_icons-react-B4130bhe.js} +3 -3
- package/public/assets/shared/{@tabler_icons-react-CdTxTJVq.js.map → @tabler_icons-react-B4130bhe.js.map} +1 -1
- package/public/assets/shared/{wouter-CNISJ5i6.js → wouter-CUC1caxu.js} +2 -2
- package/public/assets/shared/wouter-CUC1caxu.js.map +1 -0
- package/public/assets/user-XF1UyJMA.js +2 -0
- package/public/assets/user-XF1UyJMA.js.map +1 -0
- package/public/auth.html +105 -7
- package/public/assets/IconWorld-Bx3cKMms.js +0 -2
- package/public/assets/IconWorld-Bx3cKMms.js.map +0 -1
- package/public/assets/IconWorldPlus-BHSmzWsZ.js +0 -2
- package/public/assets/IconWorldPlus-BHSmzWsZ.js.map +0 -1
- package/public/assets/app-J90GEVdo.js +0 -7
- package/public/assets/app-J90GEVdo.js.map +0 -1
- package/public/assets/index-wYDkzaAO.js +0 -12
- package/public/assets/index-wYDkzaAO.js.map +0 -1
- package/public/assets/shared/@components-BWkdUJhC.js +0 -2
- package/public/assets/shared/@components-BWkdUJhC.js.map +0 -1
- package/public/assets/shared/@hooks-BuRTblPY.js +0 -2
- package/public/assets/shared/@hooks-BuRTblPY.js.map +0 -1
- package/public/assets/shared/wouter-CNISJ5i6.js.map +0 -1
- package/public/assets/useHasScope-DZPCXNQE.js +0 -2
- 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
|
|
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
|
|
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.
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
144
|
+
}).run();
|
|
145
|
+
return this.findUserById(id);
|
|
297
146
|
}
|
|
298
147
|
// set a user's password
|
|
299
148
|
async userPasswd(username, password) {
|
|
300
|
-
const
|
|
149
|
+
const h = await this.hash(password);
|
|
301
150
|
if (!await this.userExists(username))
|
|
302
151
|
throw new Error("user does not exist");
|
|
303
|
-
|
|
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
|
-
|
|
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
|
|
157
|
+
return 1;
|
|
315
158
|
}
|
|
316
159
|
async deleteUser(username) {
|
|
317
|
-
|
|
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
|
|
162
|
+
return 1;
|
|
323
163
|
}
|
|
324
|
-
async setUserPermissions(username,
|
|
325
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
|
344
|
-
const result = {
|
|
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(
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
366
|
-
if (!
|
|
367
|
-
if (await bcrypt.compare(password,
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
379
|
-
|
|
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
|
-
|
|
218
|
+
drizzleOrm.and(drizzleOrm.eq(schema.users.username, ""), drizzleOrm.eq(schema.users.isOwner, true)),
|
|
382
219
|
// the user exists and has an id
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
});
|
|
442
|
-
if (!
|
|
443
|
-
const codes = (
|
|
444
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
{
|
|
491
|
-
{
|
|
492
|
-
|
|
493
|
-
|
|
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:
|
|
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 =
|
|
511
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
{
|
|
520
|
-
|
|
521
|
-
|
|
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(
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
-
created:
|
|
577
|
-
|
|
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
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
{
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
$
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
799
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
{
|
|
837
|
-
|
|
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 =
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
|
872
|
-
|
|
873
|
-
|
|
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
|