@axium/server 0.8.0 → 0.9.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/dist/auth.d.ts +0 -10
- package/dist/auth.js +8 -16
- package/dist/cli.js +145 -38
- package/dist/config.d.ts +39 -474
- package/dist/config.js +53 -43
- package/dist/database.d.ts +3 -3
- package/dist/database.js +53 -24
- package/dist/plugins.d.ts +1 -0
- package/dist/plugins.js +1 -0
- package/package.json +10 -7
- package/web/api/metadata.ts +3 -3
- package/web/api/passkeys.ts +3 -3
- package/web/api/register.ts +4 -4
- package/web/api/session.ts +2 -2
- package/web/api/users.ts +29 -18
- package/web/api/utils.ts +2 -2
- package/web/hooks.server.ts +8 -2
- package/web/lib/ClipboardCopy.svelte +42 -0
- package/web/lib/FormDialog.svelte +9 -1
- package/web/lib/auth.ts +2 -2
- package/web/lib/icons/Icon.svelte +2 -6
- package/web/lib/styles.css +7 -1
- package/web/routes/[...path]/+page.server.ts +1 -1
- package/web/routes/[appId]/[...page]/+page.server.ts +1 -1
- package/web/routes/account/+page.svelte +115 -48
- package/web/routes/api/[...path]/+server.ts +2 -2
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import ClipboardCopy from '$lib/ClipboardCopy.svelte';
|
|
2
4
|
import FormDialog from '$lib/FormDialog.svelte';
|
|
3
5
|
import Icon from '$lib/icons/Icon.svelte';
|
|
4
6
|
import {
|
|
5
|
-
|
|
7
|
+
createPasskey,
|
|
6
8
|
deletePasskey,
|
|
9
|
+
deleteUser,
|
|
10
|
+
emailVerificationEnabled,
|
|
11
|
+
getCurrentSession,
|
|
7
12
|
getPasskeys,
|
|
13
|
+
getSessions,
|
|
14
|
+
logout,
|
|
15
|
+
logoutAll,
|
|
8
16
|
sendVerificationEmail,
|
|
9
17
|
updatePasskey,
|
|
10
18
|
updateUser,
|
|
11
|
-
createPasskey,
|
|
12
|
-
deleteUser,
|
|
13
19
|
} from '@axium/client/user';
|
|
14
|
-
import type { Passkey } from '@axium/core/api';
|
|
20
|
+
import type { Passkey, Session } from '@axium/core/api';
|
|
15
21
|
import { getUserImage, type User } from '@axium/core/user';
|
|
16
22
|
|
|
17
23
|
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
18
24
|
|
|
19
25
|
let verificationSent = $state(false);
|
|
26
|
+
let currentSession = $state<Session & { user: User }>();
|
|
20
27
|
let user = $state<User>();
|
|
28
|
+
let canVerify = $state(false);
|
|
29
|
+
let passkeys = $state<Passkey[]>([]);
|
|
30
|
+
let sessions = $state<Session[]>([]);
|
|
21
31
|
|
|
22
32
|
async function ready() {
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
currentSession = await getCurrentSession().catch(() => {
|
|
34
|
+
goto('/login?after=/account');
|
|
35
|
+
return null;
|
|
36
|
+
})!;
|
|
37
|
+
user = currentSession.user;
|
|
25
38
|
|
|
26
39
|
passkeys = await getPasskeys(user.id);
|
|
27
|
-
}
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
sessions = await getSessions(user.id);
|
|
42
|
+
|
|
43
|
+
canVerify = await emailVerificationEnabled(user.id);
|
|
44
|
+
}
|
|
30
45
|
|
|
31
46
|
async function _editUser(data) {
|
|
32
47
|
const result = await updateUser(user.id, data);
|
|
@@ -39,14 +54,8 @@
|
|
|
39
54
|
</svelte:head>
|
|
40
55
|
|
|
41
56
|
{#snippet action(name: string, i: string = 'pen')}
|
|
42
|
-
<button
|
|
43
|
-
|
|
44
|
-
style:cursor="pointer"
|
|
45
|
-
onclick={() => {
|
|
46
|
-
dialogs[name].showModal();
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
<Icon {i} />
|
|
57
|
+
<button style:display="contents" onclick={() => dialogs[name].showModal()}>
|
|
58
|
+
<Icon {i} --size="16px" />
|
|
50
59
|
</button>
|
|
51
60
|
{/snippet}
|
|
52
61
|
|
|
@@ -56,9 +65,10 @@
|
|
|
56
65
|
<p class="greeting">Welcome, {user.name}</p>
|
|
57
66
|
|
|
58
67
|
<div class="section main">
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
<
|
|
68
|
+
<h3>Personal Information</h3>
|
|
69
|
+
<div class="item info">
|
|
70
|
+
<p class="subtle">Name</p>
|
|
71
|
+
<p>{user.name}</p>
|
|
62
72
|
{@render action('edit_name')}
|
|
63
73
|
</div>
|
|
64
74
|
<FormDialog bind:dialog={dialogs.edit_name} submit={_editUser} submitText="Change">
|
|
@@ -67,20 +77,20 @@
|
|
|
67
77
|
<input name="name" type="text" value={user.name || ''} required />
|
|
68
78
|
</div>
|
|
69
79
|
</FormDialog>
|
|
70
|
-
<div class="item">
|
|
71
|
-
<
|
|
72
|
-
<
|
|
80
|
+
<div class="item info">
|
|
81
|
+
<p class="subtle">Email</p>
|
|
82
|
+
<p>
|
|
73
83
|
{user.email}
|
|
74
84
|
{#if user.emailVerified}
|
|
75
|
-
<dfn title="Email verified on {
|
|
85
|
+
<dfn title="Email verified on {user.emailVerified.toLocaleDateString()}">
|
|
76
86
|
<Icon i="regular/circle-check" />
|
|
77
87
|
</dfn>
|
|
78
|
-
{:else}
|
|
88
|
+
{:else if canVerify}
|
|
79
89
|
<button onclick={() => sendVerificationEmail(user.id).then(() => (verificationSent = true))}>
|
|
80
90
|
{verificationSent ? 'Verification email sent' : 'Verify'}
|
|
81
91
|
</button>
|
|
82
92
|
{/if}
|
|
83
|
-
</
|
|
93
|
+
</p>
|
|
84
94
|
{@render action('edit_email')}
|
|
85
95
|
</div>
|
|
86
96
|
<FormDialog bind:dialog={dialogs.edit_email} submit={_editUser} submitText="Change">
|
|
@@ -90,16 +100,20 @@
|
|
|
90
100
|
</div>
|
|
91
101
|
</FormDialog>
|
|
92
102
|
|
|
93
|
-
<div class="item">
|
|
94
|
-
<p class="subtle">User ID <dfn title="This is your UUID."><Icon i="regular/circle-info" /></dfn></p>
|
|
103
|
+
<div class="item info">
|
|
104
|
+
<p class="subtle">User ID <dfn title="This is your UUID. It can't be changed."><Icon i="regular/circle-info" /></dfn></p>
|
|
95
105
|
<p>{user.id}</p>
|
|
106
|
+
<ClipboardCopy value={user.id} --size="16px" />
|
|
96
107
|
</div>
|
|
97
108
|
<span>
|
|
98
109
|
<a class="signout" href="/logout"><button>Sign out</button></a>
|
|
99
|
-
<button style:cursor="pointer" onclick={() => dialogs.delete.showModal()}
|
|
100
|
-
|
|
110
|
+
<button style:cursor="pointer" onclick={() => dialogs.delete.showModal()} class="danger">Delete Account</button>
|
|
111
|
+
<FormDialog
|
|
112
|
+
bind:dialog={dialogs.delete}
|
|
113
|
+
submit={() => deleteUser(user.id).then(() => goto('/'))}
|
|
114
|
+
submitText="Delete Account"
|
|
115
|
+
submitDanger
|
|
101
116
|
>
|
|
102
|
-
<FormDialog bind:dialog={dialogs.delete} submit={() => deleteUser(user.id)} submitText="Delete Account" submitDanger>
|
|
103
117
|
<p>Are you sure you want to delete your account?<br />This action can't be undone.</p>
|
|
104
118
|
</FormDialog>
|
|
105
119
|
</span>
|
|
@@ -108,25 +122,25 @@
|
|
|
108
122
|
<div class="section main">
|
|
109
123
|
<h3>Passkeys</h3>
|
|
110
124
|
{#each passkeys as passkey}
|
|
111
|
-
<div class="passkey">
|
|
125
|
+
<div class="item passkey">
|
|
112
126
|
<dfn title={passkey.deviceType == 'multiDevice' ? 'Multiple devices' : 'Single device'}>
|
|
113
|
-
<Icon i={passkey.deviceType == 'multiDevice' ? 'laptop-mobile' : 'mobile'} />
|
|
127
|
+
<Icon i={passkey.deviceType == 'multiDevice' ? 'laptop-mobile' : 'mobile'} --size="16px" />
|
|
114
128
|
</dfn>
|
|
115
129
|
<dfn title="This passkey is {passkey.backedUp ? '' : 'not '}backed up">
|
|
116
|
-
<Icon i={passkey.backedUp ? 'circle-check' : 'circle-xmark'} />
|
|
130
|
+
<Icon i={passkey.backedUp ? 'circle-check' : 'circle-xmark'} --size="16px" />
|
|
117
131
|
</dfn>
|
|
118
|
-
<p>Created {new Date(passkey.createdAt).toLocaleString()}</p>
|
|
119
132
|
{#if passkey.name}
|
|
120
133
|
<p>{passkey.name}</p>
|
|
121
134
|
{:else}
|
|
122
135
|
<p class="subtle"><i>Unnamed</i></p>
|
|
123
136
|
{/if}
|
|
137
|
+
<p>Created {passkey.createdAt.toLocaleString()}</p>
|
|
124
138
|
{@render action('edit_passkey#' + passkey.id)}
|
|
125
139
|
{#if passkeys.length > 1}
|
|
126
140
|
{@render action('delete_passkey#' + passkey.id, 'trash')}
|
|
127
141
|
{:else}
|
|
128
142
|
<dfn title="You must have at least one passkey" class="disabled">
|
|
129
|
-
<Icon i="trash-slash" --fill="#888" />
|
|
143
|
+
<Icon i="trash-slash" --fill="#888" --size="16px" />
|
|
130
144
|
</dfn>
|
|
131
145
|
{/if}
|
|
132
146
|
</div>
|
|
@@ -153,12 +167,56 @@
|
|
|
153
167
|
<p>Are you sure you want to delete this passkey?<br />This action can't be undone.</p>
|
|
154
168
|
</FormDialog>
|
|
155
169
|
{/each}
|
|
156
|
-
<
|
|
170
|
+
<span>
|
|
171
|
+
<button onclick={() => createPasskey(user.id).then(passkeys.push.bind(passkeys))}><Icon i="plus" /> Create</button>
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="section main">
|
|
176
|
+
<h3>Sessions</h3>
|
|
177
|
+
{#each sessions as session}
|
|
178
|
+
<div class="item session">
|
|
179
|
+
<p>
|
|
180
|
+
{session.id.slice(0, 4)}...{session.id.slice(-4)}
|
|
181
|
+
{#if session.id == currentSession.id}
|
|
182
|
+
<span class="current">Current</span>
|
|
183
|
+
{/if}
|
|
184
|
+
{#if session.elevated}
|
|
185
|
+
<span class="elevated">Elevated</span>
|
|
186
|
+
{/if}
|
|
187
|
+
</p>
|
|
188
|
+
<p>Created {session.created.toLocaleString()}</p>
|
|
189
|
+
<p>Expires {session.expires.toLocaleString()}</p>
|
|
190
|
+
{@render action('logout#' + session.id, 'right-from-bracket')}
|
|
191
|
+
</div>
|
|
192
|
+
<FormDialog
|
|
193
|
+
bind:dialog={dialogs['logout#' + session.id]}
|
|
194
|
+
submit={() =>
|
|
195
|
+
logout(user.id, session.id).then(() => {
|
|
196
|
+
if (session.id == currentSession.id) goto('/');
|
|
197
|
+
else sessions.splice(sessions.indexOf(session), 1);
|
|
198
|
+
})}
|
|
199
|
+
submitText="Logout"
|
|
200
|
+
>
|
|
201
|
+
<p>Are you sure you want to log out this session?</p>
|
|
202
|
+
</FormDialog>
|
|
203
|
+
{/each}
|
|
204
|
+
<span>
|
|
205
|
+
<button onclick={() => dialogs.logout_all.showModal()} class="danger">Logout All</button>
|
|
206
|
+
</span>
|
|
207
|
+
<FormDialog
|
|
208
|
+
bind:dialog={dialogs['logout_all']}
|
|
209
|
+
submit={() => logoutAll(user.id).then(() => goto('/'))}
|
|
210
|
+
submitText="Logout All Sessions"
|
|
211
|
+
submitDanger
|
|
212
|
+
>
|
|
213
|
+
<p>Are you sure you want to log out all sessions?</p>
|
|
214
|
+
</FormDialog>
|
|
157
215
|
</div>
|
|
158
216
|
</div>
|
|
159
217
|
{:catch error}
|
|
160
218
|
<div class="error">
|
|
161
|
-
<h3>Failed to load
|
|
219
|
+
<h3>Failed to load account</h3>
|
|
162
220
|
<p>{'message' in error ? error.message : error}</p>
|
|
163
221
|
</div>
|
|
164
222
|
{/await}
|
|
@@ -191,12 +249,16 @@
|
|
|
191
249
|
|
|
192
250
|
.section .item {
|
|
193
251
|
display: grid;
|
|
194
|
-
grid-template-columns: 10em 1fr 2em;
|
|
195
252
|
align-items: center;
|
|
196
253
|
width: 100%;
|
|
197
254
|
gap: 1em;
|
|
198
255
|
text-wrap: nowrap;
|
|
256
|
+
border-top: 1px solid #8888;
|
|
199
257
|
padding-bottom: 1em;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.info {
|
|
261
|
+
grid-template-columns: 10em 1fr 2em;
|
|
200
262
|
|
|
201
263
|
> :first-child {
|
|
202
264
|
margin-left: 1em;
|
|
@@ -204,21 +266,26 @@
|
|
|
204
266
|
}
|
|
205
267
|
|
|
206
268
|
.passkey {
|
|
207
|
-
display: grid;
|
|
208
269
|
grid-template-columns: 1em 1em 1fr 1fr 1em 1em;
|
|
209
|
-
border-top: 1px solid #8888;
|
|
210
|
-
align-items: center;
|
|
211
|
-
width: 100%;
|
|
212
|
-
gap: 1em;
|
|
213
|
-
text-wrap: nowrap;
|
|
214
|
-
padding-bottom: 1em;
|
|
215
270
|
|
|
216
|
-
dfn {
|
|
271
|
+
dfn:not(.disabled) {
|
|
217
272
|
cursor: help;
|
|
218
273
|
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.session {
|
|
277
|
+
grid-template-columns: 1fr 1fr 1fr 1em;
|
|
278
|
+
|
|
279
|
+
.current {
|
|
280
|
+
border-radius: 2em;
|
|
281
|
+
padding: 0 0.5em;
|
|
282
|
+
background-color: #337;
|
|
283
|
+
}
|
|
219
284
|
|
|
220
|
-
|
|
221
|
-
|
|
285
|
+
.elevated {
|
|
286
|
+
border-radius: 2em;
|
|
287
|
+
padding: 0 0.5em;
|
|
288
|
+
background-color: #733;
|
|
222
289
|
}
|
|
223
290
|
}
|
|
224
291
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RequestMethod } from '@axium/core/requests';
|
|
2
|
-
import { resolveRoute } from '@axium/server/routes
|
|
3
|
-
import { config } from '@axium/server/config
|
|
2
|
+
import { resolveRoute } from '@axium/server/routes';
|
|
3
|
+
import { config } from '@axium/server/config';
|
|
4
4
|
import { error, json, type RequestEvent, type RequestHandler } from '@sveltejs/kit';
|
|
5
5
|
import z from 'zod/v4';
|
|
6
6
|
|