@axium/server 0.7.6 → 0.8.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/apps.d.ts +15 -0
- package/dist/apps.js +20 -0
- package/dist/auth.d.ts +63 -30
- package/dist/auth.js +110 -129
- package/dist/cli.js +33 -6
- package/dist/config.d.ts +241 -61
- package/dist/config.js +26 -2
- package/dist/database.d.ts +28 -37
- package/dist/database.js +124 -50
- package/dist/io.js +6 -2
- package/dist/plugins.d.ts +7 -24
- package/dist/plugins.js +9 -14
- package/dist/routes.d.ts +55 -0
- package/dist/routes.js +54 -0
- package/package.json +7 -15
- package/web/api/index.ts +7 -0
- package/web/api/metadata.ts +35 -0
- package/web/api/passkeys.ts +56 -0
- package/web/api/readme.md +1 -0
- package/web/api/register.ts +83 -0
- package/web/api/schemas.ts +22 -0
- package/web/api/session.ts +33 -0
- package/web/api/users.ts +340 -0
- package/web/api/utils.ts +66 -0
- package/web/auth.ts +1 -5
- package/web/hooks.server.ts +6 -1
- package/web/index.server.ts +0 -1
- package/web/lib/Dialog.svelte +3 -6
- package/web/lib/FormDialog.svelte +53 -14
- package/web/lib/Toast.svelte +8 -1
- package/web/lib/UserCard.svelte +1 -1
- package/web/lib/auth.ts +12 -0
- package/web/lib/icons/Icon.svelte +5 -7
- package/web/lib/index.ts +0 -2
- package/web/lib/styles.css +12 -1
- package/web/routes/+layout.svelte +1 -1
- package/web/routes/[...path]/+page.server.ts +13 -0
- package/web/routes/[appId]/[...page]/+page.server.ts +14 -0
- package/web/routes/_axium/default/+page.svelte +11 -0
- package/web/routes/account/+page.svelte +224 -0
- package/web/routes/api/[...path]/+server.ts +49 -0
- package/web/routes/login/+page.svelte +25 -0
- package/web/routes/logout/+page.svelte +13 -0
- package/web/routes/register/+page.svelte +21 -0
- package/web/tsconfig.json +2 -1
- package/web/utils.ts +9 -15
- package/web/actions.ts +0 -58
- package/web/lib/Account.svelte +0 -76
- package/web/lib/SignUp.svelte +0 -20
- package/web/lib/account.css +0 -36
- package/web/routes/+page.server.ts +0 -16
- package/web/routes/+page.svelte +0 -10
- package/web/routes/name/+page.server.ts +0 -5
- package/web/routes/name/+page.svelte +0 -20
- package/web/routes/signup/+page.server.ts +0 -10
- package/web/routes/signup/+page.svelte +0 -15
|
@@ -1,35 +1,74 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
3
|
import Dialog from './Dialog.svelte';
|
|
4
4
|
import './styles.css';
|
|
5
5
|
|
|
6
|
-
let {
|
|
6
|
+
let {
|
|
7
|
+
children,
|
|
8
|
+
dialog = $bindable(),
|
|
9
|
+
submitText = 'Submit',
|
|
10
|
+
cancel = () => {},
|
|
11
|
+
submit = (data: object): Promise<any> => Promise.resolve(),
|
|
12
|
+
pageMode = false,
|
|
13
|
+
submitDanger = false,
|
|
14
|
+
...rest
|
|
15
|
+
}: {
|
|
16
|
+
children(): any;
|
|
17
|
+
dialog?: HTMLDialogElement;
|
|
18
|
+
/** Change the text displayed for the submit button */
|
|
19
|
+
submitText?: string;
|
|
20
|
+
/** Basically a callback for when the dialog is canceled */
|
|
21
|
+
cancel?(): unknown;
|
|
22
|
+
/** Called on submission, this should do the actual submission */
|
|
23
|
+
submit?(data: Record<string, FormDataEntryValue>): Promise<any>;
|
|
24
|
+
/** Whether to display the dialog as a full-page form */
|
|
25
|
+
pageMode?: boolean;
|
|
26
|
+
submitDanger?: boolean;
|
|
27
|
+
} = $props();
|
|
28
|
+
|
|
29
|
+
let error = $state(null);
|
|
7
30
|
|
|
8
31
|
$effect(() => {
|
|
9
|
-
if (
|
|
32
|
+
if (pageMode) dialog.showModal();
|
|
10
33
|
});
|
|
11
34
|
|
|
12
|
-
|
|
35
|
+
function onclose(e?: MouseEvent) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
cancel();
|
|
38
|
+
}
|
|
13
39
|
|
|
14
|
-
function
|
|
40
|
+
function onsubmit(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
|
|
15
41
|
e.preventDefault();
|
|
16
|
-
|
|
17
|
-
|
|
42
|
+
const data = Object.fromEntries(new FormData(e.currentTarget));
|
|
43
|
+
submit(data)
|
|
44
|
+
.then(result => {
|
|
45
|
+
if (pageMode) goto('/');
|
|
46
|
+
else dialog.close();
|
|
47
|
+
})
|
|
48
|
+
.catch((e: unknown) => {
|
|
49
|
+
if (!e) error = 'An unknown error occurred';
|
|
50
|
+
else if (typeof e == 'object' && 'message' in e) error = e.message;
|
|
51
|
+
else error = e;
|
|
52
|
+
});
|
|
18
53
|
}
|
|
19
54
|
</script>
|
|
20
55
|
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
{#snippet submitButton()}
|
|
57
|
+
<button type="submit" class={['submit', submitDanger && 'danger']}>{submitText}</button>
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
60
|
+
<Dialog bind:dialog {onclose} {...rest}>
|
|
61
|
+
<form {onsubmit} class="main" method="dialog">
|
|
62
|
+
{#if error}
|
|
63
|
+
<div class="error">{error}</div>
|
|
25
64
|
{/if}
|
|
26
65
|
{@render children()}
|
|
27
66
|
{#if pageMode}
|
|
28
|
-
|
|
67
|
+
{@render submitButton()}
|
|
29
68
|
{:else}
|
|
30
69
|
<div class="actions">
|
|
31
|
-
<button type="button" {
|
|
32
|
-
|
|
70
|
+
<button type="button" onclick={() => dialog.close()}>Cancel</button>
|
|
71
|
+
{@render submitButton()}
|
|
33
72
|
</div>
|
|
34
73
|
{/if}
|
|
35
74
|
</form>
|
package/web/lib/Toast.svelte
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
{#if show}
|
|
12
|
-
<div
|
|
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}
|
package/web/lib/UserCard.svelte
CHANGED
package/web/lib/auth.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SessionInternal, UserInternal } from '@axium/server/auth.js';
|
|
2
|
+
import { getSessionAndUser } from '@axium/server/auth.js';
|
|
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
|
+
}
|
|
@@ -13,17 +13,15 @@
|
|
|
13
13
|
<link rel="preload" href={url} />
|
|
14
14
|
</svelte:head>
|
|
15
15
|
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
</svg>
|
|
20
|
-
</span>
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em">
|
|
17
|
+
<use href="{url}#{id}" />
|
|
18
|
+
</svg>
|
|
21
19
|
|
|
22
20
|
<style>
|
|
23
|
-
|
|
21
|
+
svg {
|
|
24
22
|
width: var(--size, 1em);
|
|
25
23
|
height: var(--size, 1em);
|
|
26
24
|
display: inline-block;
|
|
27
|
-
fill: #bbb;
|
|
25
|
+
fill: var(--fill, #bbb);
|
|
28
26
|
}
|
|
29
27
|
</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';
|
package/web/lib/styles.css
CHANGED
|
@@ -15,7 +15,7 @@ body {
|
|
|
15
15
|
gap: 1em;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
div:has(form.main) {
|
|
18
|
+
div:not(.main):has(form.main) {
|
|
19
19
|
position: absolute;
|
|
20
20
|
inset: 0;
|
|
21
21
|
display: flex;
|
|
@@ -83,3 +83,14 @@ button:hover {
|
|
|
83
83
|
gap: 1em;
|
|
84
84
|
overflow-y: scroll;
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
.danger {
|
|
88
|
+
border: 1px solid #d99;
|
|
89
|
+
background-color: #322;
|
|
90
|
+
color: #dbb;
|
|
91
|
+
accent-color: #dbb;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.danger:hover {
|
|
95
|
+
background-color: #633;
|
|
96
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { error, redirect, type LoadEvent } from '@sveltejs/kit';
|
|
2
|
+
import { resolveRoute } from '@axium/server/routes.js';
|
|
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.js';
|
|
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,224 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import FormDialog from '$lib/FormDialog.svelte';
|
|
3
|
+
import Icon from '$lib/icons/Icon.svelte';
|
|
4
|
+
import {
|
|
5
|
+
currentSession,
|
|
6
|
+
deletePasskey,
|
|
7
|
+
getPasskeys,
|
|
8
|
+
sendVerificationEmail,
|
|
9
|
+
updatePasskey,
|
|
10
|
+
updateUser,
|
|
11
|
+
createPasskey,
|
|
12
|
+
deleteUser,
|
|
13
|
+
} from '@axium/client/user';
|
|
14
|
+
import type { Passkey } from '@axium/core/api';
|
|
15
|
+
import { getUserImage, type User } from '@axium/core/user';
|
|
16
|
+
|
|
17
|
+
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
18
|
+
|
|
19
|
+
let verificationSent = $state(false);
|
|
20
|
+
let user = $state<User>();
|
|
21
|
+
|
|
22
|
+
async function ready() {
|
|
23
|
+
const session = await currentSession();
|
|
24
|
+
user = session.user;
|
|
25
|
+
|
|
26
|
+
passkeys = await getPasskeys(user.id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let passkeys = $state<Passkey[]>([]);
|
|
30
|
+
|
|
31
|
+
async function _editUser(data) {
|
|
32
|
+
const result = await updateUser(user.id, data);
|
|
33
|
+
user = result;
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<svelte:head>
|
|
38
|
+
<title>Account</title>
|
|
39
|
+
</svelte:head>
|
|
40
|
+
|
|
41
|
+
{#snippet action(name: string, i: string = 'pen')}
|
|
42
|
+
<button
|
|
43
|
+
style:display="contents"
|
|
44
|
+
style:cursor="pointer"
|
|
45
|
+
onclick={() => {
|
|
46
|
+
dialogs[name].showModal();
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<Icon {i} />
|
|
50
|
+
</button>
|
|
51
|
+
{/snippet}
|
|
52
|
+
|
|
53
|
+
{#await ready() then}
|
|
54
|
+
<div class="Account flex-content">
|
|
55
|
+
<img class="pfp" src={getUserImage(user)} alt="User profile" />
|
|
56
|
+
<p class="greeting">Welcome, {user.name}</p>
|
|
57
|
+
|
|
58
|
+
<div class="section main">
|
|
59
|
+
<div class="item">
|
|
60
|
+
<span class="subtle">Name</span>
|
|
61
|
+
<span>{user.name}</span>
|
|
62
|
+
{@render action('edit_name')}
|
|
63
|
+
</div>
|
|
64
|
+
<FormDialog bind:dialog={dialogs.edit_name} submit={_editUser} submitText="Change">
|
|
65
|
+
<div>
|
|
66
|
+
<label for="name">What do you want to be called?</label>
|
|
67
|
+
<input name="name" type="text" value={user.name || ''} required />
|
|
68
|
+
</div>
|
|
69
|
+
</FormDialog>
|
|
70
|
+
<div class="item">
|
|
71
|
+
<span class="subtle">Email</span>
|
|
72
|
+
<span>
|
|
73
|
+
{user.email}
|
|
74
|
+
{#if user.emailVerified}
|
|
75
|
+
<dfn title="Email verified on {new Date(user.emailVerified).toLocaleDateString()}">
|
|
76
|
+
<Icon i="regular/circle-check" />
|
|
77
|
+
</dfn>
|
|
78
|
+
{:else}
|
|
79
|
+
<button onclick={() => sendVerificationEmail(user.id).then(() => (verificationSent = true))}>
|
|
80
|
+
{verificationSent ? 'Verification email sent' : 'Verify'}
|
|
81
|
+
</button>
|
|
82
|
+
{/if}
|
|
83
|
+
</span>
|
|
84
|
+
{@render action('edit_email')}
|
|
85
|
+
</div>
|
|
86
|
+
<FormDialog bind:dialog={dialogs.edit_email} submit={_editUser} submitText="Change">
|
|
87
|
+
<div>
|
|
88
|
+
<label for="email">Email Address</label>
|
|
89
|
+
<input name="email" type="email" value={user.email || ''} required />
|
|
90
|
+
</div>
|
|
91
|
+
</FormDialog>
|
|
92
|
+
|
|
93
|
+
<div class="item">
|
|
94
|
+
<p class="subtle">User ID <dfn title="This is your UUID."><Icon i="regular/circle-info" /></dfn></p>
|
|
95
|
+
<p>{user.id}</p>
|
|
96
|
+
</div>
|
|
97
|
+
<span>
|
|
98
|
+
<a class="signout" href="/logout"><button>Sign out</button></a>
|
|
99
|
+
<button style:cursor="pointer" onclick={() => dialogs.delete.showModal()} style:width="fit-content" class="danger"
|
|
100
|
+
>Delete Account</button
|
|
101
|
+
>
|
|
102
|
+
<FormDialog bind:dialog={dialogs.delete} submit={() => deleteUser(user.id)} submitText="Delete Account" submitDanger>
|
|
103
|
+
<p>Are you sure you want to delete your account?<br />This action can't be undone.</p>
|
|
104
|
+
</FormDialog>
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="section main">
|
|
109
|
+
<h3>Passkeys</h3>
|
|
110
|
+
{#each passkeys as passkey}
|
|
111
|
+
<div class="passkey">
|
|
112
|
+
<dfn title={passkey.deviceType == 'multiDevice' ? 'Multiple devices' : 'Single device'}>
|
|
113
|
+
<Icon i={passkey.deviceType == 'multiDevice' ? 'laptop-mobile' : 'mobile'} />
|
|
114
|
+
</dfn>
|
|
115
|
+
<dfn title="This passkey is {passkey.backedUp ? '' : 'not '}backed up">
|
|
116
|
+
<Icon i={passkey.backedUp ? 'circle-check' : 'circle-xmark'} />
|
|
117
|
+
</dfn>
|
|
118
|
+
<p>Created {new Date(passkey.createdAt).toLocaleString()}</p>
|
|
119
|
+
{#if passkey.name}
|
|
120
|
+
<p>{passkey.name}</p>
|
|
121
|
+
{:else}
|
|
122
|
+
<p class="subtle"><i>Unnamed</i></p>
|
|
123
|
+
{/if}
|
|
124
|
+
{@render action('edit_passkey#' + passkey.id)}
|
|
125
|
+
{#if passkeys.length > 1}
|
|
126
|
+
{@render action('delete_passkey#' + passkey.id, 'trash')}
|
|
127
|
+
{:else}
|
|
128
|
+
<dfn title="You must have at least one passkey" class="disabled">
|
|
129
|
+
<Icon i="trash-slash" --fill="#888" />
|
|
130
|
+
</dfn>
|
|
131
|
+
{/if}
|
|
132
|
+
</div>
|
|
133
|
+
<FormDialog
|
|
134
|
+
bind:dialog={dialogs['edit_passkey#' + passkey.id]}
|
|
135
|
+
submit={data => {
|
|
136
|
+
if (typeof data.name != 'string') throw 'Passkey name must be a string';
|
|
137
|
+
passkey.name = data.name;
|
|
138
|
+
return updatePasskey(passkey.id, data);
|
|
139
|
+
}}
|
|
140
|
+
submitText="Change"
|
|
141
|
+
>
|
|
142
|
+
<div>
|
|
143
|
+
<label for="name">Passkey Name</label>
|
|
144
|
+
<input name="name" type="text" value={passkey.name || ''} />
|
|
145
|
+
</div>
|
|
146
|
+
</FormDialog>
|
|
147
|
+
<FormDialog
|
|
148
|
+
bind:dialog={dialogs['delete_passkey#' + passkey.id]}
|
|
149
|
+
submit={() => deletePasskey(passkey.id).then(() => passkeys.splice(passkeys.indexOf(passkey), 1))}
|
|
150
|
+
submitText="Delete"
|
|
151
|
+
submitDanger={true}
|
|
152
|
+
>
|
|
153
|
+
<p>Are you sure you want to delete this passkey?<br />This action can't be undone.</p>
|
|
154
|
+
</FormDialog>
|
|
155
|
+
{/each}
|
|
156
|
+
<button onclick={() => createPasskey(user.id).then(passkeys.push.bind(passkeys))}><Icon i="plus" /> Create</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
{:catch error}
|
|
160
|
+
<div class="error">
|
|
161
|
+
<h3>Failed to load your account</h3>
|
|
162
|
+
<p>{'message' in error ? error.message : error}</p>
|
|
163
|
+
</div>
|
|
164
|
+
{/await}
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
.pfp {
|
|
168
|
+
width: 100px;
|
|
169
|
+
height: 100px;
|
|
170
|
+
border-radius: 50%;
|
|
171
|
+
border: 1px solid #8888;
|
|
172
|
+
margin-top: 3em;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.greeting {
|
|
176
|
+
font-size: 2em;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.signout {
|
|
180
|
+
margin-top: 2em;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.section {
|
|
184
|
+
width: 50%;
|
|
185
|
+
padding-top: 4em;
|
|
186
|
+
|
|
187
|
+
> div:has(+ div) {
|
|
188
|
+
border-bottom: 1px solid #8888;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.section .item {
|
|
193
|
+
display: grid;
|
|
194
|
+
grid-template-columns: 10em 1fr 2em;
|
|
195
|
+
align-items: center;
|
|
196
|
+
width: 100%;
|
|
197
|
+
gap: 1em;
|
|
198
|
+
text-wrap: nowrap;
|
|
199
|
+
padding-bottom: 1em;
|
|
200
|
+
|
|
201
|
+
> :first-child {
|
|
202
|
+
margin-left: 1em;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.passkey {
|
|
207
|
+
display: grid;
|
|
208
|
+
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
|
+
|
|
216
|
+
dfn {
|
|
217
|
+
cursor: help;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dfn.disabled {
|
|
221
|
+
cursor: not-allowed;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RequestMethod } from '@axium/core/requests';
|
|
2
|
+
import { resolveRoute } from '@axium/server/routes.js';
|
|
3
|
+
import { config } from '@axium/server/config.js';
|
|
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>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import FormDialog from '$lib/FormDialog.svelte';
|
|
3
|
+
import { register } from '@axium/client/user';
|
|
4
|
+
|
|
5
|
+
let { dialog = $bindable(), pageMode = true } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<svelte:head>
|
|
9
|
+
<title>Sign Up</title>
|
|
10
|
+
</svelte:head>
|
|
11
|
+
|
|
12
|
+
<FormDialog bind:dialog submitText="Register" submit={register} {pageMode}>
|
|
13
|
+
<div>
|
|
14
|
+
<label for="name">Display Name</label>
|
|
15
|
+
<input name="name" type="text" required />
|
|
16
|
+
</div>
|
|
17
|
+
<div>
|
|
18
|
+
<label for="email">Email</label>
|
|
19
|
+
<input name="email" type="email" required />
|
|
20
|
+
</div>
|
|
21
|
+
</FormDialog>
|
package/web/tsconfig.json
CHANGED
package/web/utils.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import type { Session } from '@auth/sveltekit';
|
|
2
1
|
import type { ActionFailure, RequestEvent } from '@sveltejs/kit';
|
|
3
|
-
import { fail
|
|
4
|
-
import
|
|
5
|
-
import { fromError } from 'zod-validation-error';
|
|
6
|
-
import config from '../dist/config.js';
|
|
7
|
-
|
|
8
|
-
export async function loadSession(event: RequestEvent): Promise<{ session: Session }> {
|
|
9
|
-
const session = await event.locals.auth();
|
|
10
|
-
if (!session) redirect(307, '/auth/signin');
|
|
11
|
-
if (!session.user.name && event.url.pathname != config.web.prefix + '/name') redirect(307, config.web.prefix + '/name');
|
|
12
|
-
return { session };
|
|
13
|
-
}
|
|
2
|
+
import { fail } from '@sveltejs/kit';
|
|
3
|
+
import z from 'zod/v4';
|
|
14
4
|
|
|
15
5
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
16
|
-
export interface FormFail<S extends z.
|
|
6
|
+
export interface FormFail<S extends z.ZodType, E extends object = object> extends ActionFailure<z.infer<S> & { error: string } & E> {}
|
|
17
7
|
|
|
18
|
-
export async function parseForm<S extends z.
|
|
8
|
+
export async function parseForm<S extends z.ZodObject, E extends object = object>(
|
|
9
|
+
event: RequestEvent,
|
|
10
|
+
schema: S,
|
|
11
|
+
errorData?: E
|
|
12
|
+
): Promise<[z.infer<S>, FormFail<S, E> | null]> {
|
|
19
13
|
const formData = Object.fromEntries(await event.request.formData());
|
|
20
14
|
const { data, error, success } = schema.safeParse(formData);
|
|
21
15
|
|
|
@@ -26,7 +20,7 @@ export async function parseForm<S extends z.AnyZodObject, E extends object = obj
|
|
|
26
20
|
fail(400, {
|
|
27
21
|
...data,
|
|
28
22
|
...errorData,
|
|
29
|
-
error:
|
|
23
|
+
error: z.prettifyError(error),
|
|
30
24
|
}),
|
|
31
25
|
];
|
|
32
26
|
}
|