omegga 1.5.2 → 1.6.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/CHANGELOG.md +19 -2
- package/bin/omegga +1 -1
- package/dist/cli/terminal.js +46 -0
- package/dist/cli/terminal.js.map +1 -1
- package/dist/omegga/commandInjector.js +13 -6
- package/dist/omegga/commandInjector.js.map +1 -1
- package/dist/omegga/player.js +2 -2
- package/dist/omegga/player.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/webserver/backend/database.js +219 -7
- package/dist/webserver/backend/database.js.map +1 -1
- package/dist/webserver/backend/events.js +1 -1
- package/dist/webserver/backend/events.js.map +1 -1
- package/dist/webserver/backend/index.js +120 -2
- package/dist/webserver/backend/index.js.map +1 -1
- package/dist/webserver/backend/permissions.js +74 -0
- package/dist/webserver/backend/permissions.js.map +1 -0
- package/dist/webserver/backend/router/chat.js +9 -7
- package/dist/webserver/backend/router/chat.js.map +1 -1
- package/dist/webserver/backend/router/index.js +3 -1
- package/dist/webserver/backend/router/index.js.map +1 -1
- package/dist/webserver/backend/router/mfa.js +162 -0
- package/dist/webserver/backend/router/mfa.js.map +1 -0
- package/dist/webserver/backend/router/player.js +7 -6
- package/dist/webserver/backend/router/player.js.map +1 -1
- package/dist/webserver/backend/router/plugin.js +14 -10
- package/dist/webserver/backend/router/plugin.js.map +1 -1
- package/dist/webserver/backend/router/role.js +2 -1
- package/dist/webserver/backend/router/role.js.map +1 -1
- package/dist/webserver/backend/router/server.js +85 -71
- package/dist/webserver/backend/router/server.js.map +1 -1
- package/dist/webserver/backend/router/session.js +11 -2
- package/dist/webserver/backend/router/session.js.map +1 -1
- package/dist/webserver/backend/router/user.js +175 -13
- package/dist/webserver/backend/router/user.js.map +1 -1
- package/dist/webserver/backend/router/world.js +10 -9
- package/dist/webserver/backend/router/world.js.map +1 -1
- package/dist/webserver/backend/scopes.js +303 -0
- package/dist/webserver/backend/scopes.js.map +1 -0
- package/dist/webserver/backend/totp.js +32 -0
- package/dist/webserver/backend/totp.js.map +1 -0
- package/dist/webserver/backend/trpc.js +47 -4
- package/dist/webserver/backend/trpc.js.map +1 -1
- package/doc/permissions.md +193 -0
- package/package.json +7 -2
- package/public/app.html +932 -526
- package/public/assets/IconShieldCheck-Bjlm8mX2.js +2 -0
- package/public/assets/IconShieldCheck-Bjlm8mX2.js.map +1 -0
- package/public/assets/IconWorld-XmQNg6JI.js +2 -0
- package/public/assets/IconWorld-XmQNg6JI.js.map +1 -0
- package/public/assets/IconWorldPlus-CxGGv2eD.js +2 -0
- package/public/assets/IconWorldPlus-CxGGv2eD.js.map +1 -0
- package/public/assets/app-DMr_-wik.js +7 -0
- package/public/assets/app-DMr_-wik.js.map +1 -0
- package/public/assets/auth-CEYiizOt.js +2 -0
- package/public/assets/auth-CEYiizOt.js.map +1 -0
- package/public/assets/index-BDkarAkX.js +3 -0
- package/public/assets/index-BDkarAkX.js.map +1 -0
- package/public/assets/{index-2cgXLhrt.js → index-DEL-PD_8.js} +2 -2
- package/public/assets/{index-2cgXLhrt.js.map → index-DEL-PD_8.js.map} +1 -1
- package/public/assets/index-D_STNela.js +2 -0
- package/public/assets/index-D_STNela.js.map +1 -0
- package/public/assets/index-Dkh3HiBz.js +4 -0
- package/public/assets/index-Dkh3HiBz.js.map +1 -0
- package/public/assets/shared/@components-CgsSYMe6.js +2 -0
- package/public/assets/shared/@components-CgsSYMe6.js.map +1 -0
- package/public/assets/shared/@hooks-DT93N7wd.js +2 -0
- package/public/assets/shared/{@hooks-Ch_hJYh0.js.map → @hooks-DT93N7wd.js.map} +1 -1
- package/public/assets/shared/@tabler_icons-react-DrVFX3uL.js +3 -0
- package/public/assets/shared/{@tabler_icons-react-iQk11eD_.js.map → @tabler_icons-react-DrVFX3uL.js.map} +1 -1
- package/public/assets/shared/{wouter-CUC1caxu.js → wouter-CNISJ5i6.js} +2 -2
- package/public/assets/shared/wouter-CNISJ5i6.js.map +1 -0
- package/public/assets/useHasScope-BOdFktko.js +2 -0
- package/public/assets/useHasScope-BOdFktko.js.map +1 -0
- package/public/auth.html +97 -33
- package/templates/safe-ts/omegga.d.ts +1 -1
- package/public/assets/IconArrowRight-BuVxhqKX.js +0 -2
- package/public/assets/IconArrowRight-BuVxhqKX.js.map +0 -1
- package/public/assets/IconWorld-B6GK5qpx.js +0 -2
- package/public/assets/IconWorld-B6GK5qpx.js.map +0 -1
- package/public/assets/IconWorldPlus-Dkos72Ws.js +0 -2
- package/public/assets/IconWorldPlus-Dkos72Ws.js.map +0 -1
- package/public/assets/app-CLZ-5FIi.js +0 -6
- package/public/assets/app-CLZ-5FIi.js.map +0 -1
- package/public/assets/auth-6N37o421.js +0 -2
- package/public/assets/auth-6N37o421.js.map +0 -1
- package/public/assets/index-DLZ8-XzO.js +0 -4
- package/public/assets/index-DLZ8-XzO.js.map +0 -1
- package/public/assets/index-DY750sgZ.js +0 -2
- package/public/assets/index-DY750sgZ.js.map +0 -1
- package/public/assets/shared/@components-DPHARroA.js +0 -2
- package/public/assets/shared/@components-DPHARroA.js.map +0 -1
- package/public/assets/shared/@hooks-Ch_hJYh0.js +0 -2
- package/public/assets/shared/@tabler_icons-react-iQk11eD_.js +0 -3
- package/public/assets/shared/wouter-CUC1caxu.js.map +0 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const ScopeDomain = {
|
|
4
|
+
Chat: "chat",
|
|
5
|
+
Player: "player",
|
|
6
|
+
Plugin: "plugin",
|
|
7
|
+
Server: "server",
|
|
8
|
+
User: "user",
|
|
9
|
+
World: "world",
|
|
10
|
+
Role: "role",
|
|
11
|
+
Session: "session"
|
|
12
|
+
};
|
|
13
|
+
const ScopeName = {
|
|
14
|
+
ChatSend: "chat.send",
|
|
15
|
+
ChatRecent: "chat.recent",
|
|
16
|
+
ChatHistory: "chat.history",
|
|
17
|
+
ChatCalendar: "chat.calendar",
|
|
18
|
+
PlayerList: "player.list",
|
|
19
|
+
PlayerGet: "player.get",
|
|
20
|
+
PlayerBan: "player.ban",
|
|
21
|
+
PlayerKick: "player.kick",
|
|
22
|
+
PlayerUnban: "player.unban",
|
|
23
|
+
PlayerClearBricks: "player.clearBricks",
|
|
24
|
+
PluginList: "plugin.list",
|
|
25
|
+
PluginGet: "plugin.get",
|
|
26
|
+
PluginConfig: "plugin.config",
|
|
27
|
+
PluginLoad: "plugin.load",
|
|
28
|
+
PluginUnload: "plugin.unload",
|
|
29
|
+
PluginToggle: "plugin.toggle",
|
|
30
|
+
PluginReloadAll: "plugin.reloadAll",
|
|
31
|
+
ServerStatus: "server.status",
|
|
32
|
+
ServerStart: "server.start",
|
|
33
|
+
ServerStop: "server.stop",
|
|
34
|
+
ServerRestart: "server.restart",
|
|
35
|
+
ServerUpdateCheck: "server.update.check",
|
|
36
|
+
ServerUpdateRun: "server.update.run",
|
|
37
|
+
ServerAutorestartGet: "server.autorestart.get",
|
|
38
|
+
ServerAutorestartSet: "server.autorestart.set",
|
|
39
|
+
ServerUtilization: "server.utilization",
|
|
40
|
+
UserList: "user.list",
|
|
41
|
+
UserCreate: "user.create",
|
|
42
|
+
UserPasswd: "user.passwd",
|
|
43
|
+
UserBan: "user.ban",
|
|
44
|
+
UserDelete: "user.delete",
|
|
45
|
+
UserPermissions: "user.permissions",
|
|
46
|
+
UserReadMfa: "user.readMfa",
|
|
47
|
+
UserResetMfa: "user.resetMfa",
|
|
48
|
+
WorldList: "world.list",
|
|
49
|
+
WorldActive: "world.active",
|
|
50
|
+
WorldNext: "world.next",
|
|
51
|
+
WorldRevisions: "world.revisions",
|
|
52
|
+
WorldMeta: "world.meta",
|
|
53
|
+
WorldLoad: "world.load",
|
|
54
|
+
WorldUse: "world.use",
|
|
55
|
+
WorldSave: "world.save",
|
|
56
|
+
WorldCreate: "world.create",
|
|
57
|
+
RoleList: "role.list",
|
|
58
|
+
SessionInfo: "session.info"
|
|
59
|
+
};
|
|
60
|
+
const S = ScopeName;
|
|
61
|
+
const D = ScopeDomain;
|
|
62
|
+
const SCOPES = {
|
|
63
|
+
// Chat
|
|
64
|
+
[S.ChatSend]: {
|
|
65
|
+
description: "Send chat messages",
|
|
66
|
+
readOnly: false,
|
|
67
|
+
domain: D.Chat
|
|
68
|
+
},
|
|
69
|
+
[S.ChatRecent]: {
|
|
70
|
+
description: "View recent chat messages",
|
|
71
|
+
readOnly: true,
|
|
72
|
+
domain: D.Chat
|
|
73
|
+
},
|
|
74
|
+
[S.ChatHistory]: {
|
|
75
|
+
description: "View chat history",
|
|
76
|
+
readOnly: true,
|
|
77
|
+
domain: D.Chat
|
|
78
|
+
},
|
|
79
|
+
[S.ChatCalendar]: {
|
|
80
|
+
description: "View chat calendar",
|
|
81
|
+
readOnly: true,
|
|
82
|
+
domain: D.Chat
|
|
83
|
+
},
|
|
84
|
+
// Player
|
|
85
|
+
[S.PlayerList]: {
|
|
86
|
+
description: "View player list",
|
|
87
|
+
readOnly: true,
|
|
88
|
+
domain: D.Player
|
|
89
|
+
},
|
|
90
|
+
[S.PlayerGet]: {
|
|
91
|
+
description: "View player details",
|
|
92
|
+
readOnly: true,
|
|
93
|
+
domain: D.Player
|
|
94
|
+
},
|
|
95
|
+
[S.PlayerBan]: {
|
|
96
|
+
description: "Ban players",
|
|
97
|
+
readOnly: false,
|
|
98
|
+
domain: D.Player
|
|
99
|
+
},
|
|
100
|
+
[S.PlayerKick]: {
|
|
101
|
+
description: "Kick players",
|
|
102
|
+
readOnly: false,
|
|
103
|
+
domain: D.Player
|
|
104
|
+
},
|
|
105
|
+
[S.PlayerUnban]: {
|
|
106
|
+
description: "Unban players",
|
|
107
|
+
readOnly: false,
|
|
108
|
+
domain: D.Player
|
|
109
|
+
},
|
|
110
|
+
[S.PlayerClearBricks]: {
|
|
111
|
+
description: "Clear player bricks",
|
|
112
|
+
readOnly: false,
|
|
113
|
+
domain: D.Player
|
|
114
|
+
},
|
|
115
|
+
// Plugin
|
|
116
|
+
[S.PluginList]: {
|
|
117
|
+
description: "View plugin list",
|
|
118
|
+
readOnly: true,
|
|
119
|
+
domain: D.Plugin
|
|
120
|
+
},
|
|
121
|
+
[S.PluginGet]: {
|
|
122
|
+
description: "View plugin details",
|
|
123
|
+
readOnly: true,
|
|
124
|
+
domain: D.Plugin
|
|
125
|
+
},
|
|
126
|
+
[S.PluginConfig]: {
|
|
127
|
+
description: "Change plugin configuration",
|
|
128
|
+
readOnly: false,
|
|
129
|
+
domain: D.Plugin
|
|
130
|
+
},
|
|
131
|
+
[S.PluginLoad]: {
|
|
132
|
+
description: "Load plugins",
|
|
133
|
+
readOnly: false,
|
|
134
|
+
domain: D.Plugin
|
|
135
|
+
},
|
|
136
|
+
[S.PluginUnload]: {
|
|
137
|
+
description: "Unload plugins",
|
|
138
|
+
readOnly: false,
|
|
139
|
+
domain: D.Plugin
|
|
140
|
+
},
|
|
141
|
+
[S.PluginToggle]: {
|
|
142
|
+
description: "Enable/disable plugins",
|
|
143
|
+
readOnly: false,
|
|
144
|
+
domain: D.Plugin
|
|
145
|
+
},
|
|
146
|
+
[S.PluginReloadAll]: {
|
|
147
|
+
description: "Reload all plugins",
|
|
148
|
+
readOnly: false,
|
|
149
|
+
domain: D.Plugin
|
|
150
|
+
},
|
|
151
|
+
// Server
|
|
152
|
+
[S.ServerStatus]: {
|
|
153
|
+
description: "View server status",
|
|
154
|
+
readOnly: true,
|
|
155
|
+
domain: D.Server
|
|
156
|
+
},
|
|
157
|
+
[S.ServerStart]: {
|
|
158
|
+
description: "Start the server",
|
|
159
|
+
readOnly: false,
|
|
160
|
+
domain: D.Server
|
|
161
|
+
},
|
|
162
|
+
[S.ServerStop]: {
|
|
163
|
+
description: "Stop the server",
|
|
164
|
+
readOnly: false,
|
|
165
|
+
domain: D.Server
|
|
166
|
+
},
|
|
167
|
+
[S.ServerRestart]: {
|
|
168
|
+
description: "Restart the server",
|
|
169
|
+
readOnly: false,
|
|
170
|
+
domain: D.Server
|
|
171
|
+
},
|
|
172
|
+
[S.ServerUpdateCheck]: {
|
|
173
|
+
description: "Check for server updates",
|
|
174
|
+
readOnly: false,
|
|
175
|
+
domain: D.Server
|
|
176
|
+
},
|
|
177
|
+
[S.ServerUpdateRun]: {
|
|
178
|
+
description: "Update the server",
|
|
179
|
+
readOnly: false,
|
|
180
|
+
domain: D.Server
|
|
181
|
+
},
|
|
182
|
+
[S.ServerAutorestartGet]: {
|
|
183
|
+
description: "View auto-restart config",
|
|
184
|
+
readOnly: true,
|
|
185
|
+
domain: D.Server
|
|
186
|
+
},
|
|
187
|
+
[S.ServerAutorestartSet]: {
|
|
188
|
+
description: "Change auto-restart config",
|
|
189
|
+
readOnly: false,
|
|
190
|
+
domain: D.Server
|
|
191
|
+
},
|
|
192
|
+
[S.ServerUtilization]: {
|
|
193
|
+
description: "View current system utilization",
|
|
194
|
+
readOnly: true,
|
|
195
|
+
domain: D.Server
|
|
196
|
+
},
|
|
197
|
+
// User
|
|
198
|
+
[S.UserList]: {
|
|
199
|
+
description: "View web UI users",
|
|
200
|
+
readOnly: true,
|
|
201
|
+
domain: D.User
|
|
202
|
+
},
|
|
203
|
+
[S.UserCreate]: {
|
|
204
|
+
description: "Create web UI users",
|
|
205
|
+
readOnly: false,
|
|
206
|
+
domain: D.User
|
|
207
|
+
},
|
|
208
|
+
[S.UserPasswd]: {
|
|
209
|
+
description: "Change user passwords",
|
|
210
|
+
readOnly: false,
|
|
211
|
+
domain: D.User
|
|
212
|
+
},
|
|
213
|
+
[S.UserBan]: {
|
|
214
|
+
description: "Disable/enable web UI users",
|
|
215
|
+
readOnly: false,
|
|
216
|
+
domain: D.User
|
|
217
|
+
},
|
|
218
|
+
[S.UserDelete]: {
|
|
219
|
+
description: "Delete web UI users",
|
|
220
|
+
readOnly: false,
|
|
221
|
+
domain: D.User
|
|
222
|
+
},
|
|
223
|
+
[S.UserPermissions]: {
|
|
224
|
+
description: "Manage user permissions",
|
|
225
|
+
readOnly: false,
|
|
226
|
+
domain: D.User
|
|
227
|
+
},
|
|
228
|
+
[S.UserReadMfa]: {
|
|
229
|
+
description: "View MFA status of other users",
|
|
230
|
+
readOnly: true,
|
|
231
|
+
domain: D.User
|
|
232
|
+
},
|
|
233
|
+
[S.UserResetMfa]: {
|
|
234
|
+
description: "Reset MFA for other users",
|
|
235
|
+
readOnly: false,
|
|
236
|
+
domain: D.User
|
|
237
|
+
},
|
|
238
|
+
// World
|
|
239
|
+
[S.WorldList]: {
|
|
240
|
+
description: "View world list",
|
|
241
|
+
readOnly: true,
|
|
242
|
+
domain: D.World
|
|
243
|
+
},
|
|
244
|
+
[S.WorldActive]: {
|
|
245
|
+
description: "View active world",
|
|
246
|
+
readOnly: true,
|
|
247
|
+
domain: D.World
|
|
248
|
+
},
|
|
249
|
+
[S.WorldNext]: {
|
|
250
|
+
description: "View next world",
|
|
251
|
+
readOnly: true,
|
|
252
|
+
domain: D.World
|
|
253
|
+
},
|
|
254
|
+
[S.WorldRevisions]: {
|
|
255
|
+
description: "View world revisions",
|
|
256
|
+
readOnly: true,
|
|
257
|
+
domain: D.World
|
|
258
|
+
},
|
|
259
|
+
[S.WorldMeta]: {
|
|
260
|
+
description: "View world metadata",
|
|
261
|
+
readOnly: true,
|
|
262
|
+
domain: D.World
|
|
263
|
+
},
|
|
264
|
+
[S.WorldLoad]: {
|
|
265
|
+
description: "Load worlds",
|
|
266
|
+
readOnly: false,
|
|
267
|
+
domain: D.World
|
|
268
|
+
},
|
|
269
|
+
[S.WorldUse]: {
|
|
270
|
+
description: "Set default world",
|
|
271
|
+
readOnly: false,
|
|
272
|
+
domain: D.World
|
|
273
|
+
},
|
|
274
|
+
[S.WorldSave]: {
|
|
275
|
+
description: "Save worlds",
|
|
276
|
+
readOnly: false,
|
|
277
|
+
domain: D.World
|
|
278
|
+
},
|
|
279
|
+
[S.WorldCreate]: {
|
|
280
|
+
description: "Create worlds",
|
|
281
|
+
readOnly: false,
|
|
282
|
+
domain: D.World
|
|
283
|
+
},
|
|
284
|
+
// Role
|
|
285
|
+
[S.RoleList]: {
|
|
286
|
+
description: "View server roles",
|
|
287
|
+
readOnly: true,
|
|
288
|
+
domain: D.Role
|
|
289
|
+
},
|
|
290
|
+
// Session
|
|
291
|
+
[S.SessionInfo]: {
|
|
292
|
+
description: "View session info",
|
|
293
|
+
readOnly: true,
|
|
294
|
+
domain: D.Session
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
[
|
|
298
|
+
...new Set(Object.values(SCOPES).map((s) => s.domain))
|
|
299
|
+
];
|
|
300
|
+
exports.SCOPES = SCOPES;
|
|
301
|
+
exports.ScopeDomain = ScopeDomain;
|
|
302
|
+
exports.ScopeName = ScopeName;
|
|
303
|
+
//# sourceMappingURL=scopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopes.js","sources":["../../../src/webserver/backend/scopes.ts"],"sourcesContent":["export const ScopeDomain = {\n Chat: 'chat',\n Player: 'player',\n Plugin: 'plugin',\n Server: 'server',\n User: 'user',\n World: 'world',\n Role: 'role',\n Session: 'session',\n} as const;\n\nexport const ScopeName = {\n ChatSend: 'chat.send',\n ChatRecent: 'chat.recent',\n ChatHistory: 'chat.history',\n ChatCalendar: 'chat.calendar',\n\n PlayerList: 'player.list',\n PlayerGet: 'player.get',\n PlayerBan: 'player.ban',\n PlayerKick: 'player.kick',\n PlayerUnban: 'player.unban',\n PlayerClearBricks: 'player.clearBricks',\n\n PluginList: 'plugin.list',\n PluginGet: 'plugin.get',\n PluginConfig: 'plugin.config',\n PluginLoad: 'plugin.load',\n PluginUnload: 'plugin.unload',\n PluginToggle: 'plugin.toggle',\n PluginReloadAll: 'plugin.reloadAll',\n\n ServerStatus: 'server.status',\n ServerStart: 'server.start',\n ServerStop: 'server.stop',\n ServerRestart: 'server.restart',\n ServerUpdateCheck: 'server.update.check',\n ServerUpdateRun: 'server.update.run',\n ServerAutorestartGet: 'server.autorestart.get',\n ServerAutorestartSet: 'server.autorestart.set',\n ServerUtilization: 'server.utilization',\n\n UserList: 'user.list',\n UserCreate: 'user.create',\n UserPasswd: 'user.passwd',\n UserBan: 'user.ban',\n UserDelete: 'user.delete',\n UserPermissions: 'user.permissions',\n UserReadMfa: 'user.readMfa',\n UserResetMfa: 'user.resetMfa',\n\n WorldList: 'world.list',\n WorldActive: 'world.active',\n WorldNext: 'world.next',\n WorldRevisions: 'world.revisions',\n WorldMeta: 'world.meta',\n WorldLoad: 'world.load',\n WorldUse: 'world.use',\n WorldSave: 'world.save',\n WorldCreate: 'world.create',\n\n RoleList: 'role.list',\n\n SessionInfo: 'session.info',\n} as const;\n\nconst S = ScopeName;\nconst D = ScopeDomain;\n\nexport const SCOPES = {\n // Chat\n [S.ChatSend]: {\n description: 'Send chat messages',\n readOnly: false,\n domain: D.Chat,\n },\n [S.ChatRecent]: {\n description: 'View recent chat messages',\n readOnly: true,\n domain: D.Chat,\n },\n [S.ChatHistory]: {\n description: 'View chat history',\n readOnly: true,\n domain: D.Chat,\n },\n [S.ChatCalendar]: {\n description: 'View chat calendar',\n readOnly: true,\n domain: D.Chat,\n },\n\n // Player\n [S.PlayerList]: {\n description: 'View player list',\n readOnly: true,\n domain: D.Player,\n },\n [S.PlayerGet]: {\n description: 'View player details',\n readOnly: true,\n domain: D.Player,\n },\n [S.PlayerBan]: {\n description: 'Ban players',\n readOnly: false,\n domain: D.Player,\n },\n [S.PlayerKick]: {\n description: 'Kick players',\n readOnly: false,\n domain: D.Player,\n },\n [S.PlayerUnban]: {\n description: 'Unban players',\n readOnly: false,\n domain: D.Player,\n },\n [S.PlayerClearBricks]: {\n description: 'Clear player bricks',\n readOnly: false,\n domain: D.Player,\n },\n\n // Plugin\n [S.PluginList]: {\n description: 'View plugin list',\n readOnly: true,\n domain: D.Plugin,\n },\n [S.PluginGet]: {\n description: 'View plugin details',\n readOnly: true,\n domain: D.Plugin,\n },\n [S.PluginConfig]: {\n description: 'Change plugin configuration',\n readOnly: false,\n domain: D.Plugin,\n },\n [S.PluginLoad]: {\n description: 'Load plugins',\n readOnly: false,\n domain: D.Plugin,\n },\n [S.PluginUnload]: {\n description: 'Unload plugins',\n readOnly: false,\n domain: D.Plugin,\n },\n [S.PluginToggle]: {\n description: 'Enable/disable plugins',\n readOnly: false,\n domain: D.Plugin,\n },\n [S.PluginReloadAll]: {\n description: 'Reload all plugins',\n readOnly: false,\n domain: D.Plugin,\n },\n\n // Server\n [S.ServerStatus]: {\n description: 'View server status',\n readOnly: true,\n domain: D.Server,\n },\n [S.ServerStart]: {\n description: 'Start the server',\n readOnly: false,\n domain: D.Server,\n },\n [S.ServerStop]: {\n description: 'Stop the server',\n readOnly: false,\n domain: D.Server,\n },\n [S.ServerRestart]: {\n description: 'Restart the server',\n readOnly: false,\n domain: D.Server,\n },\n [S.ServerUpdateCheck]: {\n description: 'Check for server updates',\n readOnly: false,\n domain: D.Server,\n },\n [S.ServerUpdateRun]: {\n description: 'Update the server',\n readOnly: false,\n domain: D.Server,\n },\n [S.ServerAutorestartGet]: {\n description: 'View auto-restart config',\n readOnly: true,\n domain: D.Server,\n },\n [S.ServerAutorestartSet]: {\n description: 'Change auto-restart config',\n readOnly: false,\n domain: D.Server,\n },\n [S.ServerUtilization]: {\n description: 'View current system utilization',\n readOnly: true,\n domain: D.Server,\n },\n\n // User\n [S.UserList]: {\n description: 'View web UI users',\n readOnly: true,\n domain: D.User,\n },\n [S.UserCreate]: {\n description: 'Create web UI users',\n readOnly: false,\n domain: D.User,\n },\n [S.UserPasswd]: {\n description: 'Change user passwords',\n readOnly: false,\n domain: D.User,\n },\n [S.UserBan]: {\n description: 'Disable/enable web UI users',\n readOnly: false,\n domain: D.User,\n },\n [S.UserDelete]: {\n description: 'Delete web UI users',\n readOnly: false,\n domain: D.User,\n },\n [S.UserPermissions]: {\n description: 'Manage user permissions',\n readOnly: false,\n domain: D.User,\n },\n [S.UserReadMfa]: {\n description: 'View MFA status of other users',\n readOnly: true,\n domain: D.User,\n },\n [S.UserResetMfa]: {\n description: 'Reset MFA for other users',\n readOnly: false,\n domain: D.User,\n },\n\n // World\n [S.WorldList]: {\n description: 'View world list',\n readOnly: true,\n domain: D.World,\n },\n [S.WorldActive]: {\n description: 'View active world',\n readOnly: true,\n domain: D.World,\n },\n [S.WorldNext]: {\n description: 'View next world',\n readOnly: true,\n domain: D.World,\n },\n [S.WorldRevisions]: {\n description: 'View world revisions',\n readOnly: true,\n domain: D.World,\n },\n [S.WorldMeta]: {\n description: 'View world metadata',\n readOnly: true,\n domain: D.World,\n },\n [S.WorldLoad]: {\n description: 'Load worlds',\n readOnly: false,\n domain: D.World,\n },\n [S.WorldUse]: {\n description: 'Set default world',\n readOnly: false,\n domain: D.World,\n },\n [S.WorldSave]: {\n description: 'Save worlds',\n readOnly: false,\n domain: D.World,\n },\n [S.WorldCreate]: {\n description: 'Create worlds',\n readOnly: false,\n domain: D.World,\n },\n\n // Role\n [S.RoleList]: {\n description: 'View server roles',\n readOnly: true,\n domain: D.Role,\n },\n\n // Session\n [S.SessionInfo]: {\n description: 'View session info',\n readOnly: true,\n domain: D.Session,\n },\n} as const;\n\nexport type Scope = keyof typeof SCOPES;\n\nexport const DOMAINS = [\n ...new Set(Object.values(SCOPES).map(s => s.domain)),\n] as const;\n\nexport type Domain = (typeof DOMAINS)[number];\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const otpauth = require("otpauth");
|
|
4
|
+
const ISSUER = "Omegga";
|
|
5
|
+
const DIGITS = 6;
|
|
6
|
+
const PERIOD = 30;
|
|
7
|
+
const ALGORITHM = "SHA1";
|
|
8
|
+
function makeTOTP(secret, label = "") {
|
|
9
|
+
return new otpauth.TOTP({
|
|
10
|
+
issuer: ISSUER,
|
|
11
|
+
label,
|
|
12
|
+
algorithm: ALGORITHM,
|
|
13
|
+
digits: DIGITS,
|
|
14
|
+
period: PERIOD,
|
|
15
|
+
secret: otpauth.Secret.fromBase32(secret)
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function generateSecret() {
|
|
19
|
+
return new otpauth.Secret().base32;
|
|
20
|
+
}
|
|
21
|
+
function verifyToken(token, secret, window = 1) {
|
|
22
|
+
if (!/^\d{6}$/.test(token)) return false;
|
|
23
|
+
const delta = makeTOTP(secret).validate({ token, window });
|
|
24
|
+
return delta !== null;
|
|
25
|
+
}
|
|
26
|
+
function generateURI(secret, username) {
|
|
27
|
+
return makeTOTP(secret, username).toString();
|
|
28
|
+
}
|
|
29
|
+
exports.generateSecret = generateSecret;
|
|
30
|
+
exports.generateURI = generateURI;
|
|
31
|
+
exports.verifyToken = verifyToken;
|
|
32
|
+
//# sourceMappingURL=totp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.js","sources":["../../../src/webserver/backend/totp.ts"],"sourcesContent":["import { TOTP, Secret } from 'otpauth';\n\nconst ISSUER = 'Omegga';\nconst DIGITS = 6;\nconst PERIOD = 30;\nconst ALGORITHM = 'SHA1';\n\nfunction makeTOTP(secret: string, label = ''): TOTP {\n return new TOTP({\n issuer: ISSUER,\n label,\n algorithm: ALGORITHM,\n digits: DIGITS,\n period: PERIOD,\n secret: Secret.fromBase32(secret),\n });\n}\n\nexport function generateSecret(): string {\n return new Secret().base32;\n}\n\nexport function generateToken(secret: string): string {\n return makeTOTP(secret).generate();\n}\n\nexport function verifyToken(\n token: string,\n secret: string,\n window = 1,\n): boolean {\n if (!/^\\d{6}$/.test(token)) return false;\n const delta = makeTOTP(secret).validate({ token, window });\n return delta !== null;\n}\n\nexport function generateURI(secret: string, username: string): string {\n return makeTOTP(secret, username).toString();\n}\n"],"names":["TOTP","Secret"],"mappings":";;;AAEA,MAAM,SAAS;AACf,MAAM,SAAS;AACf,MAAM,SAAS;AACf,MAAM,YAAY;AAElB,SAAS,SAAS,QAAgB,QAAQ,IAAU;AAClD,SAAO,IAAIA,QAAAA,KAAK;AAAA,IACd,QAAQ;AAAA,IACR;AAAA,IACA,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQC,QAAAA,OAAO,WAAW,MAAM;AAAA,EAAA,CACjC;AACH;AAEO,SAAS,iBAAyB;AACvC,SAAO,IAAIA,QAAAA,SAAS;AACtB;AAMO,SAAS,YACd,OACA,QACA,SAAS,GACA;AACT,MAAI,CAAC,UAAU,KAAK,KAAK,EAAG,QAAO;AACnC,QAAM,QAAQ,SAAS,MAAM,EAAE,SAAS,EAAE,OAAO,QAAQ;AACzD,SAAO,UAAU;AACnB;AAEO,SAAS,YAAY,QAAgB,UAA0B;AACpE,SAAO,SAAS,QAAQ,QAAQ,EAAE,SAAA;AACpC;;;;"}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const logger = require("../../logger.js");
|
|
4
4
|
const server = require("@trpc/server");
|
|
5
|
+
const events = require("./events.js");
|
|
6
|
+
const permissions = require("./permissions.js");
|
|
5
7
|
let _deps = null;
|
|
6
8
|
function setContextDeps(deps) {
|
|
7
9
|
_deps = deps;
|
|
@@ -13,7 +15,11 @@ function getContextDeps() {
|
|
|
13
15
|
async function createContext(opts) {
|
|
14
16
|
const { database } = getContextDeps();
|
|
15
17
|
const req = opts.req;
|
|
16
|
-
const
|
|
18
|
+
const session = req.session;
|
|
19
|
+
if (session?.mfaPending) {
|
|
20
|
+
throw new server.TRPCError({ code: "UNAUTHORIZED" });
|
|
21
|
+
}
|
|
22
|
+
const userId = session?.userId;
|
|
17
23
|
const user = await database.findUserById(userId);
|
|
18
24
|
if (!user || user.isBanned) {
|
|
19
25
|
throw new server.TRPCError({ code: "UNAUTHORIZED" });
|
|
@@ -23,21 +29,58 @@ async function createContext(opts) {
|
|
|
23
29
|
{ $set: { lastOnline: Date.now() } }
|
|
24
30
|
);
|
|
25
31
|
const usernameText = `[${(user.username || "Admin").brightMagenta}]`;
|
|
32
|
+
let _userAbort = null;
|
|
33
|
+
const getUserAbort = () => {
|
|
34
|
+
if (!_userAbort) {
|
|
35
|
+
_userAbort = new AbortController();
|
|
36
|
+
const ac = _userAbort;
|
|
37
|
+
const onInvalidated = (name) => {
|
|
38
|
+
if (name === user.username) ac.abort();
|
|
39
|
+
};
|
|
40
|
+
events.serverEvents.on("userInvalidated", onInvalidated);
|
|
41
|
+
ac.signal.addEventListener("abort", () => {
|
|
42
|
+
events.serverEvents.off("userInvalidated", onInvalidated);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return _userAbort;
|
|
46
|
+
};
|
|
26
47
|
return {
|
|
27
48
|
user,
|
|
49
|
+
req,
|
|
50
|
+
get userAbort() {
|
|
51
|
+
return getUserAbort();
|
|
52
|
+
},
|
|
28
53
|
log: (...args) => logger.default.logp(usernameText, ...args),
|
|
29
54
|
error: (...args) => logger.default.errorp(usernameText, ...args)
|
|
30
55
|
};
|
|
31
56
|
}
|
|
32
|
-
const t = server.initTRPC.context().create(
|
|
57
|
+
const t = server.initTRPC.context().create({
|
|
58
|
+
errorFormatter({ shape, error, path, ctx }) {
|
|
59
|
+
const user = ctx?.user?.username || "?";
|
|
60
|
+
logger.default.errorp(
|
|
61
|
+
`[${user.brightMagenta}]`,
|
|
62
|
+
`${error.code} ${path ?? "?"}: ${error.message}`
|
|
63
|
+
);
|
|
64
|
+
return shape;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
33
67
|
const router = t.router;
|
|
34
68
|
t.procedure;
|
|
35
69
|
const mergeRouters = t.mergeRouters;
|
|
36
|
-
const requireScope = (scope) => t.middleware(({ ctx, next }) => {
|
|
70
|
+
const requireScope = (scope) => t.middleware(async ({ ctx, next }) => {
|
|
37
71
|
if (!ctx.user) throw new server.TRPCError({ code: "UNAUTHORIZED" });
|
|
72
|
+
if (ctx.user.isOwner) return next({ ctx });
|
|
73
|
+
const { database } = getContextDeps();
|
|
74
|
+
const defaults = await database.getDefaultPermissions();
|
|
75
|
+
if (!permissions.userHasScope(ctx.user, scope, defaults)) {
|
|
76
|
+
throw new server.TRPCError({
|
|
77
|
+
code: "FORBIDDEN",
|
|
78
|
+
message: `missing scope: ${scope}`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
38
81
|
return next({ ctx });
|
|
39
82
|
});
|
|
40
|
-
const protectedProcedure = (scope) => t.procedure.use(requireScope());
|
|
83
|
+
const protectedProcedure = (scope) => t.procedure.use(requireScope(scope));
|
|
41
84
|
exports.createContext = createContext;
|
|
42
85
|
exports.getContextDeps = getContextDeps;
|
|
43
86
|
exports.mergeRouters = mergeRouters;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trpc.js","sources":["../../../src/webserver/backend/trpc.ts"],"sourcesContent":["import Logger from '@/logger';\nimport type Omegga from '@omegga/server';\nimport { initTRPC, TRPCError } from '@trpc/server';\nimport type { CreateExpressContextOptions } from '@trpc/server/adapters/express';\nimport type Database from './database';\nimport type { Scope } from './scopes';\nimport type { IStoreUser } from './types';\n\nexport type ContextDeps = {\n database: Database;\n omegga: Omegga;\n};\n\nlet _deps: ContextDeps | null = null;\n\nexport function setContextDeps(deps: ContextDeps) {\n _deps = deps;\n}\n\nexport function getContextDeps(): ContextDeps {\n if (!_deps) throw new Error('tRPC context deps not initialized');\n return _deps;\n}\n\nexport type Context = {\n user: IStoreUser & { _id: string };\n log: (...args: any[]) => void;\n error: (...args: any[]) => void;\n};\n\nexport async function createContext(\n opts: CreateExpressContextOptions,\n): Promise<Context> {\n const { database } = getContextDeps();\n const req = opts.req;\n\n const
|
|
1
|
+
{"version":3,"file":"trpc.js","sources":["../../../src/webserver/backend/trpc.ts"],"sourcesContent":["import Logger from '@/logger';\nimport type Omegga from '@omegga/server';\nimport { initTRPC, TRPCError } from '@trpc/server';\nimport type { CreateExpressContextOptions } from '@trpc/server/adapters/express';\nimport type Database from './database';\nimport { serverEvents } from './events';\nimport { userHasScope } from './permissions';\nimport type { Scope } from './scopes';\nimport type { IStoreUser } from './types';\n\nexport type ContextDeps = {\n database: Database;\n omegga: Omegga;\n};\n\nlet _deps: ContextDeps | null = null;\n\nexport function setContextDeps(deps: ContextDeps) {\n _deps = deps;\n}\n\nexport function getContextDeps(): ContextDeps {\n if (!_deps) throw new Error('tRPC context deps not initialized');\n return _deps;\n}\n\nexport type Context = {\n user: IStoreUser & { _id: string };\n req: import('express').Request;\n userAbort: AbortController;\n log: (...args: any[]) => void;\n error: (...args: any[]) => void;\n};\n\nexport async function createContext(\n opts: CreateExpressContextOptions,\n): Promise<Context> {\n const { database } = getContextDeps();\n const req = opts.req;\n\n const session = (req as any).session;\n if (session?.mfaPending) {\n throw new TRPCError({ code: 'UNAUTHORIZED' });\n }\n const userId = session?.userId;\n const user = await database.findUserById(userId);\n if (!user || user.isBanned) {\n throw new TRPCError({ code: 'UNAUTHORIZED' });\n }\n\n // Update lastOnline (migrated from Socket.IO connect handler)\n await database.stores.users.update<IStoreUser>(\n { _id: user._id },\n { $set: { lastOnline: Date.now() } },\n );\n\n const usernameText = `[${(user.username || 'Admin').brightMagenta}]`;\n\n let _userAbort: AbortController | null = null;\n const getUserAbort = () => {\n if (!_userAbort) {\n _userAbort = new AbortController();\n const ac = _userAbort;\n const onInvalidated = (name: string) => {\n if (name === user.username) ac.abort();\n };\n serverEvents.on('userInvalidated', onInvalidated);\n ac.signal.addEventListener('abort', () => {\n serverEvents.off('userInvalidated', onInvalidated);\n });\n }\n return _userAbort;\n };\n\n return {\n user,\n req,\n get userAbort() {\n return getUserAbort();\n },\n log: (...args: any[]) => Logger.logp(usernameText, ...args),\n error: (...args: any[]) => Logger.errorp(usernameText, ...args),\n };\n}\n\nconst t = initTRPC.context<Context>().create({\n errorFormatter({ shape, error, path, ctx }) {\n const user = (ctx as Context | undefined)?.user?.username || '?';\n Logger.errorp(\n `[${user.brightMagenta}]`,\n `${error.code} ${path ?? '?'}: ${error.message}`,\n );\n return shape;\n },\n});\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\nexport const mergeRouters = t.mergeRouters;\n\nexport const requireScope = (scope: Scope) =>\n t.middleware(async ({ ctx, next }) => {\n if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });\n if (ctx.user.isOwner) return next({ ctx });\n const { database } = getContextDeps();\n const defaults = await database.getDefaultPermissions();\n if (!userHasScope(ctx.user, scope, defaults)) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: `missing scope: ${scope}`,\n });\n }\n return next({ ctx });\n });\n\nexport const protectedProcedure = (scope: Scope) =>\n t.procedure.use(requireScope(scope));\n"],"names":["TRPCError","serverEvents","Logger","initTRPC","userHasScope"],"mappings":";;;;;;AAeA,IAAI,QAA4B;AAEzB,SAAS,eAAe,MAAmB;AAChD,UAAQ;AACV;AAEO,SAAS,iBAA8B;AAC5C,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mCAAmC;AAC/D,SAAO;AACT;AAUA,eAAsB,cACpB,MACkB;AAClB,QAAM,EAAE,SAAA,IAAa,eAAA;AACrB,QAAM,MAAM,KAAK;AAEjB,QAAM,UAAW,IAAY;AAC7B,MAAI,SAAS,YAAY;AACvB,UAAM,IAAIA,OAAAA,UAAU,EAAE,MAAM,gBAAgB;AAAA,EAC9C;AACA,QAAM,SAAS,SAAS;AACxB,QAAM,OAAO,MAAM,SAAS,aAAa,MAAM;AAC/C,MAAI,CAAC,QAAQ,KAAK,UAAU;AAC1B,UAAM,IAAIA,OAAAA,UAAU,EAAE,MAAM,gBAAgB;AAAA,EAC9C;AAGA,QAAM,SAAS,OAAO,MAAM;AAAA,IAC1B,EAAE,KAAK,KAAK,IAAA;AAAA,IACZ,EAAE,MAAM,EAAE,YAAY,KAAK,IAAA,IAAM;AAAA,EAAE;AAGrC,QAAM,eAAe,KAAK,KAAK,YAAY,SAAS,aAAa;AAEjE,MAAI,aAAqC;AACzC,QAAM,eAAe,MAAM;AACzB,QAAI,CAAC,YAAY;AACf,mBAAa,IAAI,gBAAA;AACjB,YAAM,KAAK;AACX,YAAM,gBAAgB,CAAC,SAAiB;AACtC,YAAI,SAAS,KAAK,SAAU,IAAG,MAAA;AAAA,MACjC;AACAC,0BAAa,GAAG,mBAAmB,aAAa;AAChD,SAAG,OAAO,iBAAiB,SAAS,MAAM;AACxCA,4BAAa,IAAI,mBAAmB,aAAa;AAAA,MACnD,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,IAAI,YAAY;AACd,aAAO,aAAA;AAAA,IACT;AAAA,IACA,KAAK,IAAI,SAAgBC,OAAAA,QAAO,KAAK,cAAc,GAAG,IAAI;AAAA,IAC1D,OAAO,IAAI,SAAgBA,OAAAA,QAAO,OAAO,cAAc,GAAG,IAAI;AAAA,EAAA;AAElE;AAEA,MAAM,IAAIC,OAAAA,SAAS,QAAA,EAAmB,OAAO;AAAA,EAC3C,eAAe,EAAE,OAAO,OAAO,MAAM,OAAO;AAC1C,UAAM,OAAQ,KAA6B,MAAM,YAAY;AAC7DD,WAAAA,QAAO;AAAA,MACL,IAAI,KAAK,aAAa;AAAA,MACtB,GAAG,MAAM,IAAI,IAAI,QAAQ,GAAG,KAAK,MAAM,OAAO;AAAA,IAAA;AAEhD,WAAO;AAAA,EACT;AACF,CAAC;AAEM,MAAM,SAAS,EAAE;AACO,EAAE;AAC1B,MAAM,eAAe,EAAE;AAEvB,MAAM,eAAe,CAAC,UAC3B,EAAE,WAAW,OAAO,EAAE,KAAK,WAAW;AACpC,MAAI,CAAC,IAAI,KAAM,OAAM,IAAIF,OAAAA,UAAU,EAAE,MAAM,gBAAgB;AAC3D,MAAI,IAAI,KAAK,gBAAgB,KAAK,EAAE,KAAK;AACzC,QAAM,EAAE,SAAA,IAAa,eAAA;AACrB,QAAM,WAAW,MAAM,SAAS,sBAAA;AAChC,MAAI,CAACI,YAAAA,aAAa,IAAI,MAAM,OAAO,QAAQ,GAAG;AAC5C,UAAM,IAAIJ,OAAAA,UAAU;AAAA,MAClB,MAAM;AAAA,MACN,SAAS,kBAAkB,KAAK;AAAA,IAAA,CACjC;AAAA,EACH;AACA,SAAO,KAAK,EAAE,KAAK;AACrB,CAAC;AAEI,MAAM,qBAAqB,CAAC,UACjC,EAAE,UAAU,IAAI,aAAa,KAAK,CAAC;;;;;;;;"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Web UI Permission System
|
|
2
|
+
|
|
3
|
+
The web UI uses a hierarchical, purely additive permission model. Permissions can only grant access, never deny it.
|
|
4
|
+
|
|
5
|
+
## Data Model
|
|
6
|
+
|
|
7
|
+
Each user has a `PermissionSet`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface PermissionSet {
|
|
11
|
+
root: 'all' | 'read' | 'off';
|
|
12
|
+
domains: Partial<Record<Domain, 'all' | 'read'>>;
|
|
13
|
+
scopes: Partial<Record<Scope, boolean>>;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A server-wide **default** `PermissionSet` serves as a fallback for all users.
|
|
18
|
+
|
|
19
|
+
## Resolution
|
|
20
|
+
|
|
21
|
+
When checking whether a user has a specific scope, the resolver walks four levels in order. The first level that produces a definite answer wins.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
1. Owner bypass - isOwner grants everything
|
|
25
|
+
2. Root level - 'all' grants everything; 'read' grants read-only scopes; 'off' falls through
|
|
26
|
+
3. Domain level - 'all' grants all scopes in that domain; 'read' grants read-only scopes; absent falls through
|
|
27
|
+
4. Scope level - true grants; absent falls through to defaults
|
|
28
|
+
5. Default perms - same root/domain/scope resolution, but with no further fallback
|
|
29
|
+
6. Not granted - false
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `readOnly` flag on each scope definition determines whether "Read Only" mode at root or domain level grants that scope.
|
|
33
|
+
|
|
34
|
+
## Levels
|
|
35
|
+
|
|
36
|
+
### Root
|
|
37
|
+
|
|
38
|
+
| Value | UI Label | Behavior |
|
|
39
|
+
|-------|----------|----------|
|
|
40
|
+
| `all` | All | Grants every scope |
|
|
41
|
+
| `read` | Read Only | Grants only scopes marked `readOnly: true` |
|
|
42
|
+
| `off` | Manual | Falls through to domain and scope checks |
|
|
43
|
+
|
|
44
|
+
### Domain
|
|
45
|
+
|
|
46
|
+
| Value | UI Label | Behavior |
|
|
47
|
+
|-------|----------|----------|
|
|
48
|
+
| `all` | All | Grants every scope in this domain |
|
|
49
|
+
| `read` | Read Only | Grants only `readOnly` scopes in this domain |
|
|
50
|
+
| *(absent)* | Manual | Falls through to individual scope toggles |
|
|
51
|
+
|
|
52
|
+
Domains are purely additive. Setting a domain to "Manual" removes it from the `domains` map rather than storing a deny value.
|
|
53
|
+
|
|
54
|
+
### Scope
|
|
55
|
+
|
|
56
|
+
Permissions are purely additive. `true` grants the scope, absent (or toggled off) falls through to defaults. There is no way to explicitly deny a scope -- toggling a scope off simply removes the user-level override so the default applies.
|
|
57
|
+
|
|
58
|
+
## Domains and Scopes
|
|
59
|
+
|
|
60
|
+
Each scope belongs to exactly one domain and is either read-only (R) or read-write (W).
|
|
61
|
+
|
|
62
|
+
### Chat
|
|
63
|
+
| Scope | R/W | Description |
|
|
64
|
+
|-------|-----|-------------|
|
|
65
|
+
| `chat.send` | W | Send messages in the dashboard chat widget |
|
|
66
|
+
| `chat.recent` | R | View recent chat on the dashboard |
|
|
67
|
+
| `chat.history` | R | Browse past chat logs in the history view |
|
|
68
|
+
| `chat.calendar` | R | Navigate chat by date in the history view |
|
|
69
|
+
|
|
70
|
+
### Player
|
|
71
|
+
| Scope | R/W | Description |
|
|
72
|
+
|-------|-----|-------------|
|
|
73
|
+
| `player.list` | R | View the player list in the players view |
|
|
74
|
+
| `player.get` | R | Inspect player details and history |
|
|
75
|
+
| `player.ban` | W | Ban players from the player inspector |
|
|
76
|
+
| `player.kick` | W | Kick players from the player inspector |
|
|
77
|
+
| `player.unban` | W | Unban players from the player inspector |
|
|
78
|
+
| `player.clearBricks` | W | Clear a player's bricks from the player inspector |
|
|
79
|
+
|
|
80
|
+
### Plugin
|
|
81
|
+
| Scope | R/W | Description |
|
|
82
|
+
|-------|-----|-------------|
|
|
83
|
+
| `plugin.list` | R | View installed plugins in the plugins view |
|
|
84
|
+
| `plugin.get` | R | Inspect plugin details and configuration |
|
|
85
|
+
| `plugin.config` | W | Edit plugin settings in the plugin inspector |
|
|
86
|
+
| `plugin.load` | W | Load plugins from the plugin inspector |
|
|
87
|
+
| `plugin.unload` | W | Unload plugins from the plugin inspector |
|
|
88
|
+
| `plugin.toggle` | W | Enable or disable plugins in the plugins view |
|
|
89
|
+
| `plugin.reloadAll` | W | Reload all plugins from the plugins view |
|
|
90
|
+
|
|
91
|
+
### Server
|
|
92
|
+
| Scope | R/W | Description |
|
|
93
|
+
|-------|-----|-------------|
|
|
94
|
+
| `server.status` | R | View server status on the dashboard and server view |
|
|
95
|
+
| `server.start` | W | Start the server from the server view |
|
|
96
|
+
| `server.stop` | W | Stop the server from the server view |
|
|
97
|
+
| `server.restart` | W | Restart the server from the server view |
|
|
98
|
+
| `server.update.check` | W | Check for server updates in the server view (runs SteamCMD) |
|
|
99
|
+
| `server.update.run` | W | Run server updates from the server view |
|
|
100
|
+
| `server.autorestart.get` | R | View auto-restart settings in the server view |
|
|
101
|
+
| `server.autorestart.set` | W | Change auto-restart settings in the server view |
|
|
102
|
+
| `server.utilization` | R | View CPU, memory, and disk usage on the dashboard |
|
|
103
|
+
|
|
104
|
+
### User
|
|
105
|
+
| Scope | R/W | Description |
|
|
106
|
+
|-------|-----|-------------|
|
|
107
|
+
| `user.list` | R | View web UI user accounts in the users view |
|
|
108
|
+
| `user.create` | W | Create new user accounts in the users view |
|
|
109
|
+
| `user.passwd` | W | Change other users' passwords in the user inspector |
|
|
110
|
+
| `user.ban` | W | Disable or re-enable users in the user inspector |
|
|
111
|
+
| `user.delete` | W | Permanently delete user accounts |
|
|
112
|
+
| `user.permissions` | W | Edit user and default permissions in the users view |
|
|
113
|
+
| `user.readMfa` | R | View MFA status of other users in the user inspector |
|
|
114
|
+
| `user.resetMfa` | W | Reset MFA for other users in the user inspector |
|
|
115
|
+
|
|
116
|
+
### World
|
|
117
|
+
| Scope | R/W | Description |
|
|
118
|
+
|-------|-----|-------------|
|
|
119
|
+
| `world.list` | R | View available worlds in the worlds view |
|
|
120
|
+
| `world.active` | R | See which world is currently loaded |
|
|
121
|
+
| `world.next` | R | See which world will load next |
|
|
122
|
+
| `world.revisions` | R | View world save revisions in the world inspector |
|
|
123
|
+
| `world.meta` | R | View world metadata in the world inspector |
|
|
124
|
+
| `world.load` | W | Load worlds from the world inspector |
|
|
125
|
+
| `world.use` | W | Set the default world in the worlds view |
|
|
126
|
+
| `world.save` | W | Save the current world from the worlds or server view |
|
|
127
|
+
| `world.create` | W | Create new worlds in the worlds view |
|
|
128
|
+
|
|
129
|
+
## Self-Service
|
|
130
|
+
|
|
131
|
+
All authenticated users can access the `/account` page regardless of permissions. This page shows the user's own account info, MFA management (TOTP, passkeys, recovery codes), and password change.
|
|
132
|
+
|
|
133
|
+
The `user.self` endpoint (scoped to `session.info`) returns the current user's data without requiring `user.list`. The `user.passwd` endpoint allows any user to change their own password (requires current password); changing another user's password requires the `user.passwd` scope. MFA management endpoints (`mfa.*`) are scoped to `session.info` and require password verification for sensitive operations (TOTP setup/disable, passkey removal, recovery code generation).
|
|
134
|
+
|
|
135
|
+
## Enforcement
|
|
136
|
+
|
|
137
|
+
### Backend
|
|
138
|
+
|
|
139
|
+
Every tRPC endpoint is wrapped with `protectedProcedure(scope)`, which runs `requireScope` middleware. The middleware:
|
|
140
|
+
|
|
141
|
+
1. Rejects unauthenticated requests (`UNAUTHORIZED`)
|
|
142
|
+
2. Allows owners unconditionally
|
|
143
|
+
3. Resolves the scope against the user's permissions + server defaults
|
|
144
|
+
4. Rejects with `FORBIDDEN` if not granted
|
|
145
|
+
|
|
146
|
+
Subscription endpoints (like `server.onStatus`, `chat.onMessage`) share the scope of their corresponding query endpoint rather than having separate scopes.
|
|
147
|
+
|
|
148
|
+
### Frontend
|
|
149
|
+
|
|
150
|
+
On login, the session response includes `resolvedScopes` -- a flat `Record<string, boolean>` with every scope pre-resolved. The frontend stores this in a nanostore (`$resolvedScopes`).
|
|
151
|
+
|
|
152
|
+
- `useHasScope(...scopes)` -- returns true if the user has all specified scopes
|
|
153
|
+
- `useHasAnyScope(...scopes)` -- returns true if the user has any of the specified scopes
|
|
154
|
+
- `useRequireScope(scope)` -- redirects to `/` if the user lacks the scope
|
|
155
|
+
|
|
156
|
+
#### Sidenav Visibility
|
|
157
|
+
|
|
158
|
+
Each sidenav link is gated by a specific scope:
|
|
159
|
+
|
|
160
|
+
| Link | Required Scope | Always Visible |
|
|
161
|
+
|------|---------------|----------------|
|
|
162
|
+
| Dashboard | -- | Yes |
|
|
163
|
+
| Worlds | `world.list` | No |
|
|
164
|
+
| History | `chat.history` | No |
|
|
165
|
+
| Plugins | `plugin.list` | No |
|
|
166
|
+
| Players | `player.list` | No |
|
|
167
|
+
| Server | `server.status` | No |
|
|
168
|
+
| Users | `user.list` | No |
|
|
169
|
+
| Account | -- | Yes |
|
|
170
|
+
|
|
171
|
+
#### View Access
|
|
172
|
+
|
|
173
|
+
Each view redirects to the dashboard if the user lacks its required scope:
|
|
174
|
+
- Worlds requires `world.list`
|
|
175
|
+
- History requires `chat.history`
|
|
176
|
+
- Plugins requires `plugin.list`
|
|
177
|
+
- Players requires `player.list`
|
|
178
|
+
- Server requires `server.status`
|
|
179
|
+
- Users requires `user.list`
|
|
180
|
+
|
|
181
|
+
Within a view, individual buttons and controls are conditionally rendered based on their specific scopes. Admin actions in the user inspector (change password, disable, delete, reset MFA) are shown in an actions widget gated by their respective scopes.
|
|
182
|
+
|
|
183
|
+
The backend remains the source of truth -- frontend checks are for UX only.
|
|
184
|
+
|
|
185
|
+
## Privilege Escalation Prevention
|
|
186
|
+
|
|
187
|
+
When a non-owner user edits another user's permissions, the backend checks that the editor is not granting scopes they don't have themselves. The mutation resolves all scopes in the proposed `PermissionSet` against defaults and rejects any scope the editor lacks.
|
|
188
|
+
|
|
189
|
+
## Storage
|
|
190
|
+
|
|
191
|
+
User permissions are stored in the `users` NeDB store as part of each user document. Default permissions are stored in the `server` NeDB store as a `{ type: 'defaultPermissions' }` document.
|
|
192
|
+
|
|
193
|
+
New users are created with `EMPTY_PERMISSIONS` (`root: 'off', domains: {}, scopes: {}`), which means all access is determined by server defaults until explicitly configured.
|