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.
Files changed (95) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/bin/omegga +1 -1
  3. package/dist/cli/terminal.js +46 -0
  4. package/dist/cli/terminal.js.map +1 -1
  5. package/dist/omegga/commandInjector.js +13 -6
  6. package/dist/omegga/commandInjector.js.map +1 -1
  7. package/dist/omegga/player.js +2 -2
  8. package/dist/omegga/player.js.map +1 -1
  9. package/dist/plugin.js.map +1 -1
  10. package/dist/webserver/backend/database.js +219 -7
  11. package/dist/webserver/backend/database.js.map +1 -1
  12. package/dist/webserver/backend/events.js +1 -1
  13. package/dist/webserver/backend/events.js.map +1 -1
  14. package/dist/webserver/backend/index.js +120 -2
  15. package/dist/webserver/backend/index.js.map +1 -1
  16. package/dist/webserver/backend/permissions.js +74 -0
  17. package/dist/webserver/backend/permissions.js.map +1 -0
  18. package/dist/webserver/backend/router/chat.js +9 -7
  19. package/dist/webserver/backend/router/chat.js.map +1 -1
  20. package/dist/webserver/backend/router/index.js +3 -1
  21. package/dist/webserver/backend/router/index.js.map +1 -1
  22. package/dist/webserver/backend/router/mfa.js +162 -0
  23. package/dist/webserver/backend/router/mfa.js.map +1 -0
  24. package/dist/webserver/backend/router/player.js +7 -6
  25. package/dist/webserver/backend/router/player.js.map +1 -1
  26. package/dist/webserver/backend/router/plugin.js +14 -10
  27. package/dist/webserver/backend/router/plugin.js.map +1 -1
  28. package/dist/webserver/backend/router/role.js +2 -1
  29. package/dist/webserver/backend/router/role.js.map +1 -1
  30. package/dist/webserver/backend/router/server.js +85 -71
  31. package/dist/webserver/backend/router/server.js.map +1 -1
  32. package/dist/webserver/backend/router/session.js +11 -2
  33. package/dist/webserver/backend/router/session.js.map +1 -1
  34. package/dist/webserver/backend/router/user.js +175 -13
  35. package/dist/webserver/backend/router/user.js.map +1 -1
  36. package/dist/webserver/backend/router/world.js +10 -9
  37. package/dist/webserver/backend/router/world.js.map +1 -1
  38. package/dist/webserver/backend/scopes.js +303 -0
  39. package/dist/webserver/backend/scopes.js.map +1 -0
  40. package/dist/webserver/backend/totp.js +32 -0
  41. package/dist/webserver/backend/totp.js.map +1 -0
  42. package/dist/webserver/backend/trpc.js +47 -4
  43. package/dist/webserver/backend/trpc.js.map +1 -1
  44. package/doc/permissions.md +193 -0
  45. package/package.json +7 -2
  46. package/public/app.html +932 -526
  47. package/public/assets/IconShieldCheck-Bjlm8mX2.js +2 -0
  48. package/public/assets/IconShieldCheck-Bjlm8mX2.js.map +1 -0
  49. package/public/assets/IconWorld-XmQNg6JI.js +2 -0
  50. package/public/assets/IconWorld-XmQNg6JI.js.map +1 -0
  51. package/public/assets/IconWorldPlus-CxGGv2eD.js +2 -0
  52. package/public/assets/IconWorldPlus-CxGGv2eD.js.map +1 -0
  53. package/public/assets/app-DMr_-wik.js +7 -0
  54. package/public/assets/app-DMr_-wik.js.map +1 -0
  55. package/public/assets/auth-CEYiizOt.js +2 -0
  56. package/public/assets/auth-CEYiizOt.js.map +1 -0
  57. package/public/assets/index-BDkarAkX.js +3 -0
  58. package/public/assets/index-BDkarAkX.js.map +1 -0
  59. package/public/assets/{index-2cgXLhrt.js → index-DEL-PD_8.js} +2 -2
  60. package/public/assets/{index-2cgXLhrt.js.map → index-DEL-PD_8.js.map} +1 -1
  61. package/public/assets/index-D_STNela.js +2 -0
  62. package/public/assets/index-D_STNela.js.map +1 -0
  63. package/public/assets/index-Dkh3HiBz.js +4 -0
  64. package/public/assets/index-Dkh3HiBz.js.map +1 -0
  65. package/public/assets/shared/@components-CgsSYMe6.js +2 -0
  66. package/public/assets/shared/@components-CgsSYMe6.js.map +1 -0
  67. package/public/assets/shared/@hooks-DT93N7wd.js +2 -0
  68. package/public/assets/shared/{@hooks-Ch_hJYh0.js.map → @hooks-DT93N7wd.js.map} +1 -1
  69. package/public/assets/shared/@tabler_icons-react-DrVFX3uL.js +3 -0
  70. package/public/assets/shared/{@tabler_icons-react-iQk11eD_.js.map → @tabler_icons-react-DrVFX3uL.js.map} +1 -1
  71. package/public/assets/shared/{wouter-CUC1caxu.js → wouter-CNISJ5i6.js} +2 -2
  72. package/public/assets/shared/wouter-CNISJ5i6.js.map +1 -0
  73. package/public/assets/useHasScope-BOdFktko.js +2 -0
  74. package/public/assets/useHasScope-BOdFktko.js.map +1 -0
  75. package/public/auth.html +97 -33
  76. package/templates/safe-ts/omegga.d.ts +1 -1
  77. package/public/assets/IconArrowRight-BuVxhqKX.js +0 -2
  78. package/public/assets/IconArrowRight-BuVxhqKX.js.map +0 -1
  79. package/public/assets/IconWorld-B6GK5qpx.js +0 -2
  80. package/public/assets/IconWorld-B6GK5qpx.js.map +0 -1
  81. package/public/assets/IconWorldPlus-Dkos72Ws.js +0 -2
  82. package/public/assets/IconWorldPlus-Dkos72Ws.js.map +0 -1
  83. package/public/assets/app-CLZ-5FIi.js +0 -6
  84. package/public/assets/app-CLZ-5FIi.js.map +0 -1
  85. package/public/assets/auth-6N37o421.js +0 -2
  86. package/public/assets/auth-6N37o421.js.map +0 -1
  87. package/public/assets/index-DLZ8-XzO.js +0 -4
  88. package/public/assets/index-DLZ8-XzO.js.map +0 -1
  89. package/public/assets/index-DY750sgZ.js +0 -2
  90. package/public/assets/index-DY750sgZ.js.map +0 -1
  91. package/public/assets/shared/@components-DPHARroA.js +0 -2
  92. package/public/assets/shared/@components-DPHARroA.js.map +0 -1
  93. package/public/assets/shared/@hooks-Ch_hJYh0.js +0 -2
  94. package/public/assets/shared/@tabler_icons-react-iQk11eD_.js +0 -3
  95. 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 userId = req.session?.userId;
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 userId = (req as any).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 return {\n user,\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\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\nexport const mergeRouters = t.mergeRouters;\n\nexport const requireScope = (scope: Scope) =>\n t.middleware(({ ctx, next }) => {\n if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });\n // TODO: check ctx.user.roles against scope\n // For now, all authenticated users pass\n return next({ ctx });\n });\n\nexport const protectedProcedure = (scope: Scope) =>\n t.procedure.use(requireScope(scope));\n"],"names":["TRPCError","Logger","initTRPC"],"mappings":";;;;AAaA,IAAI,QAA4B;AAEzB,SAAS,eAAe,MAAmB;AAChD,UAAQ;AACV;AAEO,SAAS,iBAA8B;AAC5C,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mCAAmC;AAC/D,SAAO;AACT;AAQA,eAAsB,cACpB,MACkB;AAClB,QAAM,EAAE,SAAA,IAAa,eAAA;AACrB,QAAM,MAAM,KAAK;AAEjB,QAAM,SAAU,IAAY,SAAS;AACrC,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,SAAO;AAAA,IACL;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,OAAA;AAE/B,MAAM,SAAS,EAAE;AACO,EAAE;AAC1B,MAAM,eAAe,EAAE;AAEvB,MAAM,eAAe,CAAC,UAC3B,EAAE,WAAW,CAAC,EAAE,KAAK,WAAW;AAC9B,MAAI,CAAC,IAAI,KAAM,OAAM,IAAIF,OAAAA,UAAU,EAAE,MAAM,gBAAgB;AAG3D,SAAO,KAAK,EAAE,KAAK;AACrB,CAAC;AAEI,MAAM,qBAAqB,CAAC,UACjC,EAAE,UAAU,IAAI,aAAkB,CAAC;;;;;;;;"}
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.