@axium/server 0.9.0 → 0.10.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/{web/api/index.ts → dist/api/index.d.ts} +0 -2
- package/dist/api/index.js +5 -0
- package/dist/api/metadata.d.ts +1 -0
- package/dist/api/metadata.js +28 -0
- package/dist/api/passkeys.d.ts +1 -0
- package/dist/api/passkeys.js +50 -0
- package/dist/api/register.d.ts +1 -0
- package/dist/api/register.js +70 -0
- package/dist/api/session.d.ts +1 -0
- package/dist/api/session.js +31 -0
- package/dist/api/users.d.ts +1 -0
- package/dist/api/users.js +244 -0
- package/dist/apps.d.ts +0 -5
- package/dist/apps.js +2 -9
- package/dist/auth.d.ts +9 -21
- package/dist/auth.js +12 -18
- package/dist/cli.js +65 -26
- package/dist/config.d.ts +15 -8
- package/dist/config.js +38 -15
- package/dist/database.js +4 -0
- package/dist/io.d.ts +6 -4
- package/dist/io.js +26 -19
- package/dist/plugins.d.ts +4 -2
- package/dist/plugins.js +15 -14
- package/dist/requests.d.ts +11 -0
- package/dist/requests.js +58 -0
- package/dist/routes.d.ts +12 -13
- package/dist/routes.js +21 -22
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +11 -0
- package/dist/state.d.ts +4 -0
- package/dist/state.js +22 -0
- package/dist/sveltekit.d.ts +8 -0
- package/dist/sveltekit.js +90 -0
- package/package.json +10 -5
- package/svelte.config.js +36 -0
- package/web/hooks.server.ts +8 -3
- package/web/lib/Dialog.svelte +0 -1
- package/web/lib/FormDialog.svelte +0 -1
- package/web/lib/icons/Icon.svelte +2 -7
- package/web/template.html +18 -0
- package/web/tsconfig.json +3 -2
- package/web/api/metadata.ts +0 -35
- package/web/api/passkeys.ts +0 -56
- package/web/api/readme.md +0 -1
- package/web/api/register.ts +0 -83
- package/web/api/schemas.ts +0 -22
- package/web/api/session.ts +0 -33
- package/web/api/users.ts +0 -351
- package/web/api/utils.ts +0 -66
- package/web/app.html +0 -14
- package/web/auth.ts +0 -8
- package/web/index.server.ts +0 -1
- package/web/index.ts +0 -1
- package/web/lib/auth.ts +0 -12
- package/web/lib/index.ts +0 -5
- package/web/routes/+layout.svelte +0 -6
- package/web/routes/[...path]/+page.server.ts +0 -13
- package/web/routes/[appId]/[...page]/+page.server.ts +0 -14
- package/web/routes/api/[...path]/+server.ts +0 -49
- package/web/utils.ts +0 -26
- /package/{web/lib → assets}/icons/light.svg +0 -0
- /package/{web/lib → assets}/icons/regular.svg +0 -0
- /package/{web/lib → assets}/icons/solid.svg +0 -0
- /package/{web/lib → assets}/styles.css +0 -0
- /package/{web/routes → routes}/_axium/default/+page.svelte +0 -0
- /package/{web/routes → routes}/account/+page.svelte +0 -0
- /package/{web/routes → routes}/login/+page.svelte +0 -0
- /package/{web/routes → routes}/logout/+page.svelte +0 -0
- /package/{web/routes → routes}/register/+page.svelte +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { requestMethods } from '@axium/core/requests';
|
|
2
|
+
import { error } from '@sveltejs/kit';
|
|
3
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { plugins } from '../plugins.js';
|
|
6
|
+
import { addRoute, routes } from '../routes.js';
|
|
7
|
+
addRoute({
|
|
8
|
+
path: '/api/metadata',
|
|
9
|
+
async GET() {
|
|
10
|
+
if (config.api.disable_metadata) {
|
|
11
|
+
error(401, { message: 'API metadata is disabled' });
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
version: pkg.version,
|
|
15
|
+
routes: Object.fromEntries(routes
|
|
16
|
+
.entries()
|
|
17
|
+
.filter(([path]) => path.startsWith('/api/'))
|
|
18
|
+
.map(([path, route]) => [
|
|
19
|
+
path,
|
|
20
|
+
{
|
|
21
|
+
params: Object.fromEntries(Object.entries(route.params || {}).map(([key, type]) => [key, type ? type.def.type : null])),
|
|
22
|
+
methods: requestMethods.filter(m => m in route),
|
|
23
|
+
},
|
|
24
|
+
])),
|
|
25
|
+
plugins: Object.fromEntries(plugins.values().map(plugin => [plugin.name, plugin.version])),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PasskeyChangeable } from '@axium/core/schemas';
|
|
2
|
+
import { error } from '@sveltejs/kit';
|
|
3
|
+
import { omit } from 'utilium';
|
|
4
|
+
import z from 'zod/v4';
|
|
5
|
+
import { getPasskey } from '../auth.js';
|
|
6
|
+
import { database as db } from '../database.js';
|
|
7
|
+
import { addRoute } from '../routes.js';
|
|
8
|
+
import { checkAuth, parseBody, withError } from '../requests.js';
|
|
9
|
+
addRoute({
|
|
10
|
+
path: '/api/passkeys/:id',
|
|
11
|
+
params: {
|
|
12
|
+
id: z.string(),
|
|
13
|
+
},
|
|
14
|
+
async GET(event) {
|
|
15
|
+
const passkey = await getPasskey(event.params.id);
|
|
16
|
+
await checkAuth(event, passkey.userId);
|
|
17
|
+
return omit(passkey, 'counter', 'publicKey');
|
|
18
|
+
},
|
|
19
|
+
async PATCH(event) {
|
|
20
|
+
const body = await parseBody(event, PasskeyChangeable);
|
|
21
|
+
const passkey = await getPasskey(event.params.id);
|
|
22
|
+
await checkAuth(event, passkey.userId);
|
|
23
|
+
const result = await db
|
|
24
|
+
.updateTable('passkeys')
|
|
25
|
+
.set(body)
|
|
26
|
+
.where('id', '=', passkey.id)
|
|
27
|
+
.returningAll()
|
|
28
|
+
.executeTakeFirstOrThrow()
|
|
29
|
+
.catch(withError('Could not update passkey'));
|
|
30
|
+
return omit(result, 'counter', 'publicKey');
|
|
31
|
+
},
|
|
32
|
+
async DELETE(event) {
|
|
33
|
+
const passkey = await getPasskey(event.params.id);
|
|
34
|
+
await checkAuth(event, passkey.userId);
|
|
35
|
+
const { count } = await db
|
|
36
|
+
.selectFrom('passkeys')
|
|
37
|
+
.select(db.fn.countAll().as('count'))
|
|
38
|
+
.where('userId', '=', passkey.userId)
|
|
39
|
+
.executeTakeFirstOrThrow();
|
|
40
|
+
if (Number(count) <= 1)
|
|
41
|
+
error(409, 'At least one passkey is required');
|
|
42
|
+
const result = await db
|
|
43
|
+
.deleteFrom('passkeys')
|
|
44
|
+
.where('id', '=', passkey.id)
|
|
45
|
+
.returningAll()
|
|
46
|
+
.executeTakeFirstOrThrow()
|
|
47
|
+
.catch(withError('Could not delete passkey'));
|
|
48
|
+
return omit(result, 'counter', 'publicKey');
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { APIUserRegistration } from '@axium/core/schemas';
|
|
2
|
+
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
3
|
+
import { error } from '@sveltejs/kit';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import z from 'zod/v4';
|
|
6
|
+
import { createPasskey, getUser } from '../auth.js';
|
|
7
|
+
import config from '../config.js';
|
|
8
|
+
import { database as db } from '../database.js';
|
|
9
|
+
import { addRoute } from '../routes.js';
|
|
10
|
+
import { createSessionData, parseBody, withError } from '../requests.js';
|
|
11
|
+
// Map of user ID => challenge
|
|
12
|
+
const registrations = new Map();
|
|
13
|
+
async function OPTIONS(event) {
|
|
14
|
+
const { name, email } = await parseBody(event, z.object({ name: z.string().optional(), email: z.email().optional() }));
|
|
15
|
+
const userId = randomUUID();
|
|
16
|
+
const user = await getUser(userId).catch(() => null);
|
|
17
|
+
if (user)
|
|
18
|
+
error(409, { message: 'Generated UUID is already in use, please retry.' });
|
|
19
|
+
const options = await generateRegistrationOptions({
|
|
20
|
+
rpName: config.auth.rp_name,
|
|
21
|
+
rpID: config.auth.rp_id,
|
|
22
|
+
userName: email ?? userId,
|
|
23
|
+
userDisplayName: name,
|
|
24
|
+
attestationType: 'none',
|
|
25
|
+
excludeCredentials: [],
|
|
26
|
+
authenticatorSelection: {
|
|
27
|
+
residentKey: 'preferred',
|
|
28
|
+
userVerification: 'preferred',
|
|
29
|
+
authenticatorAttachment: 'platform',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
registrations.set(userId, options.challenge);
|
|
33
|
+
return { userId, options };
|
|
34
|
+
}
|
|
35
|
+
async function POST(event) {
|
|
36
|
+
const { userId, email, name, response } = await parseBody(event, APIUserRegistration);
|
|
37
|
+
const existing = await db.selectFrom('users').selectAll().where('email', '=', email).executeTakeFirst();
|
|
38
|
+
if (existing)
|
|
39
|
+
error(409, { message: 'Email already in use' });
|
|
40
|
+
const expectedChallenge = registrations.get(userId);
|
|
41
|
+
if (!expectedChallenge)
|
|
42
|
+
error(404, { message: 'No registration challenge found for this user' });
|
|
43
|
+
registrations.delete(userId);
|
|
44
|
+
const { verified, registrationInfo } = await verifyRegistrationResponse({
|
|
45
|
+
response,
|
|
46
|
+
expectedChallenge,
|
|
47
|
+
expectedOrigin: config.auth.origin,
|
|
48
|
+
}).catch(() => error(400, { message: 'Verification failed' }));
|
|
49
|
+
if (!verified || !registrationInfo)
|
|
50
|
+
error(401, { message: 'Verification failed' });
|
|
51
|
+
await db
|
|
52
|
+
.insertInto('users')
|
|
53
|
+
.values({ id: userId, name, email })
|
|
54
|
+
.executeTakeFirstOrThrow()
|
|
55
|
+
.catch(withError('Failed to create user'));
|
|
56
|
+
await createPasskey({
|
|
57
|
+
transports: [],
|
|
58
|
+
...registrationInfo.credential,
|
|
59
|
+
userId,
|
|
60
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
61
|
+
backedUp: registrationInfo.credentialBackedUp,
|
|
62
|
+
}).catch(e => error(500, { message: 'Failed to create passkey' + (config.debug ? `: ${e.message}` : '') }));
|
|
63
|
+
return await createSessionData(event, userId);
|
|
64
|
+
}
|
|
65
|
+
addRoute({
|
|
66
|
+
path: '/api/register',
|
|
67
|
+
params: {},
|
|
68
|
+
OPTIONS,
|
|
69
|
+
POST,
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
|
+
import { omit } from 'utilium';
|
|
3
|
+
import { authenticate } from '../auth.js';
|
|
4
|
+
import { connect, database as db } from '../database.js';
|
|
5
|
+
import { addRoute } from '../routes.js';
|
|
6
|
+
import { getToken, stripUser } from '../requests.js';
|
|
7
|
+
addRoute({
|
|
8
|
+
path: '/api/session',
|
|
9
|
+
async GET(event) {
|
|
10
|
+
const result = await authenticate(event);
|
|
11
|
+
if (!result)
|
|
12
|
+
error(404, 'Session does not exist');
|
|
13
|
+
return {
|
|
14
|
+
...omit(result, 'token'),
|
|
15
|
+
user: stripUser(result.user, true),
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
async DELETE(event) {
|
|
19
|
+
const token = getToken(event);
|
|
20
|
+
if (!token)
|
|
21
|
+
error(401, 'Missing token');
|
|
22
|
+
connect();
|
|
23
|
+
const result = await db
|
|
24
|
+
.deleteFrom('sessions')
|
|
25
|
+
.where('sessions.token', '=', token)
|
|
26
|
+
.returningAll()
|
|
27
|
+
.executeTakeFirstOrThrow()
|
|
28
|
+
.catch((e) => (e.message == 'no result' ? error(404, 'Session does not exist') : error(400, 'Invalid session')));
|
|
29
|
+
return omit(result, 'token');
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { LogoutSessions, PasskeyAuthenticationResponse, PasskeyRegistration, UserAuthOptions } from '@axium/core/schemas';
|
|
2
|
+
import { UserChangeable } from '@axium/core/user';
|
|
3
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
|
|
4
|
+
import { error } from '@sveltejs/kit';
|
|
5
|
+
import { omit, pick } from 'utilium';
|
|
6
|
+
import z from 'zod/v4';
|
|
7
|
+
import { createPasskey, createVerification, getPasskey, getPasskeysByUserId, getSessions, getUser, useVerification, } from '../auth.js';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { connect, database as db } from '../database.js';
|
|
10
|
+
import { addRoute } from '../routes.js';
|
|
11
|
+
import { checkAuth, createSessionData, parseBody, stripUser, withError } from '../requests.js';
|
|
12
|
+
const challenges = new Map();
|
|
13
|
+
const params = { id: z.uuid() };
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a user's UUID using their email (in the future this might also include handles)
|
|
16
|
+
*/
|
|
17
|
+
addRoute({
|
|
18
|
+
path: '/api/user_id',
|
|
19
|
+
async POST(event) {
|
|
20
|
+
const { value } = await parseBody(event, z.object({ using: z.literal('email'), value: z.email() }));
|
|
21
|
+
connect();
|
|
22
|
+
const { id } = await db
|
|
23
|
+
.selectFrom('users')
|
|
24
|
+
.select('id')
|
|
25
|
+
.where('email', '=', value)
|
|
26
|
+
.executeTakeFirstOrThrow()
|
|
27
|
+
.catch(withError('User not found', 404));
|
|
28
|
+
return { id };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
addRoute({
|
|
32
|
+
path: '/api/users/:id',
|
|
33
|
+
params,
|
|
34
|
+
async GET(event) {
|
|
35
|
+
const userId = event.params.id;
|
|
36
|
+
const authed = await checkAuth(event, userId).catch(() => null);
|
|
37
|
+
const user = authed?.user || (await getUser(userId).catch(withError('User does not exist', 404)));
|
|
38
|
+
return stripUser(user, !!authed);
|
|
39
|
+
},
|
|
40
|
+
async PATCH(event) {
|
|
41
|
+
const userId = event.params.id;
|
|
42
|
+
const body = await parseBody(event, UserChangeable);
|
|
43
|
+
await checkAuth(event, userId);
|
|
44
|
+
if ('email' in body)
|
|
45
|
+
body.emailVerified = null;
|
|
46
|
+
const result = await db
|
|
47
|
+
.updateTable('users')
|
|
48
|
+
.set(body)
|
|
49
|
+
.where('id', '=', userId)
|
|
50
|
+
.returningAll()
|
|
51
|
+
.executeTakeFirstOrThrow()
|
|
52
|
+
.catch(withError('Failed to update user'));
|
|
53
|
+
return stripUser(result, true);
|
|
54
|
+
},
|
|
55
|
+
async DELETE(event) {
|
|
56
|
+
const userId = event.params.id;
|
|
57
|
+
await checkAuth(event, userId, true);
|
|
58
|
+
const result = await db
|
|
59
|
+
.deleteFrom('users')
|
|
60
|
+
.where('id', '=', userId)
|
|
61
|
+
.returningAll()
|
|
62
|
+
.executeTakeFirstOrThrow()
|
|
63
|
+
.catch(withError('Failed to delete user'));
|
|
64
|
+
return result;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
addRoute({
|
|
68
|
+
path: '/api/users/:id/full',
|
|
69
|
+
params,
|
|
70
|
+
async GET(event) {
|
|
71
|
+
const userId = event.params.id;
|
|
72
|
+
const { user } = await checkAuth(event, userId);
|
|
73
|
+
const sessions = await getSessions(userId);
|
|
74
|
+
return {
|
|
75
|
+
...stripUser(user, true),
|
|
76
|
+
sessions: sessions.map(s => omit(s, 'token')),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
addRoute({
|
|
81
|
+
path: '/api/users/:id/auth',
|
|
82
|
+
params,
|
|
83
|
+
async OPTIONS(event) {
|
|
84
|
+
const userId = event.params.id;
|
|
85
|
+
const { type } = await parseBody(event, UserAuthOptions);
|
|
86
|
+
await getUser(userId).catch(withError('User does not exist', 404));
|
|
87
|
+
const passkeys = await getPasskeysByUserId(userId);
|
|
88
|
+
if (!passkeys)
|
|
89
|
+
error(409, { message: 'No passkeys exists for this user' });
|
|
90
|
+
const options = await generateAuthenticationOptions({
|
|
91
|
+
rpID: config.auth.rp_id,
|
|
92
|
+
allowCredentials: passkeys.map(passkey => pick(passkey, 'id', 'transports')),
|
|
93
|
+
});
|
|
94
|
+
challenges.set(userId, { data: options.challenge, type });
|
|
95
|
+
return options;
|
|
96
|
+
},
|
|
97
|
+
async POST(event) {
|
|
98
|
+
const userId = event.params.id;
|
|
99
|
+
const response = await parseBody(event, PasskeyAuthenticationResponse);
|
|
100
|
+
const auth = challenges.get(userId);
|
|
101
|
+
if (!auth)
|
|
102
|
+
error(404, { message: 'No challenge' });
|
|
103
|
+
const { data: expectedChallenge, type } = auth;
|
|
104
|
+
challenges.delete(userId);
|
|
105
|
+
const passkey = await getPasskey(response.id).catch(withError('Passkey does not exist', 404));
|
|
106
|
+
if (passkey.userId !== userId)
|
|
107
|
+
error(403, { message: 'Passkey does not belong to this user' });
|
|
108
|
+
const { verified } = await verifyAuthenticationResponse({
|
|
109
|
+
response,
|
|
110
|
+
credential: passkey,
|
|
111
|
+
expectedChallenge,
|
|
112
|
+
expectedOrigin: config.auth.origin,
|
|
113
|
+
expectedRPID: config.auth.rp_id,
|
|
114
|
+
}).catch(withError('Verification failed', 400));
|
|
115
|
+
if (!verified)
|
|
116
|
+
error(401, { message: 'Verification failed' });
|
|
117
|
+
switch (type) {
|
|
118
|
+
case 'login':
|
|
119
|
+
return await createSessionData(event, userId);
|
|
120
|
+
case 'action':
|
|
121
|
+
if ((Date.now() - passkey.createdAt.getTime()) / 60_000 < config.auth.passkey_probation)
|
|
122
|
+
error(403, { message: 'You can not authorize sensitive actions with a newly created passkey' });
|
|
123
|
+
return await createSessionData(event, userId, true);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
// Map of user ID => challenge
|
|
128
|
+
const registrations = new Map();
|
|
129
|
+
addRoute({
|
|
130
|
+
path: '/api/users/:id/passkeys',
|
|
131
|
+
params,
|
|
132
|
+
/**
|
|
133
|
+
* Get passkey registration options for a user.
|
|
134
|
+
*/
|
|
135
|
+
async OPTIONS(event) {
|
|
136
|
+
const userId = event.params.id;
|
|
137
|
+
const existing = await getPasskeysByUserId(userId);
|
|
138
|
+
const { user } = await checkAuth(event, userId);
|
|
139
|
+
const options = await generateRegistrationOptions({
|
|
140
|
+
rpName: config.auth.rp_name,
|
|
141
|
+
rpID: config.auth.rp_id,
|
|
142
|
+
userName: userId,
|
|
143
|
+
userDisplayName: user.email,
|
|
144
|
+
attestationType: 'none',
|
|
145
|
+
excludeCredentials: existing.map(passkey => pick(passkey, 'id', 'transports')),
|
|
146
|
+
authenticatorSelection: {
|
|
147
|
+
residentKey: 'preferred',
|
|
148
|
+
userVerification: 'preferred',
|
|
149
|
+
authenticatorAttachment: 'platform',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
registrations.set(userId, options.challenge);
|
|
153
|
+
return options;
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* Get passkeys for a user.
|
|
157
|
+
*/
|
|
158
|
+
async GET(event) {
|
|
159
|
+
const userId = event.params.id;
|
|
160
|
+
await checkAuth(event, userId);
|
|
161
|
+
const passkeys = await getPasskeysByUserId(userId);
|
|
162
|
+
return passkeys.map(p => omit(p, 'publicKey', 'counter'));
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Register a new passkey for an existing user.
|
|
166
|
+
*/
|
|
167
|
+
async PUT(event) {
|
|
168
|
+
const userId = event.params.id;
|
|
169
|
+
const response = await parseBody(event, PasskeyRegistration);
|
|
170
|
+
await checkAuth(event, userId);
|
|
171
|
+
const expectedChallenge = registrations.get(userId);
|
|
172
|
+
if (!expectedChallenge)
|
|
173
|
+
error(404, { message: 'No registration challenge found for this user' });
|
|
174
|
+
registrations.delete(userId);
|
|
175
|
+
const { verified, registrationInfo } = await verifyRegistrationResponse({
|
|
176
|
+
response,
|
|
177
|
+
expectedChallenge,
|
|
178
|
+
expectedOrigin: config.auth.origin,
|
|
179
|
+
}).catch(withError('Verification failed', 400));
|
|
180
|
+
if (!verified || !registrationInfo)
|
|
181
|
+
error(401, { message: 'Verification failed' });
|
|
182
|
+
const passkey = await createPasskey({
|
|
183
|
+
transports: [],
|
|
184
|
+
...registrationInfo.credential,
|
|
185
|
+
userId,
|
|
186
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
187
|
+
backedUp: registrationInfo.credentialBackedUp,
|
|
188
|
+
}).catch(withError('Failed to create passkey'));
|
|
189
|
+
return omit(passkey, 'publicKey', 'counter');
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
addRoute({
|
|
193
|
+
path: '/api/users/:id/sessions',
|
|
194
|
+
params,
|
|
195
|
+
async GET(event) {
|
|
196
|
+
const userId = event.params.id;
|
|
197
|
+
await checkAuth(event, userId);
|
|
198
|
+
return (await getSessions(userId).catch(e => error(503, 'Failed to get sessions' + (config.debug ? ': ' + e : '')))).map(s => omit(s, 'token'));
|
|
199
|
+
},
|
|
200
|
+
async DELETE(event) {
|
|
201
|
+
const userId = event.params.id;
|
|
202
|
+
const body = await parseBody(event, LogoutSessions);
|
|
203
|
+
await checkAuth(event, userId, body.confirm_all);
|
|
204
|
+
if (!body.confirm_all && !Array.isArray(body.id))
|
|
205
|
+
error(400, { message: 'Invalid request body' });
|
|
206
|
+
const query = body.confirm_all ? db.deleteFrom('sessions') : db.deleteFrom('sessions').where('sessions.id', 'in', body.id);
|
|
207
|
+
const result = await query
|
|
208
|
+
.where('sessions.userId', '=', userId)
|
|
209
|
+
.returningAll()
|
|
210
|
+
.execute()
|
|
211
|
+
.catch(withError('Failed to delete one or more sessions'));
|
|
212
|
+
return result.map(s => omit(s, 'token'));
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
addRoute({
|
|
216
|
+
path: '/api/users/:id/verify_email',
|
|
217
|
+
params,
|
|
218
|
+
async OPTIONS(event) {
|
|
219
|
+
const userId = event.params.id;
|
|
220
|
+
if (!config.auth.email_verification)
|
|
221
|
+
return { enabled: false };
|
|
222
|
+
await checkAuth(event, userId);
|
|
223
|
+
if (!config.auth.email_verification)
|
|
224
|
+
return { enabled: false };
|
|
225
|
+
return { enabled: true };
|
|
226
|
+
},
|
|
227
|
+
async GET(event) {
|
|
228
|
+
const userId = event.params.id;
|
|
229
|
+
const { user } = await checkAuth(event, userId);
|
|
230
|
+
if (user.emailVerified)
|
|
231
|
+
error(409, { message: 'Email already verified' });
|
|
232
|
+
const verification = await createVerification('verify_email', userId, config.auth.verification_timeout * 60);
|
|
233
|
+
return omit(verification, 'token', 'role');
|
|
234
|
+
},
|
|
235
|
+
async POST(event) {
|
|
236
|
+
const userId = event.params.id;
|
|
237
|
+
const { token } = await parseBody(event, z.object({ token: z.string() }));
|
|
238
|
+
const { user } = await checkAuth(event, userId);
|
|
239
|
+
if (user.emailVerified)
|
|
240
|
+
error(409, { message: 'Email already verified' });
|
|
241
|
+
await useVerification('verify_email', userId, token).catch(withError('Invalid or expired verification token', 400));
|
|
242
|
+
return {};
|
|
243
|
+
},
|
|
244
|
+
});
|
package/dist/apps.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { LoadEvent, RequestEvent } from '@sveltejs/kit';
|
|
2
|
-
import type { WebRoute, WebRouteOptions } from './routes.js';
|
|
3
1
|
export interface CreateAppOptions {
|
|
4
2
|
id: string;
|
|
5
3
|
name?: string;
|
|
@@ -8,8 +6,5 @@ export declare const apps: Map<string, App>;
|
|
|
8
6
|
export declare class App {
|
|
9
7
|
readonly id: string;
|
|
10
8
|
name: string;
|
|
11
|
-
protected readonly routes: Map<string, WebRoute>;
|
|
12
9
|
constructor(opt: CreateAppOptions);
|
|
13
|
-
addRoute(route: WebRouteOptions): void;
|
|
14
|
-
resolveRoute(event: RequestEvent | LoadEvent): WebRoute | undefined;
|
|
15
10
|
}
|
package/dist/apps.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { pick } from 'utilium';
|
|
2
|
-
import {
|
|
3
|
-
export const apps = new Map();
|
|
2
|
+
import { _unique } from './state.js';
|
|
3
|
+
export const apps = _unique('apps', new Map());
|
|
4
4
|
export class App {
|
|
5
5
|
id;
|
|
6
6
|
name;
|
|
7
|
-
routes = new Map();
|
|
8
7
|
constructor(opt) {
|
|
9
8
|
if (apps.has(opt.id))
|
|
10
9
|
throw new ReferenceError(`App with ID "${opt.id}" already exists.`);
|
|
11
10
|
Object.assign(this, pick(opt, 'id', 'name'));
|
|
12
11
|
apps.set(this.id, this);
|
|
13
12
|
}
|
|
14
|
-
addRoute(route) {
|
|
15
|
-
addRoute(route, this.routes);
|
|
16
|
-
}
|
|
17
|
-
resolveRoute(event) {
|
|
18
|
-
return resolveRoute(event, this.routes);
|
|
19
|
-
}
|
|
20
13
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,40 +1,27 @@
|
|
|
1
1
|
import type { Passkey, Session, Verification } from '@axium/core/api';
|
|
2
2
|
import type { User } from '@axium/core/user';
|
|
3
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
3
4
|
export interface UserInternal extends User {
|
|
4
5
|
password?: string | null;
|
|
5
6
|
salt?: string | null;
|
|
6
7
|
}
|
|
7
|
-
export declare function getUser(id: string): Promise<
|
|
8
|
-
|
|
8
|
+
export declare function getUser(id: string): Promise<UserInternal>;
|
|
9
|
+
export declare function updateUser({ id, ...user }: UserInternal): Promise<{
|
|
9
10
|
id: string;
|
|
10
|
-
image: string | null | undefined;
|
|
11
|
-
email: string;
|
|
12
|
-
emailVerified: Date | null | undefined;
|
|
13
|
-
preferences: import("@axium/core/user").Preferences | undefined;
|
|
14
|
-
} | null>;
|
|
15
|
-
export declare function getUserByEmail(email: string): Promise<{
|
|
16
11
|
name: string;
|
|
17
|
-
id: string;
|
|
18
|
-
image: string | null | undefined;
|
|
19
12
|
email: string;
|
|
20
13
|
emailVerified: Date | null | undefined;
|
|
21
|
-
preferences: import("@axium/core/user").Preferences | undefined;
|
|
22
|
-
} | null>;
|
|
23
|
-
export declare function updateUser({ id, ...user }: UserInternal): Promise<{
|
|
24
|
-
name: string;
|
|
25
|
-
id: string;
|
|
26
14
|
image: string | null | undefined;
|
|
27
|
-
email: string;
|
|
28
|
-
emailVerified: Date | null | undefined;
|
|
29
15
|
preferences: import("@axium/core/user").Preferences | undefined;
|
|
30
16
|
}>;
|
|
31
17
|
export interface SessionInternal extends Session {
|
|
32
18
|
token: string;
|
|
33
19
|
}
|
|
34
20
|
export declare function createSession(userId: string, elevated?: boolean): Promise<SessionInternal>;
|
|
35
|
-
export
|
|
36
|
-
user: UserInternal
|
|
37
|
-
}
|
|
21
|
+
export interface SessionAndUser extends SessionInternal {
|
|
22
|
+
user: UserInternal;
|
|
23
|
+
}
|
|
24
|
+
export declare function getSessionAndUser(token: string): Promise<SessionAndUser>;
|
|
38
25
|
export declare function getSession(sessionId: string): Promise<SessionInternal>;
|
|
39
26
|
export declare function getSessions(userId: string): Promise<SessionInternal[]>;
|
|
40
27
|
export type VerificationRole = 'verify_email' | 'login';
|
|
@@ -52,7 +39,8 @@ export interface PasskeyInternal extends Passkey {
|
|
|
52
39
|
publicKey: Uint8Array;
|
|
53
40
|
counter: number;
|
|
54
41
|
}
|
|
55
|
-
export declare function getPasskey(id: string): Promise<PasskeyInternal
|
|
42
|
+
export declare function getPasskey(id: string): Promise<PasskeyInternal>;
|
|
56
43
|
export declare function createPasskey(passkey: Omit<PasskeyInternal, 'createdAt'>): Promise<PasskeyInternal>;
|
|
57
44
|
export declare function getPasskeysByUserId(userId: string): Promise<PasskeyInternal[]>;
|
|
58
45
|
export declare function updatePasskeyCounter(id: PasskeyInternal['id'], newCounter: PasskeyInternal['counter']): Promise<PasskeyInternal>;
|
|
46
|
+
export declare function authenticate(event: RequestEvent): Promise<SessionAndUser | null>;
|
package/dist/auth.js
CHANGED
|
@@ -3,17 +3,7 @@ import { randomBytes, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { connect, database as db } from './database.js';
|
|
4
4
|
export async function getUser(id) {
|
|
5
5
|
connect();
|
|
6
|
-
|
|
7
|
-
if (!result)
|
|
8
|
-
return null;
|
|
9
|
-
return result;
|
|
10
|
-
}
|
|
11
|
-
export async function getUserByEmail(email) {
|
|
12
|
-
connect();
|
|
13
|
-
const result = await db.selectFrom('users').selectAll().where('email', '=', email).executeTakeFirst();
|
|
14
|
-
if (!result)
|
|
15
|
-
return null;
|
|
16
|
-
return result;
|
|
6
|
+
return await db.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
|
|
17
7
|
}
|
|
18
8
|
export async function updateUser({ id, ...user }) {
|
|
19
9
|
connect();
|
|
@@ -43,9 +33,9 @@ export async function getSessionAndUser(token) {
|
|
|
43
33
|
.select(eb => jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'sessions.userId')).as('user'))
|
|
44
34
|
.where('sessions.token', '=', token)
|
|
45
35
|
.where('sessions.expires', '>', new Date())
|
|
46
|
-
.
|
|
47
|
-
if (!result)
|
|
48
|
-
throw new Error('Session
|
|
36
|
+
.executeTakeFirstOrThrow();
|
|
37
|
+
if (!result.user)
|
|
38
|
+
throw new Error('Session references non-existing user');
|
|
49
39
|
return result;
|
|
50
40
|
}
|
|
51
41
|
export async function getSession(sessionId) {
|
|
@@ -86,10 +76,7 @@ export async function useVerification(role, userId, token) {
|
|
|
86
76
|
}
|
|
87
77
|
export async function getPasskey(id) {
|
|
88
78
|
connect();
|
|
89
|
-
|
|
90
|
-
if (!result)
|
|
91
|
-
return null;
|
|
92
|
-
return result;
|
|
79
|
+
return await db.selectFrom('passkeys').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
|
|
93
80
|
}
|
|
94
81
|
export async function createPasskey(passkey) {
|
|
95
82
|
connect();
|
|
@@ -108,3 +95,10 @@ export async function updatePasskeyCounter(id, newCounter) {
|
|
|
108
95
|
throw new Error('Passkey not found');
|
|
109
96
|
return passkey;
|
|
110
97
|
}
|
|
98
|
+
export async function authenticate(event) {
|
|
99
|
+
const maybe_header = event.request.headers.get('Authorization');
|
|
100
|
+
const token = maybe_header?.startsWith('Bearer ') ? maybe_header.slice(7) : event.cookies.get('session_token');
|
|
101
|
+
if (!token)
|
|
102
|
+
return null;
|
|
103
|
+
return await getSessionAndUser(token).catch(() => null);
|
|
104
|
+
}
|