@axium/server 0.7.6 → 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.
Files changed (57) hide show
  1. package/dist/apps.d.ts +15 -0
  2. package/dist/apps.js +20 -0
  3. package/dist/auth.d.ts +53 -30
  4. package/dist/auth.js +103 -130
  5. package/dist/cli.js +176 -42
  6. package/dist/config.d.ts +57 -312
  7. package/dist/config.js +65 -31
  8. package/dist/database.d.ts +31 -40
  9. package/dist/database.js +165 -62
  10. package/dist/io.js +6 -2
  11. package/dist/plugins.d.ts +8 -24
  12. package/dist/plugins.js +10 -14
  13. package/dist/routes.d.ts +55 -0
  14. package/dist/routes.js +54 -0
  15. package/package.json +11 -16
  16. package/web/api/index.ts +7 -0
  17. package/web/api/metadata.ts +35 -0
  18. package/web/api/passkeys.ts +56 -0
  19. package/web/api/readme.md +1 -0
  20. package/web/api/register.ts +83 -0
  21. package/web/api/schemas.ts +22 -0
  22. package/web/api/session.ts +33 -0
  23. package/web/api/users.ts +351 -0
  24. package/web/api/utils.ts +66 -0
  25. package/web/auth.ts +1 -5
  26. package/web/hooks.server.ts +12 -1
  27. package/web/index.server.ts +0 -1
  28. package/web/lib/ClipboardCopy.svelte +42 -0
  29. package/web/lib/Dialog.svelte +3 -6
  30. package/web/lib/FormDialog.svelte +61 -14
  31. package/web/lib/Toast.svelte +8 -1
  32. package/web/lib/UserCard.svelte +1 -1
  33. package/web/lib/auth.ts +12 -0
  34. package/web/lib/icons/Icon.svelte +7 -13
  35. package/web/lib/index.ts +0 -2
  36. package/web/lib/styles.css +18 -1
  37. package/web/routes/+layout.svelte +1 -1
  38. package/web/routes/[...path]/+page.server.ts +13 -0
  39. package/web/routes/[appId]/[...page]/+page.server.ts +14 -0
  40. package/web/routes/_axium/default/+page.svelte +11 -0
  41. package/web/routes/account/+page.svelte +291 -0
  42. package/web/routes/api/[...path]/+server.ts +49 -0
  43. package/web/routes/login/+page.svelte +25 -0
  44. package/web/routes/logout/+page.svelte +13 -0
  45. package/web/routes/register/+page.svelte +21 -0
  46. package/web/tsconfig.json +2 -1
  47. package/web/utils.ts +9 -15
  48. package/web/actions.ts +0 -58
  49. package/web/lib/Account.svelte +0 -76
  50. package/web/lib/SignUp.svelte +0 -20
  51. package/web/lib/account.css +0 -36
  52. package/web/routes/+page.server.ts +0 -16
  53. package/web/routes/+page.svelte +0 -10
  54. package/web/routes/name/+page.server.ts +0 -5
  55. package/web/routes/name/+page.svelte +0 -20
  56. package/web/routes/signup/+page.server.ts +0 -10
  57. package/web/routes/signup/+page.svelte +0 -15
@@ -1,12 +1,9 @@
1
1
  <script>
2
- let { show, children, onclose = () => (show = false), ...rest } = $props();
3
-
4
- let dialog = $state();
5
- $effect(() => show && dialog.showModal());
6
- $effect(() => !show && dialog.close());
2
+ let { children, dialog = $bindable(), ...rest } = $props();
3
+ import './styles.css';
7
4
  </script>
8
5
 
9
- <dialog bind:this={dialog} {onclose} {...rest}>
6
+ <dialog bind:this={dialog} {...rest}>
10
7
  {@render children()}
11
8
  </dialog>
12
9
 
@@ -1,35 +1,82 @@
1
1
  <script lang="ts">
2
- import { enhance } from '$app/forms';
2
+ import { goto } from '$app/navigation';
3
+ import { page } from '$app/state';
3
4
  import Dialog from './Dialog.svelte';
4
5
  import './styles.css';
5
6
 
