@axium/sysadmin 0.0.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/LICENSE.md +157 -0
- package/README.md +3 -0
- package/db.json +77 -0
- package/dist/_probe.d.ts +1 -0
- package/dist/_probe.js +4 -0
- package/dist/client/.info.js +38 -0
- package/dist/client/cli.d.ts +1 -0
- package/dist/client/cli.js +109 -0
- package/dist/client/hooks.d.ts +1 -0
- package/dist/client/hooks.js +1 -0
- package/dist/client/info.d.ts +2 -0
- package/dist/client/info.js +171 -0
- package/dist/client/socket.d.ts +1 -0
- package/dist/client/socket.js +9 -0
- package/dist/common.d.ts +371 -0
- package/dist/common.js +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/info.d.ts +95 -0
- package/dist/info.js +58 -0
- package/dist/server/api.d.ts +1 -0
- package/dist/server/api.js +136 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.js +1 -0
- package/dist/server/hooks.d.ts +3 -0
- package/dist/server/hooks.js +3 -0
- package/dist/server/socket.d.ts +1 -0
- package/dist/server/socket.js +9 -0
- package/dist/web_hook.d.ts +8 -0
- package/dist/web_hook.js +4 -0
- package/lib/SystemCard.svelte +133 -0
- package/lib/SystemInitDialog.svelte +59 -0
- package/lib/UserCard.svelte +108 -0
- package/lib/UserInitDialog.svelte +49 -0
- package/lib/index.ts +4 -0
- package/lib/tsconfig.json +12 -0
- package/locales/en.json +79 -0
- package/package.json +71 -0
- package/routes/sysadmin/+layout.ts +22 -0
- package/routes/sysadmin/+page.svelte +115 -0
- package/routes/sysadmin/+page.ts +14 -0
- package/routes/sysadmin/systems/+page.svelte +92 -0
- package/routes/sysadmin/systems/+page.ts +11 -0
- package/routes/sysadmin/systems/[id]/+page.svelte +290 -0
- package/routes/sysadmin/systems/[id]/+page.ts +11 -0
- package/routes/sysadmin/users/+page.svelte +85 -0
- package/routes/sysadmin/users/+page.ts +11 -0
- package/routes/sysadmin/users/[id]/+page.svelte +82 -0
- package/routes/sysadmin/users/[id]/+page.ts +11 -0
- package/routes/tsconfig.json +12 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as acl from '@axium/server/acl';
|
|
2
|
+
import { authRequestForItem, checkAuthForUser } from '@axium/server/auth';
|
|
3
|
+
import { database } from '@axium/server/database';
|
|
4
|
+
import { parseBody, withError } from '@axium/server/requests';
|
|
5
|
+
import { addRoute } from '@axium/server/routes';
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { SystemInit, SystemUserInit } from '../common.js';
|
|
8
|
+
function parseSystem(system, isShared) {
|
|
9
|
+
return Object.assign({ acl: [] }, system, { isShared });
|
|
10
|
+
}
|
|
11
|
+
addRoute({
|
|
12
|
+
path: '/api/users/:id/sysadmin/systems',
|
|
13
|
+
params: { id: z.uuid() },
|
|
14
|
+
async GET(request, { id: userId }) {
|
|
15
|
+
const { user } = await checkAuthForUser(request, userId);
|
|
16
|
+
const systems = await database
|
|
17
|
+
.selectFrom('systems')
|
|
18
|
+
.selectAll()
|
|
19
|
+
.select(acl.from('systems'))
|
|
20
|
+
.where(eb => eb.or([eb('userId', '=', userId), acl.existsIn('systems', user)(eb)]))
|
|
21
|
+
.execute()
|
|
22
|
+
.catch(withError('Could not get systems'));
|
|
23
|
+
return systems.map(system => parseSystem(system, system.userId !== userId));
|
|
24
|
+
},
|
|
25
|
+
async PUT(request, { id: userId }) {
|
|
26
|
+
const init = await parseBody(request, SystemInit);
|
|
27
|
+
await checkAuthForUser(request, userId);
|
|
28
|
+
return parseSystem(await database
|
|
29
|
+
.insertInto('systems')
|
|
30
|
+
.values({ ...init, userId })
|
|
31
|
+
.returningAll()
|
|
32
|
+
.executeTakeFirstOrThrow()
|
|
33
|
+
.catch(withError('Could not create system')), false);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
addRoute({
|
|
37
|
+
path: '/api/sysadmin/systems/:id',
|
|
38
|
+
params: { id: z.uuid() },
|
|
39
|
+
async GET(request, { id }) {
|
|
40
|
+
const { item, fromACL } = await authRequestForItem(request, 'systems', id, { read: true });
|
|
41
|
+
return parseSystem(item, fromACL);
|
|
42
|
+
},
|
|
43
|
+
async PATCH(request, { id }) {
|
|
44
|
+
const init = await parseBody(request, SystemInit);
|
|
45
|
+
const { fromACL } = await authRequestForItem(request, 'systems', id, { edit: true });
|
|
46
|
+
const system = await database
|
|
47
|
+
.updateTable('systems')
|
|
48
|
+
.set(init)
|
|
49
|
+
.where('id', '=', id)
|
|
50
|
+
.returningAll()
|
|
51
|
+
.returning(acl.from('systems'))
|
|
52
|
+
.executeTakeFirstOrThrow()
|
|
53
|
+
.catch(withError('Could not update system'));
|
|
54
|
+
return parseSystem(system, fromACL);
|
|
55
|
+
},
|
|
56
|
+
async DELETE(request, { id }) {
|
|
57
|
+
const { fromACL } = await authRequestForItem(request, 'systems', id, { manage: true });
|
|
58
|
+
const system = await database
|
|
59
|
+
.deleteFrom('systems')
|
|
60
|
+
.where('id', '=', id)
|
|
61
|
+
.returningAll()
|
|
62
|
+
.returning(acl.from('systems'))
|
|
63
|
+
.executeTakeFirstOrThrow()
|
|
64
|
+
.catch(withError('Could not delete system'));
|
|
65
|
+
return parseSystem(system, fromACL);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
addRoute({
|
|
69
|
+
path: '/api/users/:id/sysadmin/users',
|
|
70
|
+
params: { id: z.uuid() },
|
|
71
|
+
async GET(request, { id: userId }) {
|
|
72
|
+
await checkAuthForUser(request, userId);
|
|
73
|
+
return await database
|
|
74
|
+
.selectFrom('system_users')
|
|
75
|
+
.selectAll()
|
|
76
|
+
.where('userId', '=', userId)
|
|
77
|
+
.execute()
|
|
78
|
+
.catch(withError('Could not get system users'));
|
|
79
|
+
},
|
|
80
|
+
async PUT(request, { id: userId }) {
|
|
81
|
+
const init = await parseBody(request, SystemUserInit);
|
|
82
|
+
await checkAuthForUser(request, userId);
|
|
83
|
+
return await database
|
|
84
|
+
.insertInto('system_users')
|
|
85
|
+
.values({ ...init, userId })
|
|
86
|
+
.returningAll()
|
|
87
|
+
.executeTakeFirstOrThrow()
|
|
88
|
+
.catch(withError('Could not create system user'));
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
addRoute({
|
|
92
|
+
path: '/api/sysadmin/users/:id',
|
|
93
|
+
params: { id: z.uuid() },
|
|
94
|
+
async GET(request, { id }) {
|
|
95
|
+
const user = await database
|
|
96
|
+
.selectFrom('system_users')
|
|
97
|
+
.selectAll()
|
|
98
|
+
.where('id', '=', id)
|
|
99
|
+
.executeTakeFirstOrThrow()
|
|
100
|
+
.catch(withError('Could not get system user', 404));
|
|
101
|
+
await checkAuthForUser(request, user.userId);
|
|
102
|
+
return user;
|
|
103
|
+
},
|
|
104
|
+
async PATCH(request, { id }) {
|
|
105
|
+
const init = await parseBody(request, SystemUserInit);
|
|
106
|
+
const existing = await database
|
|
107
|
+
.selectFrom('system_users')
|
|
108
|
+
.select('userId')
|
|
109
|
+
.where('id', '=', id)
|
|
110
|
+
.executeTakeFirstOrThrow()
|
|
111
|
+
.catch(withError('Could not get system user', 404));
|
|
112
|
+
await checkAuthForUser(request, existing.userId);
|
|
113
|
+
return await database
|
|
114
|
+
.updateTable('system_users')
|
|
115
|
+
.set(init)
|
|
116
|
+
.where('id', '=', id)
|
|
117
|
+
.returningAll()
|
|
118
|
+
.executeTakeFirstOrThrow()
|
|
119
|
+
.catch(withError('Could not update system user'));
|
|
120
|
+
},
|
|
121
|
+
async DELETE(request, { id }) {
|
|
122
|
+
const existing = await database
|
|
123
|
+
.selectFrom('system_users')
|
|
124
|
+
.select('userId')
|
|
125
|
+
.where('id', '=', id)
|
|
126
|
+
.executeTakeFirstOrThrow()
|
|
127
|
+
.catch(withError('Could not get system user', 404));
|
|
128
|
+
await checkAuthForUser(request, existing.userId);
|
|
129
|
+
return await database
|
|
130
|
+
.deleteFrom('system_users')
|
|
131
|
+
.where('id', '=', id)
|
|
132
|
+
.returningAll()
|
|
133
|
+
.executeTakeFirstOrThrow()
|
|
134
|
+
.catch(withError('Could not delete system user'));
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './socket.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { addListener, io } from '@axium/server/socket';
|
|
2
|
+
addListener('sysadmin:ping', async (socket, cb) => {
|
|
3
|
+
const result = await io.in(`local:${socket.data.user.id}`).timeout(5000).emitWithAck('sysadmin:ping');
|
|
4
|
+
cb(result);
|
|
5
|
+
});
|
|
6
|
+
addListener('sysadmin:getSystemInfo', async (socket, cb) => {
|
|
7
|
+
const result = await io.in(`local:${socket.data.user.id}`).timeout(5000).emitWithAck('sysadmin:getSystemInfo');
|
|
8
|
+
cb(result);
|
|
9
|
+
});
|
package/dist/web_hook.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchAPI, text } from '@axium/client';
|
|
3
|
+
import { contextMenu, type ContextMenuItem } from '@axium/client/attachments';
|
|
4
|
+
import { Icon, Popover } from '@axium/client/components';
|
|
5
|
+
import { copy } from '@axium/client/gui';
|
|
6
|
+
import { toastStatus } from '@axium/client/toast';
|
|
7
|
+
import { systemTypeIcons, type System } from '@axium/sysadmin';
|
|
8
|
+
import SystemInitDialog from './SystemInitDialog.svelte';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
system = $bindable(),
|
|
12
|
+
online,
|
|
13
|
+
edited,
|
|
14
|
+
deleted,
|
|
15
|
+
}: {
|
|
16
|
+
system: System;
|
|
17
|
+
online: boolean;
|
|
18
|
+
/** Called with the updated system after an edit */
|
|
19
|
+
edited?(system: System): unknown;
|
|
20
|
+
/** Called after the system is deleted */
|
|
21
|
+
deleted?(system: System): unknown;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
let editDialog = $state<HTMLDialogElement>();
|
|
25
|
+
|
|
26
|
+
const link = $derived(`/sysadmin/systems/${system.id}`);
|
|
27
|
+
|
|
28
|
+
function remove() {
|
|
29
|
+
toastStatus(
|
|
30
|
+
fetchAPI('DELETE', 'sysadmin/systems/:id', {}, system.id).then(() => deleted?.(system)),
|
|
31
|
+
text('sysadmin.SystemCard.toast_deleted')
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The actions shown in both the hover popover and the right-click context menu. */
|
|
36
|
+
const menuItems = (): ContextMenuItem[] => [
|
|
37
|
+
{ i: 'pen', text: text('sysadmin.SystemCard.edit'), action: () => editDialog!.showModal() },
|
|
38
|
+
{ i: 'link-horizontal', text: text('sysadmin.SystemCard.copy_link'), action: () => copy('text/plain', link) },
|
|
39
|
+
{
|
|
40
|
+
i: 'arrow-up-right-from-square',
|
|
41
|
+
text: text('sysadmin.SystemCard.open_new_tab'),
|
|
42
|
+
action: () => window.open(`/sysadmin/systems/${system.id}`),
|
|
43
|
+
},
|
|
44
|
+
{ i: 'trash', text: text('generic.delete'), danger: true, action: remove },
|
|
45
|
+
];
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div class="SystemCard" {@attach contextMenu(menuItems)}>
|
|
49
|
+
<span class={['indicator', online ? 'online' : 'offline']} title={online ? text('sysadmin.online') : text('sysadmin.offline')}></span>
|
|
50
|
+
<a class="link" href="/sysadmin/systems/{system.id}">
|
|
51
|
+
<Icon i={systemTypeIcons[system.type]} class="icon" --size="2em" />
|
|
52
|
+
<div class="info">
|
|
53
|
+
<span class="name">{system.name}</span>
|
|
54
|
+
<span class="subtle hostname">{system.hostname}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</a>
|
|
57
|
+
|
|
58
|
+
<Popover showToggle="hover">
|
|
59
|
+
{#each menuItems() as item}
|
|
60
|
+
<div class={['menu-item', item.danger && 'danger']} onclick={item.action}>
|
|
61
|
+
<Icon i={item.i!} />
|
|
62
|
+
<span>{item.text}</span>
|
|
63
|
+
</div>
|
|
64
|
+
{/each}
|
|
65
|
+
</Popover>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<SystemInitDialog
|
|
69
|
+
bind:dialog={editDialog}
|
|
70
|
+
{system}
|
|
71
|
+
edited={updated => {
|
|
72
|
+
system = updated;
|
|
73
|
+
edited?.(updated);
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.SystemCard {
|
|
79
|
+
position: relative;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 0.5em;
|
|
83
|
+
padding: 1em;
|
|
84
|
+
border-radius: 0.5em;
|
|
85
|
+
background-color: var(--bg-alt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.link {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 1em;
|
|
92
|
+
flex: 1;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
color: inherit;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.indicator {
|
|
100
|
+
position: absolute;
|
|
101
|
+
top: 0.6em;
|
|
102
|
+
left: 0.6em;
|
|
103
|
+
width: 0.6em;
|
|
104
|
+
height: 0.6em;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
|
|
107
|
+
&.online {
|
|
108
|
+
background-color: var(--green);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&.offline {
|
|
112
|
+
background-color: hsl(0 0 calc(var(--fg-light) - 30));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.info {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: 0.1em;
|
|
120
|
+
flex: 1;
|
|
121
|
+
min-width: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.name {
|
|
125
|
+
font-weight: bold;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.hostname {
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
text-overflow: ellipsis;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchAPI, text } from '@axium/client';
|
|
3
|
+
import { FormDialog } from '@axium/client/components';
|
|
4
|
+
import { SystemInit, SystemType, type System } from '@axium/sysadmin';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
dialog = $bindable(),
|
|
8
|
+
userId,
|
|
9
|
+
system,
|
|
10
|
+
created,
|
|
11
|
+
edited,
|
|
12
|
+
}: {
|
|
13
|
+
/** The dialog element, for triggering programmatically (e.g. an edit action) */
|
|
14
|
+
dialog?: HTMLDialogElement;
|
|
15
|
+
/** The user the system is created for (used in create mode) */
|
|
16
|
+
userId?: string;
|
|
17
|
+
/** An existing system to edit. When set, the dialog operates in edit mode. */
|
|
18
|
+
system?: System;
|
|
19
|
+
/** Called with the newly created system (create mode) */
|
|
20
|
+
created?(system: System): unknown;
|
|
21
|
+
/** Called with the updated system (edit mode) */
|
|
22
|
+
edited?(system: System): unknown;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const types = SystemType.def.values;
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<FormDialog
|
|
29
|
+
id={system ? undefined : 'system-init'}
|
|
30
|
+
bind:dialog
|
|
31
|
+
submitText={system ? text('sysadmin.SystemInitDialog.save') : text('sysadmin.SystemInitDialog.submit')}
|
|
32
|
+
submit={async raw => {
|
|
33
|
+
const init = SystemInit.parse(raw);
|
|
34
|
+
if (system) {
|
|
35
|
+
const updated = await fetchAPI('PATCH', 'sysadmin/systems/:id', init, system.id);
|
|
36
|
+
await edited?.(updated);
|
|
37
|
+
} else {
|
|
38
|
+
const created_ = await fetchAPI('PUT', 'users/:id/sysadmin/systems', init, userId!);
|
|
39
|
+
await created?.(created_);
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<div>
|
|
44
|
+
<label for="name">{text('sysadmin.SystemInitDialog.name')}</label>
|
|
45
|
+
<input name="name" type="text" required value={system?.name ?? ''} />
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<label for="hostname">{text('sysadmin.SystemInitDialog.hostname')}</label>
|
|
49
|
+
<input name="hostname" type="text" required value={system?.hostname ?? ''} />
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<label for="type">{text('sysadmin.SystemInitDialog.type')}</label>
|
|
53
|
+
<select name="type" value={system?.type ?? 'server'}>
|
|
54
|
+
{#each types as type}
|
|
55
|
+
<option value={type}>{text(`sysadmin.system_type.${type}`)}</option>
|
|
56
|
+
{/each}
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
</FormDialog>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchAPI, text } from '@axium/client';
|
|
3
|
+
import { contextMenu, type ContextMenuItem } from '@axium/client/attachments';
|
|
4
|
+
import { Icon, Popover } from '@axium/client/components';
|
|
5
|
+
import { copy } from '@axium/client/gui';
|
|
6
|
+
import { toastStatus } from '@axium/client/toast';
|
|
7
|
+
import type { SystemUser } from '@axium/sysadmin';
|
|
8
|
+
import UserInitDialog from './UserInitDialog.svelte';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
user = $bindable(),
|
|
12
|
+
edited,
|
|
13
|
+
deleted,
|
|
14
|
+
}: {
|
|
15
|
+
user: SystemUser;
|
|
16
|
+
/** Called with the updated user after an edit */
|
|
17
|
+
edited?(user: SystemUser): unknown;
|
|
18
|
+
/** Called after the user is deleted */
|
|
19
|
+
deleted?(user: SystemUser): unknown;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let editDialog = $state<HTMLDialogElement>();
|
|
23
|
+
|
|
24
|
+
const link = $derived(`/sysadmin/users/${user.id}`);
|
|
25
|
+
|
|
26
|
+
function remove() {
|
|
27
|
+
toastStatus(
|
|
28
|
+
fetchAPI('DELETE', 'sysadmin/users/:id', {}, user.id).then(() => deleted?.(user)),
|
|
29
|
+
text('sysadmin.UserCard.toast_deleted')
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The actions shown in both the hover popover and the right-click context menu. */
|
|
34
|
+
const menuItems = (): ContextMenuItem[] => [
|
|
35
|
+
{ i: 'pen', text: text('sysadmin.UserCard.edit'), action: () => editDialog!.showModal() },
|
|
36
|
+
{ i: 'link-horizontal', text: text('sysadmin.UserCard.copy_link'), action: () => copy('text/plain', link) },
|
|
37
|
+
{ i: 'arrow-up-right-from-square', text: text('sysadmin.UserCard.open_new_tab'), action: () => window.open(link) },
|
|
38
|
+
{ i: 'trash', text: text('generic.delete'), danger: true, action: remove },
|
|
39
|
+
];
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="UserCard" {@attach contextMenu(menuItems)}>
|
|
43
|
+
<a class="link" href={link}>
|
|
44
|
+
<Icon i="user" class="icon" --size="2em" />
|
|
45
|
+
<div class="info">
|
|
46
|
+
<span class="name">{user.name}</span>
|
|
47
|
+
<span class="subtle username">{user.username}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</a>
|
|
50
|
+
|
|
51
|
+
<Popover showToggle="hover">
|
|
52
|
+
{#each menuItems() as item}
|
|
53
|
+
<div class={['menu-item', item.danger && 'danger']} onclick={item.action}>
|
|
54
|
+
<Icon i={item.i!} />
|
|
55
|
+
<span>{item.text}</span>
|
|
56
|
+
</div>
|
|
57
|
+
{/each}
|
|
58
|
+
</Popover>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<UserInitDialog
|
|
62
|
+
bind:dialog={editDialog}
|
|
63
|
+
{user}
|
|
64
|
+
edited={updated => {
|
|
65
|
+
user = updated;
|
|
66
|
+
edited?.(updated);
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.UserCard {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 0.5em;
|
|
75
|
+
padding: 1em;
|
|
76
|
+
border-radius: 0.5em;
|
|
77
|
+
background-color: var(--bg-alt);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.link {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 1em;
|
|
84
|
+
flex: 1;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
color: inherit;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.info {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
gap: 0.1em;
|
|
95
|
+
flex: 1;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.name {
|
|
100
|
+
font-weight: bold;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.username {
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
text-overflow: ellipsis;
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchAPI, text } from '@axium/client';
|
|
3
|
+
import { FormDialog } from '@axium/client/components';
|
|
4
|
+
import { SystemUserInit, type SystemUser } from '@axium/sysadmin';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
dialog = $bindable(),
|
|
8
|
+
userId,
|
|
9
|
+
user,
|
|
10
|
+
created,
|
|
11
|
+
edited,
|
|
12
|
+
}: {
|
|
13
|
+
/** The dialog element, for triggering programmatically (e.g. an edit action) */
|
|
14
|
+
dialog?: HTMLDialogElement;
|
|
15
|
+
/** The user the system user is created for (used in create mode) */
|
|
16
|
+
userId?: string;
|
|
17
|
+
/** An existing system user to edit. When set, the dialog operates in edit mode. */
|
|
18
|
+
user?: SystemUser;
|
|
19
|
+
/** Called with the newly created system user (create mode) */
|
|
20
|
+
created?(user: SystemUser): unknown;
|
|
21
|
+
/** Called with the updated system user (edit mode) */
|
|
22
|
+
edited?(user: SystemUser): unknown;
|
|
23
|
+
} = $props();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<FormDialog
|
|
27
|
+
id={user ? undefined : 'system-user-init'}
|
|
28
|
+
bind:dialog
|
|
29
|
+
submitText={user ? text('sysadmin.UserInitDialog.save') : text('sysadmin.UserInitDialog.submit')}
|
|
30
|
+
submit={async raw => {
|
|
31
|
+
const init = SystemUserInit.parse(raw);
|
|
32
|
+
if (user) {
|
|
33
|
+
const updated = await fetchAPI('PATCH', 'sysadmin/users/:id', init, user.id);
|
|
34
|
+
await edited?.(updated);
|
|
35
|
+
} else {
|
|
36
|
+
const created_ = await fetchAPI('PUT', 'users/:id/sysadmin/users', init, userId!);
|
|
37
|
+
await created?.(created_);
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div>
|
|
42
|
+
<label for="name">{text('sysadmin.UserInitDialog.name')}</label>
|
|
43
|
+
<input name="name" type="text" required value={user?.name ?? ''} />
|
|
44
|
+
</div>
|
|
45
|
+
<div>
|
|
46
|
+
<label for="username">{text('sysadmin.UserInitDialog.username')}</label>
|
|
47
|
+
<input name="username" type="text" required value={user?.username ?? ''} />
|
|
48
|
+
</div>
|
|
49
|
+
</FormDialog>
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../tsconfig.json", "../.svelte-kit/tsconfig.json"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "..",
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"module": "preserve",
|
|
7
|
+
"moduleResolution": "Bundler"
|
|
8
|
+
},
|
|
9
|
+
"include": ["**/*.svelte", "**/*.ts"],
|
|
10
|
+
"exclude": [],
|
|
11
|
+
"references": [{ "path": ".." }]
|
|
12
|
+
}
|
package/locales/en.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app_name": {
|
|
3
|
+
"sysadmin": "System Administration"
|
|
4
|
+
},
|
|
5
|
+
"sysadmin": {
|
|
6
|
+
"page_title": "System Administration",
|
|
7
|
+
"systems": "Systems",
|
|
8
|
+
"system_users": "System Users",
|
|
9
|
+
"view_all": "View all",
|
|
10
|
+
"no_systems": "No systems yet.",
|
|
11
|
+
"no_system_users": "No system users yet.",
|
|
12
|
+
"new_system": "Add System",
|
|
13
|
+
"new_system_user": "Add User",
|
|
14
|
+
"online": "Online",
|
|
15
|
+
"offline": "Offline",
|
|
16
|
+
"back_to_main": "Back to System Administration",
|
|
17
|
+
"back_to_systems": "Back to Systems",
|
|
18
|
+
"back_to_users": "Back to System Users",
|
|
19
|
+
"SystemInitDialog": {
|
|
20
|
+
"submit": "Add System",
|
|
21
|
+
"save": "Save",
|
|
22
|
+
"name": "Name",
|
|
23
|
+
"hostname": "Hostname",
|
|
24
|
+
"type": "Type"
|
|
25
|
+
},
|
|
26
|
+
"SystemCard": {
|
|
27
|
+
"edit": "Edit",
|
|
28
|
+
"copy_link": "Copy Link",
|
|
29
|
+
"open_new_tab": "Open in New Tab",
|
|
30
|
+
"toast_deleted": "System deleted"
|
|
31
|
+
},
|
|
32
|
+
"UserInitDialog": {
|
|
33
|
+
"submit": "Add User",
|
|
34
|
+
"save": "Save",
|
|
35
|
+
"name": "Name",
|
|
36
|
+
"username": "Username"
|
|
37
|
+
},
|
|
38
|
+
"UserCard": {
|
|
39
|
+
"edit": "Edit",
|
|
40
|
+
"copy_link": "Copy Link",
|
|
41
|
+
"open_new_tab": "Open in New Tab",
|
|
42
|
+
"toast_deleted": "User deleted"
|
|
43
|
+
},
|
|
44
|
+
"user": {
|
|
45
|
+
"page_title": "{name} — System Users"
|
|
46
|
+
},
|
|
47
|
+
"system_type": {
|
|
48
|
+
"server": "Server",
|
|
49
|
+
"pc": "PC",
|
|
50
|
+
"laptop": "Laptop"
|
|
51
|
+
},
|
|
52
|
+
"system": {
|
|
53
|
+
"page_title": "{name} — Systems",
|
|
54
|
+
"offline_notice": "This system is offline. Live information is unavailable.",
|
|
55
|
+
"loading_info": "Loading system information…",
|
|
56
|
+
"info_error": "Could not load system information.",
|
|
57
|
+
"refresh": "Refresh",
|
|
58
|
+
"hardware": "Hardware",
|
|
59
|
+
"cpu": "CPU",
|
|
60
|
+
"cpu_cores": "{count} cores",
|
|
61
|
+
"gpu": "Graphics",
|
|
62
|
+
"memory": "Memory",
|
|
63
|
+
"swap": "Swap",
|
|
64
|
+
"storage": "Storage",
|
|
65
|
+
"network": "Network",
|
|
66
|
+
"wireless": "Wireless",
|
|
67
|
+
"wired": "Wired",
|
|
68
|
+
"connected": "Connected",
|
|
69
|
+
"disconnected": "Disconnected",
|
|
70
|
+
"os": "Operating System",
|
|
71
|
+
"platform": "Platform",
|
|
72
|
+
"release": "Release",
|
|
73
|
+
"kernel": "Kernel",
|
|
74
|
+
"arch": "Architecture",
|
|
75
|
+
"uptime": "Uptime",
|
|
76
|
+
"usage": "{used} / {total}"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|