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