6
- let { children, active = $bindable(null), form, submitText = 'Submit', oncancel = () => {}, action = '', pageMode = false, ...rest } = $props();
7
+ function resolveRedirectAfter() {
8
+ const maybe = page.url.searchParams.get('after');
9
+ if (!maybe || maybe == page.url.pathname) return '/';
10
+ for (const prefix of ['/api/']) if (maybe.startsWith(prefix)) return '/';
11
+ return maybe || '/';
12
+ }
13
+
14
+ let {
15
+ children,
16
+ dialog = $bindable(),
17
+ submitText = 'Submit',
18
+ cancel = () => {},
19
+ submit = (data: object): Promise<any> => Promise.resolve(),
20
+ pageMode = false,
21
+ submitDanger = false,
22
+ ...rest
23
+ }: {
24
+ children(): any;
25
+ dialog?: HTMLDialogElement;
26
+ /** Change the text displayed for the submit button */
27
+ submitText?: string;
28
+ /** Basically a callback for when the dialog is canceled */
29
+ cancel?(): unknown;
30
+ /** Called on submission, this should do the actual submission */
31
+ submit?(data: Record<string, FormDataEntryValue>): Promise<any>;
32
+ /** Whether to display the dialog as a full-page form */
33
+ pageMode?: boolean;
34
+ submitDanger?: boolean;
35
+ } = $props();
36
+
37
+ let error = $state(null);
7
38
 
8
39
  $effect(() => {
9
- if (form?.success) active = null;
40
+ if (pageMode) dialog.showModal();
10
41
  });
11
42
 
12
- const show = $derived(!!active || pageMode);
43
+ function onclose(e?: MouseEvent) {
44
+ e.preventDefault();
45
+ cancel();
46
+ }
13
47
 
