@axium/sysadmin 0.1.3 → 0.2.1
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/db.json +13 -0
- package/dist/client/info.js +22 -0
- package/dist/common.d.ts +59 -0
- package/dist/common.js +6 -0
- package/dist/info.d.ts +21 -0
- package/dist/info.js +12 -0
- package/dist/server/api.js +38 -0
- package/locales/en.json +19 -4
- package/package.json +1 -1
- package/routes/sysadmin/systems/[id]/+page.svelte +104 -7
- package/routes/sysadmin/systems/[id]/+page.ts +5 -2
- package/routes/sysadmin/users/[id]/+page.svelte +48 -4
- package/routes/sysadmin/users/[id]/+page.ts +5 -2
package/db.json
CHANGED
|
@@ -68,6 +68,19 @@
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"delta": true,
|
|
74
|
+
"alter_tables": {
|
|
75
|
+
"systems": {
|
|
76
|
+
"add_columns": {
|
|
77
|
+
"connectedUserId": { "type": "uuid", "references": "system_users.id", "onDelete": "set null" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"add_indexes": {
|
|
82
|
+
"systems_connectedUserId": { "on": "systems", "columns": ["connectedUserId"] }
|
|
83
|
+
}
|
|
71
84
|
}
|
|
72
85
|
],
|
|
73
86
|
"wipe": ["systems", "system_users", "acl.systems"],
|
package/dist/client/info.js
CHANGED
|
@@ -129,6 +129,27 @@ function memory() {
|
|
|
129
129
|
total,
|
|
130
130
|
used: total - available,
|
|
131
131
|
};
|
|
132
|
+
try {
|
|
133
|
+
const dmiMemoryInfo = execFileSync('sudo', ['-n', 'dmidecode', '-t', '17'], {
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
136
|
+
});
|
|
137
|
+
for (const match of dmiMemoryInfo.matchAll(/^\s*(?:Configured Memory|Configured Clock)?\s*Speed:\s*(\d+)\s*MT\/s/gim)) {
|
|
138
|
+
memory.speed = Math.max(memory.speed, Number(match[1]));
|
|
139
|
+
}
|
|
140
|
+
const populated = (field) => {
|
|
141
|
+
for (const [, match] of dmiMemoryInfo.matchAll(new RegExp(`^[ \\t]*${field}:[ \\t]*(.+)$`, 'gim'))) {
|
|
142
|
+
const value = match.trim();
|
|
143
|
+
if (value && value !== 'Unknown')
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
memory.formFactor = populated('Form Factor');
|
|
148
|
+
memory.type = populated('Type');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// dmidecode missing, not root, or sudo needs a password
|
|
152
|
+
}
|
|
132
153
|
const swapTotal = info.SwapTotal ?? 0n;
|
|
133
154
|
if (swapTotal > 0n)
|
|
134
155
|
memory.swap = { total: swapTotal, used: swapTotal - (info.SwapFree ?? 0n) };
|
|
@@ -179,5 +200,6 @@ export function systemInfo() {
|
|
|
179
200
|
uptime: os.uptime(),
|
|
180
201
|
version: os.version(),
|
|
181
202
|
hostname: os.hostname(),
|
|
203
|
+
user: os.userInfo(),
|
|
182
204
|
};
|
|
183
205
|
}
|
package/dist/common.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare const SystemInit: z.ZodObject<{
|
|
|
6
6
|
name: z.ZodString;
|
|
7
7
|
hostname: z.ZodString;
|
|
8
8
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
9
|
+
connectedUserId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
9
10
|
}, z.core.$strip>;
|
|
10
11
|
export interface SystemInit extends z.infer<typeof SystemInit> {
|
|
11
12
|
}
|
|
@@ -15,6 +16,7 @@ export declare const System: z.ZodObject<{
|
|
|
15
16
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
16
17
|
id: z.ZodUUID;
|
|
17
18
|
userId: z.ZodUUID;
|
|
19
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
18
20
|
isShared: z.ZodBoolean;
|
|
19
21
|
acl: z.ZodArray<z.ZodObject<{
|
|
20
22
|
itemId: z.ZodUUID;
|
|
@@ -68,6 +70,7 @@ declare const SysadminAPI: {
|
|
|
68
70
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
69
71
|
id: z.ZodUUID;
|
|
70
72
|
userId: z.ZodUUID;
|
|
73
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
71
74
|
isShared: z.ZodBoolean;
|
|
72
75
|
acl: z.ZodArray<z.ZodObject<{
|
|
73
76
|
itemId: z.ZodUUID;
|
|
@@ -95,12 +98,14 @@ declare const SysadminAPI: {
|
|
|
95
98
|
name: z.ZodString;
|
|
96
99
|
hostname: z.ZodString;
|
|
97
100
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
101
|
+
connectedUserId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
98
102
|
}, z.core.$strip>, z.ZodObject<{
|
|
99
103
|
name: z.ZodString;
|
|
100
104
|
hostname: z.ZodString;
|
|
101
105
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
102
106
|
id: z.ZodUUID;
|
|
103
107
|
userId: z.ZodUUID;
|
|
108
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
104
109
|
isShared: z.ZodBoolean;
|
|
105
110
|
acl: z.ZodArray<z.ZodObject<{
|
|
106
111
|
itemId: z.ZodUUID;
|
|
@@ -149,6 +154,7 @@ declare const SysadminAPI: {
|
|
|
149
154
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
150
155
|
id: z.ZodUUID;
|
|
151
156
|
userId: z.ZodUUID;
|
|
157
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
152
158
|
isShared: z.ZodBoolean;
|
|
153
159
|
acl: z.ZodArray<z.ZodObject<{
|
|
154
160
|
itemId: z.ZodUUID;
|
|
@@ -176,12 +182,14 @@ declare const SysadminAPI: {
|
|
|
176
182
|
name: z.ZodString;
|
|
177
183
|
hostname: z.ZodString;
|
|
178
184
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
185
|
+
connectedUserId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
179
186
|
}, z.core.$strip>, z.ZodObject<{
|
|
180
187
|
name: z.ZodString;
|
|
181
188
|
hostname: z.ZodString;
|
|
182
189
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
183
190
|
id: z.ZodUUID;
|
|
184
191
|
userId: z.ZodUUID;
|
|
192
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
185
193
|
isShared: z.ZodBoolean;
|
|
186
194
|
acl: z.ZodArray<z.ZodObject<{
|
|
187
195
|
itemId: z.ZodUUID;
|
|
@@ -211,6 +219,7 @@ declare const SysadminAPI: {
|
|
|
211
219
|
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
212
220
|
id: z.ZodUUID;
|
|
213
221
|
userId: z.ZodUUID;
|
|
222
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
214
223
|
isShared: z.ZodBoolean;
|
|
215
224
|
acl: z.ZodArray<z.ZodObject<{
|
|
216
225
|
itemId: z.ZodUUID;
|
|
@@ -258,6 +267,38 @@ declare const SysadminAPI: {
|
|
|
258
267
|
userId: z.ZodUUID;
|
|
259
268
|
}, z.core.$strip>;
|
|
260
269
|
};
|
|
270
|
+
readonly 'sysadmin/users/:id/systems': {
|
|
271
|
+
readonly GET: z.ZodArray<z.ZodObject<{
|
|
272
|
+
name: z.ZodString;
|
|
273
|
+
hostname: z.ZodString;
|
|
274
|
+
type: z.ZodDefault<z.ZodLiteral<"server" | "pc" | "laptop">>;
|
|
275
|
+
id: z.ZodUUID;
|
|
276
|
+
userId: z.ZodUUID;
|
|
277
|
+
connectedUserId: z.ZodNullable<z.ZodUUID>;
|
|
278
|
+
isShared: z.ZodBoolean;
|
|
279
|
+
acl: z.ZodArray<z.ZodObject<{
|
|
280
|
+
itemId: z.ZodUUID;
|
|
281
|
+
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
282
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
283
|
+
tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
284
|
+
user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
285
|
+
id: z.ZodUUID;
|
|
286
|
+
name: z.ZodString;
|
|
287
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
288
|
+
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
289
|
+
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
290
|
+
debug: z.ZodDefault<z.ZodBoolean>;
|
|
291
|
+
}, z.core.$strip>>>;
|
|
292
|
+
roles: z.ZodArray<z.ZodString>;
|
|
293
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
294
|
+
registeredAt: z.ZodCoercedDate<unknown>;
|
|
295
|
+
isAdmin: z.ZodOptional<z.ZodBoolean>;
|
|
296
|
+
isSuspended: z.ZodOptional<z.ZodBoolean>;
|
|
297
|
+
}, z.core.$strip>>>;
|
|
298
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
299
|
+
}, z.core.$catchall<z.ZodBoolean>>>;
|
|
300
|
+
}, z.core.$strip>>;
|
|
301
|
+
};
|
|
261
302
|
};
|
|
262
303
|
type SysadminAPI = typeof SysadminAPI;
|
|
263
304
|
declare module '@axium/core/api' {
|
|
@@ -279,6 +320,8 @@ declare const SysadminClientToServer: {
|
|
|
279
320
|
}, z.core.$strip>>;
|
|
280
321
|
memory: z.ZodObject<{
|
|
281
322
|
speed: z.ZodInt;
|
|
323
|
+
formFactor: z.ZodOptional<z.ZodString>;
|
|
324
|
+
type: z.ZodOptional<z.ZodString>;
|
|
282
325
|
swap: z.ZodOptional<z.ZodObject<{
|
|
283
326
|
total: z.ZodCoercedBigInt<unknown>;
|
|
284
327
|
used: z.ZodCoercedBigInt<unknown>;
|
|
@@ -307,6 +350,13 @@ declare const SysadminClientToServer: {
|
|
|
307
350
|
uptime: z.ZodNumber;
|
|
308
351
|
version: z.ZodString;
|
|
309
352
|
hostname: z.ZodString;
|
|
353
|
+
user: z.ZodObject<{
|
|
354
|
+
username: z.ZodString;
|
|
355
|
+
uid: z.ZodInt;
|
|
356
|
+
gid: z.ZodInt;
|
|
357
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
358
|
+
homedir: z.ZodString;
|
|
359
|
+
}, z.core.$strip>;
|
|
310
360
|
}, z.core.$strip>>]];
|
|
311
361
|
readonly 'sysadmin:ping': readonly [readonly [z.ZodArray<z.ZodObject<{
|
|
312
362
|
hostname: z.ZodString;
|
|
@@ -328,6 +378,8 @@ declare const SysadminServerToClient: {
|
|
|
328
378
|
}, z.core.$strip>>;
|
|
329
379
|
memory: z.ZodObject<{
|
|
330
380
|
speed: z.ZodInt;
|
|
381
|
+
formFactor: z.ZodOptional<z.ZodString>;
|
|
382
|
+
type: z.ZodOptional<z.ZodString>;
|
|
331
383
|
swap: z.ZodOptional<z.ZodObject<{
|
|
332
384
|
total: z.ZodCoercedBigInt<unknown>;
|
|
333
385
|
used: z.ZodCoercedBigInt<unknown>;
|
|
@@ -356,6 +408,13 @@ declare const SysadminServerToClient: {
|
|
|
356
408
|
uptime: z.ZodNumber;
|
|
357
409
|
version: z.ZodString;
|
|
358
410
|
hostname: z.ZodString;
|
|
411
|
+
user: z.ZodObject<{
|
|
412
|
+
username: z.ZodString;
|
|
413
|
+
uid: z.ZodInt;
|
|
414
|
+
gid: z.ZodInt;
|
|
415
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
416
|
+
homedir: z.ZodString;
|
|
417
|
+
}, z.core.$strip>;
|
|
359
418
|
}, z.core.$strip>]];
|
|
360
419
|
readonly 'sysadmin:ping': readonly [readonly [z.ZodObject<{
|
|
361
420
|
hostname: z.ZodString;
|
package/dist/common.js
CHANGED
|
@@ -13,10 +13,13 @@ export const SystemInit = z.object({
|
|
|
13
13
|
name: z.string().nonempty().max(250),
|
|
14
14
|
hostname: z.string().nonempty().max(250),
|
|
15
15
|
type: SystemType.default('server'),
|
|
16
|
+
/** The connected system user, if any. `null` disconnects. */
|
|
17
|
+
connectedUserId: z.uuid().nullish(),
|
|
16
18
|
});
|
|
17
19
|
export const System = SystemInit.extend({
|
|
18
20
|
id: z.uuid(),
|
|
19
21
|
userId: z.uuid(),
|
|
22
|
+
connectedUserId: z.uuid().nullable(),
|
|
20
23
|
isShared: z.boolean(),
|
|
21
24
|
acl: AccessControl.array(),
|
|
22
25
|
});
|
|
@@ -51,6 +54,9 @@ const SysadminAPI = {
|
|
|
51
54
|
PATCH: [SystemUserInit, SystemUser],
|
|
52
55
|
DELETE: SystemUser,
|
|
53
56
|
},
|
|
57
|
+
'sysadmin/users/:id/systems': {
|
|
58
|
+
GET: System.array(),
|
|
59
|
+
},
|
|
54
60
|
};
|
|
55
61
|
Object.assign($API, SysadminAPI);
|
|
56
62
|
const SysadminClientToServer = {
|
package/dist/info.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserInfo } from 'node:os';
|
|
1
2
|
import * as z from 'zod';
|
|
2
3
|
export declare const TotalUsed: z.ZodObject<{
|
|
3
4
|
total: z.ZodCoercedBigInt<unknown>;
|
|
@@ -22,6 +23,8 @@ export interface GPU extends z.infer<typeof GPU> {
|
|
|
22
23
|
}
|
|
23
24
|
export declare const Memory: z.ZodObject<{
|
|
24
25
|
speed: z.ZodInt;
|
|
26
|
+
formFactor: z.ZodOptional<z.ZodString>;
|
|
27
|
+
type: z.ZodOptional<z.ZodString>;
|
|
25
28
|
swap: z.ZodOptional<z.ZodObject<{
|
|
26
29
|
total: z.ZodCoercedBigInt<unknown>;
|
|
27
30
|
used: z.ZodCoercedBigInt<unknown>;
|
|
@@ -48,6 +51,15 @@ export declare const NetworkInterface: z.ZodObject<{
|
|
|
48
51
|
}, z.core.$strip>;
|
|
49
52
|
export interface NetworkInterface extends z.infer<typeof NetworkInterface> {
|
|
50
53
|
}
|
|
54
|
+
export declare const SystemInfoUser: z.ZodObject<{
|
|
55
|
+
username: z.ZodString;
|
|
56
|
+
uid: z.ZodInt;
|
|
57
|
+
gid: z.ZodInt;
|
|
58
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
59
|
+
homedir: z.ZodString;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
export interface SystemInfoUser extends z.infer<typeof SystemInfoUser>, UserInfo<string> {
|
|
62
|
+
}
|
|
51
63
|
export declare const SystemInfo: z.ZodObject<{
|
|
52
64
|
cpus: z.ZodArray<z.ZodObject<{
|
|
53
65
|
model: z.ZodString;
|
|
@@ -62,6 +74,8 @@ export declare const SystemInfo: z.ZodObject<{
|
|
|
62
74
|
}, z.core.$strip>>;
|
|
63
75
|
memory: z.ZodObject<{
|
|
64
76
|
speed: z.ZodInt;
|
|
77
|
+
formFactor: z.ZodOptional<z.ZodString>;
|
|
78
|
+
type: z.ZodOptional<z.ZodString>;
|
|
65
79
|
swap: z.ZodOptional<z.ZodObject<{
|
|
66
80
|
total: z.ZodCoercedBigInt<unknown>;
|
|
67
81
|
used: z.ZodCoercedBigInt<unknown>;
|
|
@@ -90,6 +104,13 @@ export declare const SystemInfo: z.ZodObject<{
|
|
|
90
104
|
uptime: z.ZodNumber;
|
|
91
105
|
version: z.ZodString;
|
|
92
106
|
hostname: z.ZodString;
|
|
107
|
+
user: z.ZodObject<{
|
|
108
|
+
username: z.ZodString;
|
|
109
|
+
uid: z.ZodInt;
|
|
110
|
+
gid: z.ZodInt;
|
|
111
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
112
|
+
homedir: z.ZodString;
|
|
113
|
+
}, z.core.$strip>;
|
|
93
114
|
}, z.core.$strip>;
|
|
94
115
|
export interface SystemInfo extends z.infer<typeof SystemInfo> {
|
|
95
116
|
}
|
package/dist/info.js
CHANGED
|
@@ -17,6 +17,10 @@ export const Memory = z.object({
|
|
|
17
17
|
...TotalUsed.shape,
|
|
18
18
|
/** Memory speed in MT/s */
|
|
19
19
|
speed: z.int().nonnegative(),
|
|
20
|
+
/** Form factor, e.g. 'DIMM' or 'SODIMM' */
|
|
21
|
+
formFactor: z.string().optional(),
|
|
22
|
+
/** Memory type, e.g. 'DDR4' or 'DDR5' */
|
|
23
|
+
type: z.string().optional(),
|
|
20
24
|
/** Only available when swap is in use */
|
|
21
25
|
swap: TotalUsed.optional(),
|
|
22
26
|
});
|
|
@@ -32,6 +36,13 @@ export const NetworkInterface = z.object({
|
|
|
32
36
|
connection: z.string().optional(),
|
|
33
37
|
speed: z.number().nonnegative().optional(),
|
|
34
38
|
});
|
|
39
|
+
export const SystemInfoUser = z.object({
|
|
40
|
+
username: z.string(),
|
|
41
|
+
uid: z.int(),
|
|
42
|
+
gid: z.int(),
|
|
43
|
+
shell: z.string().nullable(),
|
|
44
|
+
homedir: z.string(),
|
|
45
|
+
});
|
|
35
46
|
export const SystemInfo = z.object({
|
|
36
47
|
cpus: CPU.array(),
|
|
37
48
|
gpus: GPU.array(),
|
|
@@ -53,6 +64,7 @@ export const SystemInfo = z.object({
|
|
|
53
64
|
/** Kernel version, e.g. `#1 SMP PREEMPT_DYNAMIC ...` */
|
|
54
65
|
version: z.string(),
|
|
55
66
|
hostname: z.string(),
|
|
67
|
+
user: SystemInfoUser,
|
|
56
68
|
});
|
|
57
69
|
// OS //
|
|
58
70
|
// User //
|
package/dist/server/api.js
CHANGED
|
@@ -8,6 +8,21 @@ import { SystemInit, SystemUserInit } from '../common.js';
|
|
|
8
8
|
function parseSystem(system, isShared) {
|
|
9
9
|
return Object.assign({ acl: [] }, system, { isShared });
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Ensure the requester owns the system user they are connecting a system to.
|
|
13
|
+
* No-op when `connectedUserId` is unset (`undefined` leaves it unchanged, `null` disconnects).
|
|
14
|
+
*/
|
|
15
|
+
async function checkConnectedUser(request, connectedUserId) {
|
|
16
|
+
if (!connectedUserId)
|
|
17
|
+
return;
|
|
18
|
+
const systemUser = await database
|
|
19
|
+
.selectFrom('system_users')
|
|
20
|
+
.select('userId')
|
|
21
|
+
.where('id', '=', connectedUserId)
|
|
22
|
+
.executeTakeFirstOrThrow()
|
|
23
|
+
.catch(withError('Could not get system user', 404));
|
|
24
|
+
await checkAuthForUser(request, systemUser.userId);
|
|
25
|
+
}
|
|
11
26
|
addRoute({
|
|
12
27
|
path: '/api/users/:id/sysadmin/systems',
|
|
13
28
|
params: { id: z.uuid() },
|
|
@@ -25,6 +40,7 @@ addRoute({
|
|
|
25
40
|
async PUT(request, { id: userId }) {
|
|
26
41
|
const init = await parseBody(request, SystemInit);
|
|
27
42
|
await checkAuthForUser(request, userId);
|
|
43
|
+
await checkConnectedUser(request, init.connectedUserId);
|
|
28
44
|
return parseSystem(await database
|
|
29
45
|
.insertInto('systems')
|
|
30
46
|
.values({ ...init, userId })
|
|
@@ -43,6 +59,7 @@ addRoute({
|
|
|
43
59
|
async PATCH(request, { id }) {
|
|
44
60
|
const init = await parseBody(request, SystemInit);
|
|
45
61
|
const { fromACL } = await authRequestForItem(request, 'systems', id, { edit: true });
|
|
62
|
+
await checkConnectedUser(request, init.connectedUserId);
|
|
46
63
|
const system = await database
|
|
47
64
|
.updateTable('systems')
|
|
48
65
|
.set(init)
|
|
@@ -134,3 +151,24 @@ addRoute({
|
|
|
134
151
|
.catch(withError('Could not delete system user'));
|
|
135
152
|
},
|
|
136
153
|
});
|
|
154
|
+
addRoute({
|
|
155
|
+
path: '/api/sysadmin/users/:id/systems',
|
|
156
|
+
params: { id: z.uuid() },
|
|
157
|
+
async GET(request, { id }) {
|
|
158
|
+
const user = await database
|
|
159
|
+
.selectFrom('system_users')
|
|
160
|
+
.select('userId')
|
|
161
|
+
.where('id', '=', id)
|
|
162
|
+
.executeTakeFirstOrThrow()
|
|
163
|
+
.catch(withError('Could not get system user', 404));
|
|
164
|
+
await checkAuthForUser(request, user.userId);
|
|
165
|
+
const systems = await database
|
|
166
|
+
.selectFrom('systems')
|
|
167
|
+
.selectAll()
|
|
168
|
+
.select(acl.from('systems'))
|
|
169
|
+
.where('connectedUserId', '=', id)
|
|
170
|
+
.execute()
|
|
171
|
+
.catch(withError('Could not get systems'));
|
|
172
|
+
return systems.map(system => parseSystem(system, false));
|
|
173
|
+
},
|
|
174
|
+
});
|
package/locales/en.json
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"online": "Online",
|
|
15
15
|
"offline": "Offline",
|
|
16
16
|
"back_to_main": "Back to System Administration",
|
|
17
|
-
"back_to_systems": "Back to Systems",
|
|
18
|
-
"back_to_users": "Back to System Users",
|
|
19
17
|
"SystemInitDialog": {
|
|
20
18
|
"submit": "Add System",
|
|
21
19
|
"save": "Save",
|
|
@@ -42,7 +40,9 @@
|
|
|
42
40
|
"toast_deleted": "User deleted"
|
|
43
41
|
},
|
|
44
42
|
"user": {
|
|
45
|
-
"page_title": "{name} — System Users"
|
|
43
|
+
"page_title": "{name} — System Users",
|
|
44
|
+
"systems": "Connected Systems",
|
|
45
|
+
"no_systems": "No systems are connected to this user."
|
|
46
46
|
},
|
|
47
47
|
"system_type": {
|
|
48
48
|
"server": "Server",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"cpu_cores": "{count} cores",
|
|
61
61
|
"gpu": "Graphics",
|
|
62
62
|
"memory": "Memory",
|
|
63
|
+
"memory_speed": "{speed} MT/s",
|
|
63
64
|
"swap": "Swap",
|
|
64
65
|
"storage": "Storage",
|
|
65
66
|
"network": "Network",
|
|
@@ -74,7 +75,21 @@
|
|
|
74
75
|
"kernel": "Kernel",
|
|
75
76
|
"arch": "Architecture",
|
|
76
77
|
"uptime": "Uptime",
|
|
77
|
-
"usage": "{used} / {total}"
|
|
78
|
+
"usage": "{used} / {total}",
|
|
79
|
+
"user": {
|
|
80
|
+
"title": "User",
|
|
81
|
+
"username": "Username",
|
|
82
|
+
"uid": "UID / GID",
|
|
83
|
+
"home": "Home Directory",
|
|
84
|
+
"shell": "Shell",
|
|
85
|
+
"match_found": "This user matches '{name}'.",
|
|
86
|
+
"no_match": "No matching system user. Add one with the live username to connect it.",
|
|
87
|
+
"connect": "Connect User",
|
|
88
|
+
"connected_to": "Connected to {name}",
|
|
89
|
+
"disconnect": "Disconnect",
|
|
90
|
+
"disconnected_from": "Disconnected from {name}",
|
|
91
|
+
"username_mismatch": "The connected user “{connected}” no longer matches the live username “{live}”."
|
|
92
|
+
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { text } from '@axium/client';
|
|
3
|
-
import { formatBytes, formatDuration } from '@axium/core';
|
|
2
|
+
import { fetchAPI, text } from '@axium/client';
|
|
4
3
|
import { Icon, NumberBar } from '@axium/client/components';
|
|
5
4
|
import { connect } from '@axium/client/socket';
|
|
5
|
+
import { toastStatus } from '@axium/client/toast';
|
|
6
|
+
import { formatBytes, formatDuration } from '@axium/core';
|
|
6
7
|
import { systemTypeIcons, type SystemInfo } from '@axium/sysadmin';
|
|
7
8
|
import '@axium/sysadmin/common';
|
|
8
9
|
|
|
9
10
|
const { data } = $props();
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
let system = $state(data.system);
|
|
13
|
+
const systemUsers = data.systemUsers;
|
|
11
14
|
|
|
12
15
|
let info = $state<SystemInfo>();
|
|
13
16
|
let error = $state(false);
|
|
14
17
|
let loading = $state(true);
|
|
15
18
|
|
|
19
|
+
const matchingUser = $derived(info && systemUsers.find(u => u.username === info!.user.username));
|
|
20
|
+
const connectedUser = $derived(system.connectedUserId ? systemUsers.find(u => u.id === system.connectedUserId) : undefined);
|
|
21
|
+
|
|
22
|
+
async function setConnectedUser(connectedUserId: string | null) {
|
|
23
|
+
const updated = await fetchAPI('PATCH', 'sysadmin/systems/:id', { ...system, connectedUserId }, system.id);
|
|
24
|
+
system = updated;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
/** The fraction used, for a NumberBar, clamped to [0, 1]. */
|
|
17
28
|
function fraction(used: bigint, total: bigint): number {
|
|
18
29
|
if (total <= 0n) return 0;
|
|
@@ -20,6 +31,7 @@
|
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
function usageText(used: bigint, total: bigint): string {
|
|
34
|
+
if (used <= 0n) return formatBytes(total);
|
|
23
35
|
return text('sysadmin.system.usage', { used: formatBytes(used), total: formatBytes(total) });
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -35,7 +47,7 @@
|
|
|
35
47
|
try {
|
|
36
48
|
const all = await socket.emitWithAck('sysadmin:getSystemInfo');
|
|
37
49
|
info = all.find(i => i.hostname === system.hostname);
|
|
38
|
-
|
|
50
|
+
error = !info;
|
|
39
51
|
} catch {
|
|
40
52
|
error = true;
|
|
41
53
|
} finally {
|
|
@@ -53,9 +65,9 @@
|
|
|
53
65
|
</svelte:head>
|
|
54
66
|
|
|
55
67
|
<div class="system-page">
|
|
56
|
-
<a class="subtle icon-text back" href="/sysadmin
|
|
68
|
+
<a class="subtle icon-text back" href="/sysadmin">
|
|
57
69
|
<Icon i="arrow-left" />
|
|
58
|
-
<span>{text('sysadmin.
|
|
70
|
+
<span>{text('sysadmin.back_to_main')}</span>
|
|
59
71
|
</a>
|
|
60
72
|
|
|
61
73
|
<div class="system-header">
|
|
@@ -113,7 +125,19 @@
|
|
|
113
125
|
{/if}
|
|
114
126
|
|
|
115
127
|
<div class="component">
|
|
116
|
-
<h3
|
|
128
|
+
<h3>
|
|
129
|
+
<Icon i="memory" />
|
|
130
|
+
{text('sysadmin.system.memory')}
|
|
131
|
+
{#if info.memory.type}
|
|
132
|
+
<span class="subtle">{info.memory.type}</span>
|
|
133
|
+
{/if}
|
|
134
|
+
{#if info.memory.formFactor}
|
|
135
|
+
<span class="subtle">{info.memory.formFactor}</span>
|
|
136
|
+
{/if}
|
|
137
|
+
{#if info.memory.speed}
|
|
138
|
+
<span class="subtle">{text('sysadmin.system.memory_speed', { speed: info.memory.speed })}</span>
|
|
139
|
+
{/if}
|
|
140
|
+
</h3>
|
|
117
141
|
<NumberBar
|
|
118
142
|
value={fraction(info.memory.used, info.memory.total)}
|
|
119
143
|
max={1}
|
|
@@ -182,6 +206,68 @@
|
|
|
182
206
|
</dl>
|
|
183
207
|
</section>
|
|
184
208
|
{/if}
|
|
209
|
+
|
|
210
|
+
{#if info || connectedUser}
|
|
211
|
+
<section>
|
|
212
|
+
{const userIcon = !info ? 'user' : info.user.uid === 0 ? 'user-crown' : info.user.uid < 1000 ? 'user-gear' : 'user'}
|
|
213
|
+
<h2><Icon i={userIcon} /> {text('sysadmin.system.user.title')}</h2>
|
|
214
|
+
|
|
215
|
+
{#if info && connectedUser && connectedUser.username !== info.user.username}
|
|
216
|
+
<div class="warning icon-text">
|
|
217
|
+
<Icon i="triangle-exclamation" />
|
|
218
|
+
<span>
|
|
219
|
+
{text('sysadmin.system.user.username_mismatch', {
|
|
220
|
+
connected: connectedUser.username,
|
|
221
|
+
live: info.user.username,
|
|
222
|
+
})}
|
|
223
|
+
</span>
|
|
224
|
+
</div>
|
|
225
|
+
{/if}
|
|
226
|
+
|
|
227
|
+
{#if info}
|
|
228
|
+
<dl>
|
|
229
|
+
<dt>{text('sysadmin.system.user.username')}</dt>
|
|
230
|
+
<dd>{info.user.username}</dd>
|
|
231
|
+
<dt>{text('sysadmin.system.user.uid')}</dt>
|
|
232
|
+
<dd>{info.user.uid} <span class="subtle">/ {info.user.gid}</span></dd>
|
|
233
|
+
<dt>{text('sysadmin.system.user.home')}</dt>
|
|
234
|
+
<dd>{info.user.homedir}</dd>
|
|
235
|
+
{#if info.user.shell}
|
|
236
|
+
<dt>{text('sysadmin.system.user.shell')}</dt>
|
|
237
|
+
<dd>{info.user.shell}</dd>
|
|
238
|
+
{/if}
|
|
239
|
+
</dl>
|
|
240
|
+
{/if}
|
|
241
|
+
|
|
242
|
+
<div class="user-connection">
|
|
243
|
+
{#if connectedUser}
|
|
244
|
+
<a class="icon-text subtle" href="/sysadmin/users/{connectedUser.id}">
|
|
245
|
+
<Icon i="link" />
|
|
246
|
+
<span>{text('sysadmin.system.user.connected_to', connectedUser)}</span>
|
|
247
|
+
</a>
|
|
248
|
+
<button
|
|
249
|
+
class="icon-text"
|
|
250
|
+
onclick={() => toastStatus(setConnectedUser(null), text('sysadmin.system.user.disconnected_from', connectedUser))}
|
|
251
|
+
>
|
|
252
|
+
<Icon i="link-slash" />
|
|
253
|
+
<span>{text('sysadmin.system.user.disconnect')}</span>
|
|
254
|
+
</button>
|
|
255
|
+
{:else if matchingUser}
|
|
256
|
+
<span class="subtle">{text('sysadmin.system.user.match_found', matchingUser)}</span>
|
|
257
|
+
<button
|
|
258
|
+
class="icon-text"
|
|
259
|
+
onclick={() =>
|
|
260
|
+
toastStatus(setConnectedUser(matchingUser.id), text('sysadmin.system.user.connected_to', matchingUser))}
|
|
261
|
+
>
|
|
262
|
+
<Icon i="link" />
|
|
263
|
+
<span>{text('sysadmin.system.user.connect')}</span>
|
|
264
|
+
</button>
|
|
265
|
+
{:else}
|
|
266
|
+
<span class="subtle">{text('sysadmin.system.user.no_match')}</span>
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
269
|
+
</section>
|
|
270
|
+
{/if}
|
|
185
271
|
</div>
|
|
186
272
|
|
|
187
273
|
<style>
|
|
@@ -315,6 +401,17 @@
|
|
|
315
401
|
margin: 0;
|
|
316
402
|
}
|
|
317
403
|
|
|
404
|
+
.user-connection {
|
|
405
|
+
display: flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 1em;
|
|
408
|
+
flex-wrap: wrap;
|
|
409
|
+
|
|
410
|
+
button {
|
|
411
|
+
margin-left: auto;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
318
415
|
@media (width < 700px) {
|
|
319
416
|
.system-page {
|
|
320
417
|
padding: 1em;
|
|
@@ -5,7 +5,10 @@ export const ssr = false;
|
|
|
5
5
|
export async function load({ parent, params }) {
|
|
6
6
|
const { session } = await parent();
|
|
7
7
|
|
|
8
|
-
const system = await
|
|
8
|
+
const [system, systemUsers] = await Promise.all([
|
|
9
|
+
fetchAPI('GET', 'sysadmin/systems/:id', {}, params.id),
|
|
10
|
+
fetchAPI('GET', 'users/:id/sysadmin/users', {}, session.userId),
|
|
11
|
+
]);
|
|
9
12
|
|
|
10
|
-
return { session, system };
|
|
13
|
+
return { session, system, systemUsers };
|
|
11
14
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
2
3
|
import { fetchAPI, text } from '@axium/client';
|
|
3
4
|
import { Icon } from '@axium/client/components';
|
|
4
|
-
import {
|
|
5
|
+
import { socket } from '@axium/client/socket';
|
|
5
6
|
import { toastStatus } from '@axium/client/toast';
|
|
6
|
-
import {
|
|
7
|
+
import { SystemCard, UserInitDialog } from '@axium/sysadmin/components';
|
|
7
8
|
|
|
8
9
|
const { data } = $props();
|
|
9
10
|
|
|
10
11
|
let user = $state(data.user);
|
|
12
|
+
let systems = $state(data.systems);
|
|
11
13
|
let editDialog = $state<HTMLDialogElement>();
|
|
12
14
|
|
|
15
|
+
const onlineHosts = $derived(await socket?.emitWithAck('sysadmin:ping').then(systems => systems.map(s => s.hostname)));
|
|
16
|
+
|
|
13
17
|
function remove() {
|
|
14
18
|
toastStatus(
|
|
15
19
|
fetchAPI('DELETE', 'sysadmin/users/:id', {}, user.id).then(() => goto('/sysadmin/users')),
|
|
@@ -23,9 +27,9 @@
|
|
|
23
27
|
</svelte:head>
|
|
24
28
|
|
|
25
29
|
<div class="user-page">
|
|
26
|
-
<a class="subtle icon-text back" href="/sysadmin
|
|
30
|
+
<a class="subtle icon-text back" href="/sysadmin">
|
|
27
31
|
<Icon i="arrow-left" />
|
|
28
|
-
<span>{text('sysadmin.
|
|
32
|
+
<span>{text('sysadmin.back_to_main')}</span>
|
|
29
33
|
</a>
|
|
30
34
|
|
|
31
35
|
<div class="user-header">
|
|
@@ -43,6 +47,23 @@
|
|
|
43
47
|
<span>{text('generic.delete')}</span>
|
|
44
48
|
</button>
|
|
45
49
|
</div>
|
|
50
|
+
|
|
51
|
+
<section>
|
|
52
|
+
<h2><Icon i="server" /> {text('sysadmin.user.systems')}</h2>
|
|
53
|
+
{#if systems.length}
|
|
54
|
+
<div class="cards">
|
|
55
|
+
{#each systems as system, i (system.id)}
|
|
56
|
+
<SystemCard
|
|
57
|
+
bind:system={systems[i]}
|
|
58
|
+
online={!!onlineHosts?.includes(system.hostname)}
|
|
59
|
+
deleted={() => systems.splice(i, 1)}
|
|
60
|
+
/>
|
|
61
|
+
{/each}
|
|
62
|
+
</div>
|
|
63
|
+
{:else}
|
|
64
|
+
<p class="subtle">{text('sysadmin.user.no_systems')}</p>
|
|
65
|
+
{/if}
|
|
66
|
+
</section>
|
|
46
67
|
</div>
|
|
47
68
|
|
|
48
69
|
<UserInitDialog bind:dialog={editDialog} {user} edited={updated => (user = updated)} />
|
|
@@ -73,10 +94,33 @@
|
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
section {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 1em;
|
|
101
|
+
|
|
102
|
+
h2 {
|
|
103
|
+
margin: 0;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 0.5em;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.cards {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
113
|
+
gap: 1em;
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
@media (width < 700px) {
|
|
77
117
|
.user-page {
|
|
78
118
|
padding: 1em;
|
|
79
119
|
padding-bottom: 5em;
|
|
80
120
|
}
|
|
121
|
+
|
|
122
|
+
.cards {
|
|
123
|
+
grid-template-columns: 1fr;
|
|
124
|
+
}
|
|
81
125
|
}
|
|
82
126
|
</style>
|
|
@@ -5,7 +5,10 @@ export const ssr = false;
|
|
|
5
5
|
export async function load({ parent, params }) {
|
|
6
6
|
const { session } = await parent();
|
|
7
7
|
|
|
8
|
-
const user = await
|
|
8
|
+
const [user, systems] = await Promise.all([
|
|
9
|
+
fetchAPI('GET', 'sysadmin/users/:id', {}, params.id),
|
|
10
|
+
fetchAPI('GET', 'sysadmin/users/:id/systems', {}, params.id),
|
|
11
|
+
]);
|
|
9
12
|
|
|
10
|
-
return { session, user };
|
|
13
|
+
return { session, user, systems };
|
|
11
14
|
}
|