@axium/sysadmin 0.1.3 → 0.2.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/db.json +13 -0
- package/dist/client/info.js +1 -0
- package/dist/common.d.ts +55 -0
- package/dist/common.js +6 -0
- package/dist/info.d.ts +17 -0
- package/dist/info.js +8 -0
- package/dist/server/api.js +38 -0
- package/locales/en.json +18 -2
- package/package.json +1 -1
- package/routes/sysadmin/systems/[id]/+page.svelte +87 -3
- package/routes/sysadmin/systems/[id]/+page.ts +5 -2
- package/routes/sysadmin/users/[id]/+page.svelte +46 -2
- 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
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' {
|
|
@@ -307,6 +348,13 @@ declare const SysadminClientToServer: {
|
|
|
307
348
|
uptime: z.ZodNumber;
|
|
308
349
|
version: z.ZodString;
|
|
309
350
|
hostname: z.ZodString;
|
|
351
|
+
user: z.ZodObject<{
|
|
352
|
+
username: z.ZodString;
|
|
353
|
+
uid: z.ZodInt;
|
|
354
|
+
gid: z.ZodInt;
|
|
355
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
356
|
+
homedir: z.ZodString;
|
|
357
|
+
}, z.core.$strip>;
|
|
310
358
|
}, z.core.$strip>>]];
|
|
311
359
|
readonly 'sysadmin:ping': readonly [readonly [z.ZodArray<z.ZodObject<{
|
|
312
360
|
hostname: z.ZodString;
|
|
@@ -356,6 +404,13 @@ declare const SysadminServerToClient: {
|
|
|
356
404
|
uptime: z.ZodNumber;
|
|
357
405
|
version: z.ZodString;
|
|
358
406
|
hostname: z.ZodString;
|
|
407
|
+
user: z.ZodObject<{
|
|
408
|
+
username: z.ZodString;
|
|
409
|
+
uid: z.ZodInt;
|
|
410
|
+
gid: z.ZodInt;
|
|
411
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
412
|
+
homedir: z.ZodString;
|
|
413
|
+
}, z.core.$strip>;
|
|
359
414
|
}, z.core.$strip>]];
|
|
360
415
|
readonly 'sysadmin:ping': readonly [readonly [z.ZodObject<{
|
|
361
416
|
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>;
|
|
@@ -48,6 +49,15 @@ export declare const NetworkInterface: z.ZodObject<{
|
|
|
48
49
|
}, z.core.$strip>;
|
|
49
50
|
export interface NetworkInterface extends z.infer<typeof NetworkInterface> {
|
|
50
51
|
}
|
|
52
|
+
export declare const SystemInfoUser: z.ZodObject<{
|
|
53
|
+
username: z.ZodString;
|
|
54
|
+
uid: z.ZodInt;
|
|
55
|
+
gid: z.ZodInt;
|
|
56
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
57
|
+
homedir: z.ZodString;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export interface SystemInfoUser extends z.infer<typeof SystemInfoUser>, UserInfo<string> {
|
|
60
|
+
}
|
|
51
61
|
export declare const SystemInfo: z.ZodObject<{
|
|
52
62
|
cpus: z.ZodArray<z.ZodObject<{
|
|
53
63
|
model: z.ZodString;
|
|
@@ -90,6 +100,13 @@ export declare const SystemInfo: z.ZodObject<{
|
|
|
90
100
|
uptime: z.ZodNumber;
|
|
91
101
|
version: z.ZodString;
|
|
92
102
|
hostname: z.ZodString;
|
|
103
|
+
user: z.ZodObject<{
|
|
104
|
+
username: z.ZodString;
|
|
105
|
+
uid: z.ZodInt;
|
|
106
|
+
gid: z.ZodInt;
|
|
107
|
+
shell: z.ZodNullable<z.ZodString>;
|
|
108
|
+
homedir: z.ZodString;
|
|
109
|
+
}, z.core.$strip>;
|
|
93
110
|
}, z.core.$strip>;
|
|
94
111
|
export interface SystemInfo extends z.infer<typeof SystemInfo> {
|
|
95
112
|
}
|
package/dist/info.js
CHANGED
|
@@ -32,6 +32,13 @@ export const NetworkInterface = z.object({
|
|
|
32
32
|
connection: z.string().optional(),
|
|
33
33
|
speed: z.number().nonnegative().optional(),
|
|
34
34
|
});
|
|
35
|
+
export const SystemInfoUser = z.object({
|
|
36
|
+
username: z.string(),
|
|
37
|
+
uid: z.int(),
|
|
38
|
+
gid: z.int(),
|
|
39
|
+
shell: z.string().nullable(),
|
|
40
|
+
homedir: z.string(),
|
|
41
|
+
});
|
|
35
42
|
export const SystemInfo = z.object({
|
|
36
43
|
cpus: CPU.array(),
|
|
37
44
|
gpus: GPU.array(),
|
|
@@ -53,6 +60,7 @@ export const SystemInfo = z.object({
|
|
|
53
60
|
/** Kernel version, e.g. `#1 SMP PREEMPT_DYNAMIC ...` */
|
|
54
61
|
version: z.string(),
|
|
55
62
|
hostname: z.string(),
|
|
63
|
+
user: SystemInfoUser,
|
|
56
64
|
});
|
|
57
65
|
// OS //
|
|
58
66
|
// 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
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"toast_deleted": "User deleted"
|
|
43
43
|
},
|
|
44
44
|
"user": {
|
|
45
|
-
"page_title": "{name} — System Users"
|
|
45
|
+
"page_title": "{name} — System Users",
|
|
46
|
+
"systems": "Connected Systems",
|
|
47
|
+
"no_systems": "No systems are connected to this user."
|
|
46
48
|
},
|
|
47
49
|
"system_type": {
|
|
48
50
|
"server": "Server",
|
|
@@ -74,7 +76,21 @@
|
|
|
74
76
|
"kernel": "Kernel",
|
|
75
77
|
"arch": "Architecture",
|
|
76
78
|
"uptime": "Uptime",
|
|
77
|
-
"usage": "{used} / {total}"
|
|
79
|
+
"usage": "{used} / {total}",
|
|
80
|
+
"user": {
|
|
81
|
+
"title": "User",
|
|
82
|
+
"username": "Username",
|
|
83
|
+
"uid": "UID / GID",
|
|
84
|
+
"home": "Home Directory",
|
|
85
|
+
"shell": "Shell",
|
|
86
|
+
"match_found": "This user matches '{name}'.",
|
|
87
|
+
"no_match": "No matching system user. Add one with the live username to connect it.",
|
|
88
|
+
"connect": "Connect User",
|
|
89
|
+
"connected_to": "Connected to {name}",
|
|
90
|
+
"disconnect": "Disconnect",
|
|
91
|
+
"disconnected_from": "Disconnected from {name}",
|
|
92
|
+
"username_mismatch": "The connected user “{connected}” no longer matches the live username “{live}”."
|
|
93
|
+
}
|
|
78
94
|
}
|
|
79
95
|
}
|
|
80
96
|
}
|
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;
|
|
@@ -182,6 +193,68 @@
|
|
|
182
193
|
</dl>
|
|
183
194
|
</section>
|
|
184
195
|
{/if}
|
|
196
|
+
|
|
197
|
+
{#if info || connectedUser}
|
|
198
|
+
<section>
|
|
199
|
+
{const userIcon = !info ? 'user' : info.user.uid === 0 ? 'user-crown' : info.user.uid < 1000 ? 'user-gear' : 'user'}
|
|
200
|
+
<h2><Icon i={userIcon} /> {text('sysadmin.system.user.title')}</h2>
|
|
201
|
+
|
|
202
|
+
{#if info && connectedUser && connectedUser.username !== info.user.username}
|
|
203
|
+
<div class="warning icon-text">
|
|
204
|
+
<Icon i="triangle-exclamation" />
|
|
205
|
+
<span>
|
|
206
|
+
{text('sysadmin.system.user.username_mismatch', {
|
|
207
|
+
connected: connectedUser.username,
|
|
208
|
+
live: info.user.username,
|
|
209
|
+
})}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
{/if}
|
|
213
|
+
|
|
214
|
+
{#if info}
|
|
215
|
+
<dl>
|
|
216
|
+
<dt>{text('sysadmin.system.user.username')}</dt>
|
|
217
|
+
<dd>{info.user.username}</dd>
|
|
218
|
+
<dt>{text('sysadmin.system.user.uid')}</dt>
|
|
219
|
+
<dd>{info.user.uid} <span class="subtle">/ {info.user.gid}</span></dd>
|
|
220
|
+
<dt>{text('sysadmin.system.user.home')}</dt>
|
|
221
|
+
<dd>{info.user.homedir}</dd>
|
|
222
|
+
{#if info.user.shell}
|
|
223
|
+
<dt>{text('sysadmin.system.user.shell')}</dt>
|
|
224
|
+
<dd>{info.user.shell}</dd>
|
|
225
|
+
{/if}
|
|
226
|
+
</dl>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
229
|
+
<div class="user-connection">
|
|
230
|
+
{#if connectedUser}
|
|
231
|
+
<a class="icon-text subtle" href="/sysadmin/users/{connectedUser.id}">
|
|
232
|
+
<Icon i="link" />
|
|
233
|
+
<span>{text('sysadmin.system.user.connected_to', connectedUser)}</span>
|
|
234
|
+
</a>
|
|
235
|
+
<button
|
|
236
|
+
class="icon-text"
|
|
237
|
+
onclick={() => toastStatus(setConnectedUser(null), text('sysadmin.system.user.disconnected_from', connectedUser))}
|
|
238
|
+
>
|
|
239
|
+
<Icon i="link-slash" />
|
|
240
|
+
<span>{text('sysadmin.system.user.disconnect')}</span>
|
|
241
|
+
</button>
|
|
242
|
+
{:else if matchingUser}
|
|
243
|
+
<span class="subtle">{text('sysadmin.system.user.match_found', matchingUser)}</span>
|
|
244
|
+
<button
|
|
245
|
+
class="icon-text"
|
|
246
|
+
onclick={() =>
|
|
247
|
+
toastStatus(setConnectedUser(matchingUser.id), text('sysadmin.system.user.connected_to', matchingUser))}
|
|
248
|
+
>
|
|
249
|
+
<Icon i="link" />
|
|
250
|
+
<span>{text('sysadmin.system.user.connect')}</span>
|
|
251
|
+
</button>
|
|
252
|
+
{:else}
|
|
253
|
+
<span class="subtle">{text('sysadmin.system.user.no_match')}</span>
|
|
254
|
+
{/if}
|
|
255
|
+
</div>
|
|
256
|
+
</section>
|
|
257
|
+
{/if}
|
|
185
258
|
</div>
|
|
186
259
|
|
|
187
260
|
<style>
|
|
@@ -315,6 +388,17 @@
|
|
|
315
388
|
margin: 0;
|
|
316
389
|
}
|
|
317
390
|
|
|
391
|
+
.user-connection {
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
gap: 1em;
|
|
395
|
+
flex-wrap: wrap;
|
|
396
|
+
|
|
397
|
+
button {
|
|
398
|
+
margin-left: auto;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
318
402
|
@media (width < 700px) {
|
|
319
403
|
.system-page {
|
|
320
404
|
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')),
|
|
@@ -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
|
}
|