14
- function onclick(e: MouseEvent) {
48
+ function onsubmit(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
15
49
  e.preventDefault();
16
- active = null;
17
- oncancel(e);
50
+ const data = Object.fromEntries(new FormData(e.currentTarget));
51
+ submit(data)
52
+ .then(result => {
53
+ if (pageMode) goto(resolveRedirectAfter());
54
+ else dialog.close();
55
+ })
56
+ .catch((e: unknown) => {
57
+ if (!e) error = 'An unknown error occurred';
58
+ else if (typeof e == 'object' && 'message' in e) error = e.message;
59
+ else error = e;
60
+ });
18
61
  }
19
62
  </script>
20
63
 
21
- <Dialog {show} onclose={() => (active = null)}>
22
- <form method="POST" {action} use:enhance class="main" {...rest}>
23
- {#if form?.error}
24
- <div class="error">{form.error}</div>
64
+ {#snippet submitButton()}
65
+ <button type="submit" class={['submit', submitDanger && 'danger']}>{submitText}</button>
66
+ {/snippet}
67
+
68
+ <Dialog bind:dialog {onclose} {...rest}>
69
+ <form {onsubmit} class="main" method="dialog">
70
+ {#if error}
71
+ <div class="error">{error}</div>
25
72
  {/if}
26
73
  {@render children()}
27
74
  {#if pageMode}
28
- <button type="submit" class="submit">{submitText}</button>
75
+ {@render submitButton()}
29
76
  {:else}
30
77
  <div class="actions">
31
- <button type="button" {onclick}>Cancel</button>
32
- <button type="submit" class="submit">{submitText}</button>
78
+ <button type="button" onclick={() => dialog.close()}>Cancel</button>
79
+ {@render submitButton()}
33
80
  </div>
34
81
  {/if}
35
82
  </form>
@@ -9,7 +9,14 @@
9
9
  </script>
10
10
 
11
11
  {#if show}
12
- <div class="Toast" in:fade|global={{ duration }} onintroend={() => (hiding = true)} out:fade|global={{ delay, duration }} onoutroend={() => (hiding = false)} {...rest}>
12
+ <div
13
+ class="Toast"
14
+ in:fade|global={{ duration }}
15
+ onintroend={() => (hiding = true)}
16
+ out:fade|global={{ delay, duration }}
17
+ onoutroend={() => (hiding = false)}
18
+ {...rest}
19
+ >
13
20
  {@render children()}
14
21
  </div>
15
22
  {/if}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { User } from '@axium/core/schemas';
2
+ import type { User } from '@axium/core/user';
3
3
  import { getUserImage } from '@axium/core';
4
4
 
5
5
  const {
@@ -0,0 +1,12 @@
1
+ import type { SessionInternal, UserInternal } from '@axium/server/auth';
2
+ import { getSessionAndUser } from '@axium/server/auth';
3
+ import type { RequestEvent } from '@sveltejs/kit';
4
+
5
+ export async function authenticate(event: RequestEvent): Promise<(SessionInternal & { user: UserInternal | null }) | null> {
6
+ const maybe_header = event.request.headers.get('Authorization');
7
+ const token = maybe_header?.startsWith('Bearer ') ? maybe_header.slice(7) : event.cookies.get('session_token');
8
+
9
+ if (!token) return null;
10
+
11
+ return await getSessionAndUser(token).catch(() => null);
12
+ }
@@ -5,25 +5,19 @@
5
5
  const urls = { light, solid, regular };
6
6
  const { i } = $props();
7
7
 
8
- const [style, id] = i.includes('/') ? i.split('/') : ['solid', i];
9
- const url = urls[style];
8
+ const [style, id] = $derived(i.includes('/') ? i.split('/') : ['solid', i]);
9
+ const url = $derived(urls[style]);
10
10
  </script>
11
11
 
12
- <svelte:head>
13
- <link rel="preload" href={url} />
14
- </svelte:head>
15
-
16
- <span>
17
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em">
18
- <use href="{url}#{id}" />
19
- </svg>
20
- </span>
12
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em">
13
+ <use href="{url}#{id}" />
14
+ </svg>
21
15
 
22
16
  <style>
23
- span {
17
+ svg {
24
18
  width: var(--size, 1em);
25
19
  height: var(--size, 1em);
26
20
  display: inline-block;
27
- fill: #bbb;
21
+ fill: var(--fill, #bbb);
28
22
  }
29
23
  </style>
package/web/lib/index.ts CHANGED
@@ -1,7 +1,5 @@
1
- export { default as Account } from './Account.svelte';
2
1
  export { default as Dialog } from './Dialog.svelte';
3
2
  export { default as FormDialog } from './FormDialog.svelte';
4
3
  export * from './icons/index.js';
5
- export { default as SignUp } from './SignUp.svelte';
6
4
  export { default as Toast } from './Toast.svelte';
7
5
  export { default as UserCard } from './UserCard.svelte';
@@ -4,6 +4,7 @@ body {
4
4
  background-color: #222;
5
5
  color: #bbb;
6
6
  accent-color: #bbb;
7
+ overflow-y: scroll;
7
8
  }
8
9
 
9
10
  .main {
@@ -15,7 +16,7 @@ body {
15
16
  gap: 1em;
16
17
  }
17
18
 
18
- div:has(form.main) {
19
+ .main-container:has(form.main) {
19
20
  position: absolute;
20
21
  inset: 0;
21
22
  display: flex;
@@ -83,3 +84,19 @@ button:hover {
83
84
  gap: 1em;
84
85
  overflow-y: scroll;
85
86
  }
87
+
88
+ .danger {
89
+ border: 1px solid #d99;
90
+ background-color: #322;
91
+ color: #dbb;
92
+ accent-color: #dbb;
93
+ }
94
+
95
+ .danger:hover {
96
+ background-color: #633;
97
+ }
98
+
99
+ :disabled,
100
+ .disabled {
101
+ cursor: not-allowed;
102
+ }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import '../lib/styles.css';
2
+ import '$lib/styles.css';
3
3
  const { children } = $props();
4
4
  </script>
5
5
 
@@ -0,0 +1,13 @@
1
+ import { error, redirect, type LoadEvent } from '@sveltejs/kit';
2
+ import { resolveRoute } from '@axium/server/routes';
3
+
4
+ export async function load(event: LoadEvent) {
5
+ const route = resolveRoute(event);
6
+
7
+ if (!route && event.url.pathname === '/') redirect(303, '/_axium/default');
8
+ if (!route) error(404);
9
+
10
+ if (route.server == true) error(409, 'This is a server route, not a page route');
11
+
12
+ return await route.load(event);
13
+ }
@@ -0,0 +1,14 @@
1
+ import { error, type LoadEvent } from '@sveltejs/kit';
2
+ import { apps } from '@axium/server/apps';
3
+
4
+ export async function load(event: LoadEvent) {
5
+ const app = apps.get(event.params.appId);
6
+
7
+ if (!app) error(404);
8
+
9
+ const route = app.resolveRoute(event);
10
+
11
+ if (!route) error(404);
12
+
13
+ return await route.load(event);
14
+ }
@@ -0,0 +1,11 @@
1
+ <div class="main">
2
+ <h1>It works!</h1>
3
+ <p>This is the default Axium page. You'll want to change it.</p>
4
+ <h3>Helpful links</h3>
5
+ <ul>
6
+ <li><a href="/register">Register</a></li>
7
+ <li><a href="/login">Login</a></li>
8
+ <li><a href="/logout">Logout</a></li>
9
+ <li><a href="/account">Account</a></li>
10
+ </ul>
11
+ </div>
@@ -0,0 +1,291 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+ import ClipboardCopy from '$lib/ClipboardCopy.svelte';
4
+ import FormDialog from '$lib/FormDialog.svelte';
5
+ import Icon from '$lib/icons/Icon.svelte';
6
+ import {
7
+ createPasskey,
8
+ deletePasskey,
9
+ deleteUser,
10
+ emailVerificationEnabled,
11
+ getCurrentSession,
12
+ getPasskeys,
13
+ getSessions,
14
+ logout,
15
+ logoutAll,
16
+ sendVerificationEmail,
17
+ updatePasskey,
18
+ updateUser,
19
+ } from '@axium/client/user';
20
+ import type { Passkey, Session } from '@axium/core/api';
21
+ import { getUserImage, type User } from '@axium/core/user';
22
+
23
+ const dialogs = $state<Record<string, HTMLDialogElement>>({});
24
+
25
+ let verificationSent = $state(false);
26
+ let currentSession = $state<Session & { user: User }>();
27
+ let user = $state<User>();
28
+ let canVerify = $state(false);
29
+ let passkeys = $state<Passkey[]>([]);
30
+ let sessions = $state<Session[]>([]);
31
+
32
+ async function ready() {
33
+ currentSession = await getCurrentSession().catch(() => {
34
+ goto('/login?after=/account');
35
+ return null;
36
+ })!;
37
+ user = currentSession.user;
38
+
39
+ passkeys = await getPasskeys(user.id);
40
+
41
+ sessions = await getSessions(user.id);
42
+
43
+ canVerify = await emailVerificationEnabled(user.id);
44
+ }
45
+
46
+ async function _editUser(data) {
47
+ const result = await updateUser(user.id, data);
48
+ user = result;
49
+ }
50
+ </script>
51
+
52
+ <svelte:head>
53
+ <title>Account</title>
54
+ </svelte:head>
55
+
56
+ {#snippet action(name: string, i: string = 'pen')}
57
+ <button style:display="contents" onclick={() => dialogs[name].showModal()}>
58
+ <Icon {i} --size="16px" />
59
+ </button>
60
+ {/snippet}
61
+
62
+ {#await ready() then}
63
+ <div class="Account flex-content">
64
+ <img class="pfp" src={getUserImage(user)} alt="User profile" />
65
+ <p class="greeting">Welcome, {user.name}</p>
66
+
67
+ <div class="section main">
68
+ <h3>Personal Information</h3>
69
+ <div class="item info">
70
+ <p class="subtle">Name</p>
71
+ <p>{user.name}</p>
72
+ {@render action('edit_name')}
73
+ </div>
74
+ <FormDialog bind:dialog={dialogs.edit_name} submit={_editUser} submitText="Change">
75
+ <div>
76
+ <label for="name">What do you want to be called?</label>
77
+ <input name="name" type="text" value={user.name || ''} required />
78
+ </div>
79
+ </FormDialog>
80
+ <div class="item info">
81
+ <p class="subtle">Email</p>
82
+ <p>
83
+ {user.email}
84
+ {#if user.emailVerified}
85
+ <dfn title="Email verified on {user.emailVerified.toLocaleDateString()}">
86
+ <Icon i="regular/circle-check" />
87
+ </dfn>
88
+ {:else if canVerify}
89
+ <button onclick={() => sendVerificationEmail(user.id).then(() => (verificationSent = true))}>
90
+ {verificationSent ? 'Verification email sent' : 'Verify'}
91
+ </button>
92
+ {/if}
93
+ </p>
94
+ {@render action('edit_email')}
95
+ </div>
96
+ <FormDialog bind:dialog={dialogs.edit_email} submit={_editUser} submitText="Change">
97
+ <div>
98
+ <label for="email">Email Address</label>
99
+ <input name="email" type="email" value={user.email || ''} required />
100
+ </div>
101
+ </FormDialog>
102
+
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>
105
+ <p>{user.id}</p>
106
+ <ClipboardCopy value={user.id} --size="16px" />
107
+ </div>
108
+ <span>
109
+ <a class="signout" href="/logout"><button>Sign out</button></a>
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
116
+ >
117
+ <p>Are you sure you want to delete your account?<br />This action can't be undone.</p>
118
+ </FormDialog>
119
+ </span>
120
+ </div>
121
+
122
+ <div class="section main">
123
+ <h3>Passkeys</h3>
124
+ {#each passkeys as passkey}
125
+ <div class="item passkey">
126
+ <dfn title={passkey.deviceType == 'multiDevice' ? 'Multiple devices' : 'Single device'}>
127
+ <Icon i={passkey.deviceType == 'multiDevice' ? 'laptop-mobile' : 'mobile'} --size="16px" />
128
+ </dfn>
129
+ <dfn title="This passkey is {passkey.backedUp ? '' : 'not '}backed up">
130
+ <Icon i={passkey.backedUp ? 'circle-check' : 'circle-xmark'} --size="16px" />
131
+ </dfn>
132
+ {#if passkey.name}
133
+ <p>{passkey.name}</p>
134
+ {:else}
135
+ <p class="subtle"><i>Unnamed</i></p>
136
+ {/if}
137
+ <p>Created {passkey.createdAt.toLocaleString()}</p>
138
+ {@render action('edit_passkey#' + passkey.id)}
139
+ {#if passkeys.length > 1}
140
+ {@render action('delete_passkey#' + passkey.id, 'trash')}
141
+ {:else}
142
+ <dfn title="You must have at least one passkey" class="disabled">
143
+ <Icon i="trash-slash" --fill="#888" --size="16px" />
144
+ </dfn>
145
+ {/if}
146
+ </div>
147
+ <FormDialog
148
+ bind:dialog={dialogs['edit_passkey#' + passkey.id]}
149
+ submit={data => {
150
+ if (typeof data.name != 'string') throw 'Passkey name must be a string';
151
+ passkey.name = data.name;
152
+ return updatePasskey(passkey.id, data);
153
+ }}
154
+ submitText="Change"
155
+ >
156
+ <div>
157
+ <label for="name">Passkey Name</label>
158
+ <input name="name" type="text" value={passkey.name || ''} />
159
+ </div>
160
+ </FormDialog>
161
+ <FormDialog
162
+ bind:dialog={dialogs['delete_passkey#' + passkey.id]}
163
+ submit={() => deletePasskey(passkey.id).then(() => passkeys.splice(passkeys.indexOf(passkey), 1))}
164
+ submitText="Delete"
165
+ submitDanger={true}
166
+ >
167
+ <p>Are you sure you want to delete this passkey?<br />This action can't be undone.</p>
168
+ </FormDialog>
169
+ {/each}
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>
215
+ </div>
216
+ </div>
217
+ {:catch error}
218
+ <div class="error">
219
+ <h3>Failed to load account</h3>
220
+ <p>{'message' in error ? error.message : error}</p>
221
+ </div>
222
+ {/await}
223
+
224
+ <style>
225
+ .pfp {
226
+ width: 100px;
227
+ height: 100px;
228
+ border-radius: 50%;
229
+ border: 1px solid #8888;
230
+ margin-top: 3em;
231
+ }
232
+
233
+ .greeting {
234
+ font-size: 2em;
235
+ }
236
+
237
+ .signout {
238
+ margin-top: 2em;
239
+ }
240
+
241
+ .section {
242
+ width: 50%;
243
+ padding-top: 4em;
244
+
245
+ > div:has(+ div) {
246
+ border-bottom: 1px solid #8888;
247
+ }
248
+ }
249
+
250
+ .section .item {
251
+ display: grid;
252
+ align-items: center;
253
+ width: 100%;
254
+ gap: 1em;
255
+ text-wrap: nowrap;
256
+ border-top: 1px solid #8888;
257
+ padding-bottom: 1em;
258
+ }
259
+
260
+ .info {
261
+ grid-template-columns: 10em 1fr 2em;
262
+
263
+ > :first-child {
264
+ margin-left: 1em;
265
+ }
266
+ }
267
+
268
+ .passkey {
269
+ grid-template-columns: 1em 1em 1fr 1fr 1em 1em;
270
+
271
+ dfn:not(.disabled) {
272
+ cursor: help;
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
+ }
284
+
285
+ .elevated {
286
+ border-radius: 2em;
287
+ padding: 0 0.5em;
288
+ background-color: #733;
289
+ }
290
+ }
291
+ </style>
@@ -0,0 +1,49 @@
1
+ import type { RequestMethod } from '@axium/core/requests';
2
+ import { resolveRoute } from '@axium/server/routes';
3
+ import { config } from '@axium/server/config';
4
+ import { error, json, type RequestEvent, type RequestHandler } from '@sveltejs/kit';
5
+ import z from 'zod/v4';
6
+
7
+ function handler(method: RequestMethod): RequestHandler {
8
+ return async function (event: RequestEvent): Promise<Response> {
9
+ const _warnings: string[] = [];
10
+ if (!event.request.headers.get('Accept')?.includes('application/json')) {
11
+ _warnings.push('Only application/json is supported');
12
+ event.request.headers.set('Accept', 'application/json');
13
+ }
14
+
15
+ const route = resolveRoute(event);
16
+
17
+ if (!route) error(404, 'Route not found');
18
+ if (!route.server) error(503, 'Route is not a server route');
19
+
20
+ if (config.debug) console.log(event.request.method, route.path);
21
+
22
+ for (const [key, type] of Object.entries(route.params || {})) {
23
+ if (!type) continue;
24
+
25
+ try {
26
+ event.params[key] = type.parse(event.params[key]) as any;
27
+ } catch (e: any) {
28
+ error(400, `Invalid parameter: ${z.prettifyError(e)}`);
29
+ }
30
+ }
31
+
32
+ if (typeof route[method] != 'function') error(405, `Method ${method} not allowed for ${route.path}`);
33
+
34
+ const result: object & { _warnings?: string[] } = await route[method](event);
35
+
36
+ result._warnings ||= [];
37
+ result._warnings.push(..._warnings);
38
+
39
+ return json(result);
40
+ };
41
+ }
42
+
43
+ export const HEAD = handler('HEAD');
44
+ export const GET = handler('GET');
45
+ export const POST = handler('POST');
46
+ export const PUT = handler('PUT');
47
+ export const DELETE = handler('DELETE');
48
+ export const PATCH = handler('PATCH');
49
+ export const OPTIONS = handler('OPTIONS');
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import FormDialog from '$lib/FormDialog.svelte';
3
+ import { loginByEmail } from '@axium/client/user';
4
+
5
+ let { dialog = $bindable(), pageMode = true } = $props();
6
+
7
+ function submit(data) {
8
+ if (typeof data.email != 'string') {
9
+ throw 'Tried to upload a file for an email. Huh?!';
10
+ }
11
+
12
+ return loginByEmail(data.email);
13
+ }
14
+ </script>
15
+
16
+ <svelte:head>
17
+ <title>Login</title>
18
+ </svelte:head>
19
+
20
+ <FormDialog bind:dialog submitText="Login" {submit} {pageMode}>
21
+ <div>
22
+ <label for="email">Email</label>
23
+ <input name="email" type="email" required />
24
+ </div>
25
+ </FormDialog>
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+ import { logoutCurrentSession } from '@axium/client/user';
4
+ </script>
5
+
6
+ <svelte:head>
7
+ <title>Logout</title>
8
+ </svelte:head>
9
+
10
+ <div class="main">
11
+ <p>Are you sure you want to log out?</p>
12
+ <button onclick={() => logoutCurrentSession().finally(() => goto('/'))}>Log Out</button>
13
+ </div>