@axium/server 0.26.3 → 0.28.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/acl.d.ts +26 -45
- package/dist/acl.js +50 -52
- package/dist/api/acl.js +16 -8
- package/dist/api/admin.js +9 -12
- package/dist/api/metadata.js +4 -11
- package/dist/api/passkeys.js +6 -6
- package/dist/api/register.js +1 -1
- package/dist/api/users.js +16 -30
- package/dist/audit.d.ts +3 -3
- package/dist/audit.js +8 -9
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +29 -23
- package/dist/cli.d.ts +8 -2
- package/dist/cli.js +18 -605
- package/dist/config.d.ts +2 -2
- package/dist/config.js +8 -7
- package/dist/database.d.ts +417 -29
- package/dist/database.js +546 -247
- package/dist/db.json +71 -0
- package/dist/internal_requests.js +1 -1
- package/dist/main.d.ts +2 -0
- package/dist/main.js +833 -0
- package/dist/requests.d.ts +1 -1
- package/dist/requests.js +8 -1
- package/dist/routes.d.ts +20 -20
- package/dist/routes.js +2 -1
- package/dist/serve.js +1 -1
- package/package.json +6 -4
- package/routes/account/+page.svelte +11 -13
- package/routes/admin/audit/[id]/+page.svelte +6 -1
- package/routes/admin/plugins/+page.svelte +5 -1
- package/schemas/config.json +207 -0
- package/schemas/db.json +636 -0
- package/svelte.config.js +3 -0
package/dist/acl.d.ts
CHANGED
|
@@ -1,60 +1,41 @@
|
|
|
1
|
-
import type { AccessControl,
|
|
2
|
-
import type
|
|
1
|
+
import type { AccessControl, AccessMap, UserInternal } from '@axium/core';
|
|
2
|
+
import type * as kysely from 'kysely';
|
|
3
|
+
import type { WithRequired } from 'utilium';
|
|
3
4
|
import * as db from './database.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[K in Exclude<keyof db.Schema, `acl.${string}`> as Selectable<db.Schema[K]> extends Omit<Target, 'acl'> ? K : never]: null;
|
|
5
|
+
type _TableNames = keyof {
|
|
6
|
+
[K in keyof db.Schema as db.Schema[K] extends db.DBAccessControl ? K : never]: null;
|
|
7
|
+
};
|
|
8
|
+
type _TargetNames = keyof db.Schema & keyof {
|
|
9
|
+
[K in _TableNames as K extends `acl.${infer TB extends keyof db.Schema}` ? TB : never]: null;
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* `never` causes a ton of problems, so we use `string` if none of the tables are shareable.
|
|
13
13
|
*/
|
|
14
|
-
export type
|
|
14
|
+
export type TableName = _TableNames extends never ? keyof db.Schema : _TableNames;
|
|
15
|
+
export type TargetName = _TargetNames extends never ? keyof db.Schema : _TargetNames;
|
|
15
16
|
export interface AccessControlInternal extends AccessControl {
|
|
16
|
-
|
|
17
|
+
tag?: string | null;
|
|
17
18
|
}
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type: string;
|
|
25
|
-
required: true;
|
|
26
|
-
hasDefault: true;
|
|
27
|
-
};
|
|
28
|
-
itemId: {
|
|
29
|
-
type: string;
|
|
30
|
-
required: true;
|
|
31
|
-
};
|
|
32
|
-
permission: {
|
|
33
|
-
type: string;
|
|
34
|
-
required: true;
|
|
35
|
-
hasDefault: true;
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Adds an Access Control List (ACL) in the database for managing access to rows in an existing table.
|
|
40
|
-
* @category Plugin API
|
|
41
|
-
*/
|
|
42
|
-
export declare function createTable(table: TargetName): Promise<void>;
|
|
43
|
-
export declare function dropTable(table: TargetName): Promise<void>;
|
|
44
|
-
export declare function wipeTable(table: TargetName): Promise<void>;
|
|
45
|
-
export declare function createEntry(itemType: TargetName, data: Omit<AccessControl, 'createdAt'>): Promise<AccessControlInternal>;
|
|
46
|
-
export declare function deleteEntry(itemType: TargetName, itemId: string, userId: string): Promise<void>;
|
|
19
|
+
export type PermissionsFor<TB extends TableName> = Omit<kysely.Selectable<db.Schema[TB]>, keyof AccessControlInternal | number | symbol>;
|
|
20
|
+
export type Result<TB extends TableName> = AccessControlInternal & PermissionsFor<TB>;
|
|
21
|
+
export type WithACL<TB extends TargetName> = kysely.Selectable<db.Schema[TB]> & {
|
|
22
|
+
userId: string;
|
|
23
|
+
acl: Result<`acl.${TB}`>[];
|
|
24
|
+
} & Record<string, any>;
|
|
47
25
|
export interface ACLSelectionOptions {
|
|
48
|
-
/** If specified,
|
|
49
|
-
|
|
26
|
+
/** If specified, files by user UUID */
|
|
27
|
+
user?: Pick<UserInternal, 'id' | 'roles' | 'tags'>;
|
|
50
28
|
/** Instead of using the `id` from `table`, use the `id` from this instead */
|
|
51
29
|
alias?: string;
|
|
52
30
|
}
|
|
53
31
|
/**
|
|
54
32
|
* Helper to select all access controls for a given table, including the user information.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
33
|
+
* Optionally filter for the entries applicable to a specific user.
|
|
34
|
+
* This includes entries matching the user's ID, roles, or tags along with the "public" entry where all three "target" columns are null.
|
|
57
35
|
*/
|
|
58
|
-
export declare function from(table:
|
|
59
|
-
export declare function get(
|
|
36
|
+
export declare function from<const TB extends TargetName>(table: TB, opt?: ACLSelectionOptions): (eb: kysely.ExpressionBuilder<db.Schema, any>) => kysely.AliasedRawBuilder<Result<`acl.${TB}`>[], 'acl'>;
|
|
37
|
+
export declare function get<const TB extends TableName>(table: TB, itemId: string): Promise<WithRequired<AccessControlInternal & kysely.Selectable<db.Schema[TB]>, 'user'>[]>;
|
|
38
|
+
export declare function set<const TB extends TableName>(table: TB, itemId: string, data: AccessMap): Promise<(AccessControlInternal & kysely.Selectable<db.Schema[TB]>)[]>;
|
|
39
|
+
export declare function check<const TB extends TableName>(acl: Result<TB>[], permissions: Partial<PermissionsFor<TB>>): Set<keyof PermissionsFor<TB>>;
|
|
40
|
+
export declare function listTables(): Record<string, TableName>;
|
|
60
41
|
export {};
|
package/dist/acl.js
CHANGED
|
@@ -1,66 +1,64 @@
|
|
|
1
|
-
import * as io from '@axium/core/node/io';
|
|
2
|
-
import { sql } from 'kysely';
|
|
3
1
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
4
2
|
import * as db from './database.js';
|
|
5
|
-
const accessControllableTypes = {
|
|
6
|
-
userId: { type: 'uuid', required: true },
|
|
7
|
-
publicPermission: { type: 'int4', required: true, hasDefault: true },
|
|
8
|
-
};
|
|
9
|
-
export const expectedTypes = {
|
|
10
|
-
userId: { type: 'uuid', required: true },
|
|
11
|
-
createdAt: { type: 'timestamptz', required: true, hasDefault: true },
|
|
12
|
-
itemId: { type: 'uuid', required: true },
|
|
13
|
-
permission: { type: 'int4', required: true, hasDefault: true },
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Adds an Access Control List (ACL) in the database for managing access to rows in an existing table.
|
|
17
|
-
* @category Plugin API
|
|
18
|
-
*/
|
|
19
|
-
export async function createTable(table) {
|
|
20
|
-
await db.checkTableTypes(table, accessControllableTypes, { strict: true, extra: false });
|
|
21
|
-
io.start(`Creating table acl.${table}`);
|
|
22
|
-
await db.database.schema
|
|
23
|
-
.createTable(`acl.${table}`)
|
|
24
|
-
.addColumn('userId', 'uuid', col => col.references('users.id').onDelete('cascade'))
|
|
25
|
-
.addColumn('itemId', 'uuid', col => col.references(`${table}.id`).onDelete('cascade'))
|
|
26
|
-
.addPrimaryKeyConstraint('PK_acl_' + table, ['userId', 'itemId'])
|
|
27
|
-
.addColumn('createdAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
28
|
-
.addColumn('permission', 'integer', col => col.notNull().check(sql `permission >= 0 AND permission <= 5`))
|
|
29
|
-
.execute()
|
|
30
|
-
.then(io.done)
|
|
31
|
-
.catch(db.warnExists);
|
|
32
|
-
await db.createIndex(`acl.${table}`, 'userId');
|
|
33
|
-
await db.createIndex(`acl.${table}`, 'itemId');
|
|
34
|
-
}
|
|
35
|
-
export async function dropTable(table) {
|
|
36
|
-
io.start(`Dropping table acl.${table}`);
|
|
37
|
-
await db.database.schema.dropTable(`acl.${table}`).execute().then(io.done).catch(db.warnExists);
|
|
38
|
-
}
|
|
39
|
-
export async function wipeTable(table) {
|
|
40
|
-
io.start(`Wiping table acl.${table}`);
|
|
41
|
-
await db.database.deleteFrom(`acl.${table}`).execute().then(io.done).catch(db.warnExists);
|
|
42
|
-
}
|
|
43
|
-
export async function createEntry(itemType, data) {
|
|
44
|
-
return await db.database.insertInto(`acl.${itemType}`).values(data).returningAll().executeTakeFirstOrThrow();
|
|
45
|
-
}
|
|
46
|
-
export async function deleteEntry(itemType, itemId, userId) {
|
|
47
|
-
await db.database.deleteFrom(`acl.${itemType}`).where('itemId', '=', itemId).where('userId', '=', userId).execute();
|
|
48
|
-
}
|
|
49
3
|
/**
|
|
50
4
|
* Helper to select all access controls for a given table, including the user information.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
5
|
+
* Optionally filter for the entries applicable to a specific user.
|
|
6
|
+
* This includes entries matching the user's ID, roles, or tags along with the "public" entry where all three "target" columns are null.
|
|
53
7
|
*/
|
|
54
8
|
export function from(table, opt = {}) {
|
|
55
9
|
return (eb) => jsonArrayFrom(eb
|
|
56
10
|
.selectFrom(`acl.${table} as _acl`)
|
|
57
11
|
.selectAll()
|
|
58
|
-
.select(db.userFromId)
|
|
59
12
|
.whereRef(`_acl.itemId`, '=', `${opt.alias || table}.id`)
|
|
60
|
-
|
|
13
|
+
.where(eb => {
|
|
14
|
+
const allNull = eb.and([eb('userId', 'is', null), eb('role', 'is', null), eb('tag', 'is', null)]);
|
|
15
|
+
if (!opt.user)
|
|
16
|
+
return allNull;
|
|
17
|
+
const ors = [allNull, eb('userId', '=', opt.user.id)];
|
|
18
|
+
if (opt.user.roles.length)
|
|
19
|
+
ors.push(eb('role', 'in', opt.user.roles));
|
|
20
|
+
if (opt.user.tags.length)
|
|
21
|
+
ors.push(eb('tag', 'in', opt.user.tags));
|
|
22
|
+
return eb.or(ors);
|
|
23
|
+
}))
|
|
61
24
|
.$castTo()
|
|
62
25
|
.as('acl');
|
|
63
26
|
}
|
|
64
|
-
export async function get(
|
|
65
|
-
|
|
27
|
+
export async function get(table, itemId) {
|
|
28
|
+
// @ts-expect-error 2349
|
|
29
|
+
return await db.database.selectFrom(table).where('itemId', '=', itemId).selectAll().select(db.userFromId).execute();
|
|
30
|
+
}
|
|
31
|
+
export async function set(table, itemId, data) {
|
|
32
|
+
const entries = Object.entries(data).map(([userId, perm]) => ({ userId, ...perm }));
|
|
33
|
+
if (!entries.length)
|
|
34
|
+
return [];
|
|
35
|
+
return await db.database
|
|
36
|
+
.updateTable(table)
|
|
37
|
+
// @ts-expect-error 2349
|
|
38
|
+
.from(db.values(entries, 'data'))
|
|
39
|
+
.set()
|
|
40
|
+
.whereRef(`${table}.userId`, '=', 'data.userId')
|
|
41
|
+
.where('itemId', '=', itemId)
|
|
42
|
+
.returningAll()
|
|
43
|
+
.execute();
|
|
44
|
+
}
|
|
45
|
+
export function check(acl, permissions) {
|
|
46
|
+
const allowed = new Set();
|
|
47
|
+
const all = new Set(Object.keys(permissions));
|
|
48
|
+
const entries = Object.entries(permissions);
|
|
49
|
+
for (const control of acl) {
|
|
50
|
+
for (const [key, needed] of entries) {
|
|
51
|
+
const value = control[key];
|
|
52
|
+
if (value === needed)
|
|
53
|
+
allowed.add(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return all.difference(allowed);
|
|
57
|
+
}
|
|
58
|
+
export function listTables() {
|
|
59
|
+
const tables = {};
|
|
60
|
+
for (const [, file] of db.getSchemaFiles()) {
|
|
61
|
+
Object.assign(tables, file.acl_tables || {});
|
|
62
|
+
}
|
|
63
|
+
return tables;
|
|
66
64
|
}
|
package/dist/api/acl.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AccessMap } from '@axium/core/access';
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
import * as acl from '../acl.js';
|
|
4
|
-
import { parseBody, withError } from '../requests.js';
|
|
4
|
+
import { error, parseBody, withError } from '../requests.js';
|
|
5
5
|
import { addRoute } from '../routes.js';
|
|
6
|
+
import { checkAuthForItem } from '../auth.js';
|
|
6
7
|
addRoute({
|
|
7
8
|
path: '/api/acl/:itemType/:itemId',
|
|
8
9
|
params: {
|
|
9
10
|
itemType: z.string(),
|
|
10
11
|
itemId: z.uuid(),
|
|
11
12
|
},
|
|
12
|
-
async
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
async GET(request, { itemType, itemId }) {
|
|
14
|
+
const tables = acl.listTables();
|
|
15
|
+
if (!(itemType in tables))
|
|
16
|
+
error(400, 'Invalid item type: ' + itemType);
|
|
17
|
+
return await acl.get(tables[itemType], itemId).catch(withError('Failed to get access controls'));
|
|
18
|
+
},
|
|
19
|
+
async POST(request, { itemType, itemId }) {
|
|
20
|
+
const data = await parseBody(request, AccessMap);
|
|
21
|
+
const tables = acl.listTables();
|
|
22
|
+
if (!(itemType in tables))
|
|
23
|
+
error(400, 'Invalid item type: ' + itemType);
|
|
24
|
+
await checkAuthForItem(request, itemType, itemId, { manage: true });
|
|
25
|
+
return await acl.set(tables[itemType], itemId, data);
|
|
18
26
|
},
|
|
19
27
|
});
|
package/dist/api/admin.js
CHANGED
|
@@ -5,16 +5,13 @@ import { omit } from 'utilium';
|
|
|
5
5
|
import * as z from 'zod';
|
|
6
6
|
import pkg from '../../package.json' with { type: 'json' };
|
|
7
7
|
import { audit, events, getEvents } from '../audit.js';
|
|
8
|
-
import {
|
|
8
|
+
import { requireSession } from '../auth.js';
|
|
9
9
|
import { config } from '../config.js';
|
|
10
10
|
import { count, database as db } from '../database.js';
|
|
11
|
-
import { error,
|
|
11
|
+
import { error, withError } from '../requests.js';
|
|
12
12
|
import { addRoute } from '../routes.js';
|
|
13
13
|
async function assertAdmin(route, req) {
|
|
14
|
-
const
|
|
15
|
-
if (!token)
|
|
16
|
-
error(401, 'Missing token');
|
|
17
|
-
const admin = await getSessionAndUser(token).catch(withError('Invalid session', 400));
|
|
14
|
+
const admin = await requireSession(req);
|
|
18
15
|
if (!admin.user.isAdmin)
|
|
19
16
|
error(403, 'Not an administrator');
|
|
20
17
|
if (!config.admin_api)
|
|
@@ -55,9 +52,9 @@ addRoute({
|
|
|
55
52
|
addRoute({
|
|
56
53
|
path: '/api/admin/users/:userId',
|
|
57
54
|
params: { userId: z.uuid() },
|
|
58
|
-
async GET(req,
|
|
55
|
+
async GET(req, { userId }) {
|
|
59
56
|
await assertAdmin(this, req);
|
|
60
|
-
if (!
|
|
57
|
+
if (!userId)
|
|
61
58
|
error(400, 'Missing user ID');
|
|
62
59
|
const user = await db
|
|
63
60
|
.selectFrom('users')
|
|
@@ -65,7 +62,7 @@ addRoute({
|
|
|
65
62
|
.select(eb => jsonArrayFrom(eb.selectFrom('sessions').whereRef('sessions.userId', '=', 'users.id').selectAll())
|
|
66
63
|
.$castTo()
|
|
67
64
|
.as('sessions'))
|
|
68
|
-
.where('id', '=',
|
|
65
|
+
.where('id', '=', userId)
|
|
69
66
|
.executeTakeFirstOrThrow()
|
|
70
67
|
.catch(withError('User not found', 404));
|
|
71
68
|
return {
|
|
@@ -134,9 +131,9 @@ addRoute({
|
|
|
134
131
|
addRoute({
|
|
135
132
|
path: '/api/admin/audit/:eventId',
|
|
136
133
|
params: { eventId: z.uuid() },
|
|
137
|
-
async GET(req,
|
|
134
|
+
async GET(req, { eventId }) {
|
|
138
135
|
await assertAdmin(this, req);
|
|
139
|
-
if (!
|
|
136
|
+
if (!eventId)
|
|
140
137
|
error(400, 'Missing event ID');
|
|
141
138
|
const event = await db
|
|
142
139
|
.selectFrom('audit_log')
|
|
@@ -144,7 +141,7 @@ addRoute({
|
|
|
144
141
|
.select(eb => jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'audit_log.userId').selectAll())
|
|
145
142
|
.$castTo()
|
|
146
143
|
.as('user'))
|
|
147
|
-
.where('id', '=',
|
|
144
|
+
.where('id', '=', eventId)
|
|
148
145
|
.executeTakeFirstOrThrow()
|
|
149
146
|
.catch(withError('Audit event not found', 404));
|
|
150
147
|
return event;
|
package/dist/api/metadata.js
CHANGED
|
@@ -2,24 +2,17 @@ import { apps } from '@axium/core';
|
|
|
2
2
|
import { plugins } from '@axium/core/plugins';
|
|
3
3
|
import { requestMethods } from '@axium/core/requests';
|
|
4
4
|
import pkg from '../../package.json' with { type: 'json' };
|
|
5
|
-
import {
|
|
5
|
+
import { requireSession } from '../auth.js';
|
|
6
6
|
import { config } from '../config.js';
|
|
7
|
-
import { error
|
|
7
|
+
import { error } from '../requests.js';
|
|
8
8
|
import { addRoute, routes } from '../routes.js';
|
|
9
9
|
addRoute({
|
|
10
10
|
path: '/api/metadata',
|
|
11
11
|
async GET(request) {
|
|
12
12
|
if (!config.debug) {
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
15
|
-
error(401, 'Missing session token');
|
|
16
|
-
const session = await getSessionAndUser(token);
|
|
17
|
-
if (!session)
|
|
18
|
-
error(401, 'Invalid session');
|
|
19
|
-
if (!session.user.isAdmin)
|
|
13
|
+
const { user } = await requireSession(request);
|
|
14
|
+
if (!user.isAdmin)
|
|
20
15
|
error(403, 'User is not an administrator');
|
|
21
|
-
if (session.user.isSuspended)
|
|
22
|
-
error(403, 'User is suspended');
|
|
23
16
|
}
|
|
24
17
|
return {
|
|
25
18
|
version: pkg.version,
|
package/dist/api/passkeys.js
CHANGED
|
@@ -10,14 +10,14 @@ addRoute({
|
|
|
10
10
|
params: {
|
|
11
11
|
id: z.string(),
|
|
12
12
|
},
|
|
13
|
-
async GET(request,
|
|
14
|
-
const passkey = await getPasskey(
|
|
13
|
+
async GET(request, { id }) {
|
|
14
|
+
const passkey = await getPasskey(id);
|
|
15
15
|
await checkAuthForUser(request, passkey.userId);
|
|
16
16
|
return omit(passkey, 'counter', 'publicKey');
|
|
17
17
|
},
|
|
18
|
-
async PATCH(request,
|
|
18
|
+
async PATCH(request, { id }) {
|
|
19
19
|
const body = await parseBody(request, PasskeyChangeable);
|
|
20
|
-
const passkey = await getPasskey(
|
|
20
|
+
const passkey = await getPasskey(id);
|
|
21
21
|
await checkAuthForUser(request, passkey.userId);
|
|
22
22
|
const result = await db
|
|
23
23
|
.updateTable('passkeys')
|
|
@@ -28,8 +28,8 @@ addRoute({
|
|
|
28
28
|
.catch(withError('Could not update passkey'));
|
|
29
29
|
return omit(result, 'counter', 'publicKey');
|
|
30
30
|
},
|
|
31
|
-
async DELETE(request,
|
|
32
|
-
const passkey = await getPasskey(
|
|
31
|
+
async DELETE(request, { id }) {
|
|
32
|
+
const passkey = await getPasskey(id);
|
|
33
33
|
await checkAuthForUser(request, passkey.userId);
|
|
34
34
|
const { count } = await db
|
|
35
35
|
.selectFrom('passkeys')
|
package/dist/api/register.js
CHANGED
|
@@ -48,7 +48,7 @@ async function POST(request) {
|
|
|
48
48
|
const { verified, registrationInfo } = await verifyRegistrationResponse({
|
|
49
49
|
response,
|
|
50
50
|
expectedChallenge,
|
|
51
|
-
expectedOrigin: config.
|
|
51
|
+
expectedOrigin: config.origin,
|
|
52
52
|
}).catch(() => error(400, 'Verification failed'));
|
|
53
53
|
if (!verified || !registrationInfo)
|
|
54
54
|
error(401, 'Verification failed');
|
package/dist/api/users.js
CHANGED
|
@@ -32,14 +32,12 @@ addRoute({
|
|
|
32
32
|
addRoute({
|
|
33
33
|
path: '/api/users/:id',
|
|
34
34
|
params,
|
|
35
|
-
async GET(request,
|
|
36
|
-
const userId = params.id;
|
|
35
|
+
async GET(request, { id: userId }) {
|
|
37
36
|
const auth = await checkAuthForUser(request, userId).catch(() => null);
|
|
38
37
|
const user = auth?.user || (await getUser(userId).catch(withError('User does not exist', 404)));
|
|
39
38
|
return stripUser(user, !!auth);
|
|
40
39
|
},
|
|
41
|
-
async PATCH(request,
|
|
42
|
-
const userId = params.id;
|
|
40
|
+
async PATCH(request, { id: userId }) {
|
|
43
41
|
const body = await parseBody(request, UserChangeable);
|
|
44
42
|
await checkAuthForUser(request, userId);
|
|
45
43
|
if ('email' in body)
|
|
@@ -55,8 +53,7 @@ addRoute({
|
|
|
55
53
|
.catch(withError('Failed to update user'));
|
|
56
54
|
return stripUser(result, true);
|
|
57
55
|
},
|
|
58
|
-
async DELETE(request,
|
|
59
|
-
const userId = params.id;
|
|
56
|
+
async DELETE(request, { id: userId }) {
|
|
60
57
|
await checkAuthForUser(request, userId, true);
|
|
61
58
|
const result = await db
|
|
62
59
|
.deleteFrom('users')
|
|
@@ -71,8 +68,7 @@ addRoute({
|
|
|
71
68
|
addRoute({
|
|
72
69
|
path: '/api/users/:id/full',
|
|
73
70
|
params,
|
|
74
|
-
async GET(request,
|
|
75
|
-
const userId = params.id;
|
|
71
|
+
async GET(request, { id: userId }) {
|
|
76
72
|
const { user } = await checkAuthForUser(request, userId);
|
|
77
73
|
const sessions = await getSessions(userId);
|
|
78
74
|
return {
|
|
@@ -84,8 +80,7 @@ addRoute({
|
|
|
84
80
|
addRoute({
|
|
85
81
|
path: '/api/users/:id/auth',
|
|
86
82
|
params,
|
|
87
|
-
async OPTIONS(request,
|
|
88
|
-
const userId = params.id;
|
|
83
|
+
async OPTIONS(request, { id: userId }) {
|
|
89
84
|
const { type } = await parseBody(request, UserAuthOptions);
|
|
90
85
|
const user = await getUser(userId).catch(withError('User does not exist', 404));
|
|
91
86
|
if (user.isSuspended)
|
|
@@ -100,8 +95,7 @@ addRoute({
|
|
|
100
95
|
challenges.set(userId, { data: options.challenge, type });
|
|
101
96
|
return options;
|
|
102
97
|
},
|
|
103
|
-
async POST(request,
|
|
104
|
-
const userId = params.id;
|
|
98
|
+
async POST(request, { id: userId }) {
|
|
105
99
|
const response = await parseBody(request, PasskeyAuthenticationResponse);
|
|
106
100
|
const auth = challenges.get(userId);
|
|
107
101
|
if (!auth)
|
|
@@ -116,7 +110,7 @@ addRoute({
|
|
|
116
110
|
response,
|
|
117
111
|
credential: passkey,
|
|
118
112
|
expectedChallenge,
|
|
119
|
-
expectedOrigin: config.
|
|
113
|
+
expectedOrigin: config.origin,
|
|
120
114
|
expectedRPID: config.auth.rp_id,
|
|
121
115
|
})
|
|
122
116
|
.catch(withError('Verification failed', 400));
|
|
@@ -143,8 +137,7 @@ addRoute({
|
|
|
143
137
|
/**
|
|
144
138
|
* Get passkey registration options for a user.
|
|
145
139
|
*/
|
|
146
|
-
async OPTIONS(request,
|
|
147
|
-
const userId = params.id;
|
|
140
|
+
async OPTIONS(request, { id: userId }) {
|
|
148
141
|
const existing = await getPasskeysByUserId(userId);
|
|
149
142
|
const { user } = await checkAuthForUser(request, userId);
|
|
150
143
|
const options = await webauthn.generateRegistrationOptions({
|
|
@@ -166,8 +159,7 @@ addRoute({
|
|
|
166
159
|
/**
|
|
167
160
|
* Get passkeys for a user.
|
|
168
161
|
*/
|
|
169
|
-
async GET(request,
|
|
170
|
-
const userId = params.id;
|
|
162
|
+
async GET(request, { id: userId }) {
|
|
171
163
|
await checkAuthForUser(request, userId);
|
|
172
164
|
const passkeys = await getPasskeysByUserId(userId);
|
|
173
165
|
return passkeys.map(p => omit(p, 'publicKey', 'counter'));
|
|
@@ -175,8 +167,7 @@ addRoute({
|
|
|
175
167
|
/**
|
|
176
168
|
* Register a new passkey for an existing user.
|
|
177
169
|
*/
|
|
178
|
-
async PUT(request,
|
|
179
|
-
const userId = params.id;
|
|
170
|
+
async PUT(request, { id: userId }) {
|
|
180
171
|
const response = await parseBody(request, PasskeyRegistration);
|
|
181
172
|
await checkAuthForUser(request, userId);
|
|
182
173
|
const expectedChallenge = registrations.get(userId);
|
|
@@ -187,7 +178,7 @@ addRoute({
|
|
|
187
178
|
.verifyRegistrationResponse({
|
|
188
179
|
response,
|
|
189
180
|
expectedChallenge,
|
|
190
|
-
expectedOrigin: config.
|
|
181
|
+
expectedOrigin: config.origin,
|
|
191
182
|
})
|
|
192
183
|
.catch(withError('Verification failed', 400));
|
|
193
184
|
if (!verified || !registrationInfo)
|
|
@@ -205,13 +196,11 @@ addRoute({
|
|
|
205
196
|
addRoute({
|
|
206
197
|
path: '/api/users/:id/sessions',
|
|
207
198
|
params,
|
|
208
|
-
async GET(request,
|
|
209
|
-
const userId = params.id;
|
|
199
|
+
async GET(request, { id: userId }) {
|
|
210
200
|
await checkAuthForUser(request, userId);
|
|
211
201
|
return (await getSessions(userId).catch(e => error(503, 'Failed to get sessions' + (config.debug ? ': ' + e : '')))).map(s => omit(s, 'token'));
|
|
212
202
|
},
|
|
213
|
-
async DELETE(request,
|
|
214
|
-
const userId = params.id;
|
|
203
|
+
async DELETE(request, { id: userId }) {
|
|
215
204
|
const body = await parseBody(request, LogoutSessions);
|
|
216
205
|
await checkAuthForUser(request, userId, body.confirm_all);
|
|
217
206
|
if (!body.confirm_all && !Array.isArray(body.id))
|
|
@@ -229,8 +218,7 @@ addRoute({
|
|
|
229
218
|
addRoute({
|
|
230
219
|
path: '/api/users/:id/verify_email',
|
|
231
220
|
params,
|
|
232
|
-
async OPTIONS(request,
|
|
233
|
-
const userId = params.id;
|
|
221
|
+
async OPTIONS(request, { id: userId }) {
|
|
234
222
|
if (!config.auth.email_verification)
|
|
235
223
|
return { enabled: false };
|
|
236
224
|
await checkAuthForUser(request, userId);
|
|
@@ -238,16 +226,14 @@ addRoute({
|
|
|
238
226
|
return { enabled: false };
|
|
239
227
|
return { enabled: true };
|
|
240
228
|
},
|
|
241
|
-
async GET(request,
|
|
242
|
-
const userId = params.id;
|
|
229
|
+
async GET(request, { id: userId }) {
|
|
243
230
|
const { user } = await checkAuthForUser(request, userId);
|
|
244
231
|
if (user.emailVerified)
|
|
245
232
|
error(409, 'Email already verified');
|
|
246
233
|
const verification = await createVerification('verify_email', userId, config.auth.verification_timeout * 60);
|
|
247
234
|
return omit(verification, 'token', 'role');
|
|
248
235
|
},
|
|
249
|
-
async POST(request,
|
|
250
|
-
const userId = params.id;
|
|
236
|
+
async POST(request, { id: userId }) {
|
|
251
237
|
const { token } = await parseBody(request, z.object({ token: z.string() }));
|
|
252
238
|
const { user } = await checkAuthForUser(request, userId);
|
|
253
239
|
if (user.emailVerified)
|
package/dist/audit.d.ts
CHANGED
|
@@ -15,9 +15,6 @@ export interface $EventTypes {
|
|
|
15
15
|
logout: {
|
|
16
16
|
sessions: string[];
|
|
17
17
|
};
|
|
18
|
-
acl_id_mismatch: {
|
|
19
|
-
item: string;
|
|
20
|
-
};
|
|
21
18
|
admin_change: {
|
|
22
19
|
user: string;
|
|
23
20
|
};
|
|
@@ -25,6 +22,9 @@ export interface $EventTypes {
|
|
|
25
22
|
route: string;
|
|
26
23
|
session: string;
|
|
27
24
|
};
|
|
25
|
+
response_error: {
|
|
26
|
+
stack?: string;
|
|
27
|
+
};
|
|
28
28
|
}
|
|
29
29
|
export type EventName = keyof $EventTypes;
|
|
30
30
|
export type EventExtra<T extends EventName> = $EventTypes[T];
|
package/dist/audit.js
CHANGED
|
@@ -74,7 +74,7 @@ export async function audit(eventName, userId, extra) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
export function getEvents(filter) {
|
|
77
|
-
let query = database.selectFrom('audit_log').selectAll();
|
|
77
|
+
let query = database.selectFrom('audit_log').selectAll().orderBy('timestamp', 'desc');
|
|
78
78
|
if ('user' in filter && !filter.user)
|
|
79
79
|
query = query.where('userId', 'is', null);
|
|
80
80
|
else if (filter.user)
|
|
@@ -105,14 +105,6 @@ addEvent({
|
|
|
105
105
|
tags: ['cli'],
|
|
106
106
|
extra: { user: z.string() },
|
|
107
107
|
});
|
|
108
|
-
addEvent({
|
|
109
|
-
source: '@axium/server',
|
|
110
|
-
name: 'acl_id_mismatch',
|
|
111
|
-
severity: Severity.Critical,
|
|
112
|
-
tags: ['acl', 'auth'],
|
|
113
|
-
extra: { item: z.string() },
|
|
114
|
-
noAutoSuspend: true,
|
|
115
|
-
});
|
|
116
108
|
addEvent({
|
|
117
109
|
source: '@axium/server',
|
|
118
110
|
name: 'admin_api',
|
|
@@ -120,3 +112,10 @@ addEvent({
|
|
|
120
112
|
tags: ['auth'],
|
|
121
113
|
extra: { route: z.string(), session: z.string() },
|
|
122
114
|
});
|
|
115
|
+
addEvent({
|
|
116
|
+
source: '@axium/server',
|
|
117
|
+
name: 'response_error',
|
|
118
|
+
severity: Severity.Error,
|
|
119
|
+
tags: [],
|
|
120
|
+
extra: { stack: z.string().optional() },
|
|
121
|
+
});
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Passkey,
|
|
2
|
-
import type { Insertable } from 'kysely';
|
|
1
|
+
import type { Passkey, Session, UserInternal, Verification } from '@axium/core';
|
|
2
|
+
import type { Insertable, Selectable } from 'kysely';
|
|
3
3
|
import * as acl from './acl.js';
|
|
4
4
|
import { type Schema } from './database.js';
|
|
5
5
|
export declare function getUser(id: string): Promise<UserInternal>;
|
|
@@ -13,6 +13,7 @@ export interface SessionAndUser extends SessionInternal {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function getSessionAndUser(token: string): Promise<SessionAndUser>;
|
|
15
15
|
export declare function getSession(sessionId: string): Promise<SessionInternal>;
|
|
16
|
+
export declare function requireSession(request: Request, sensitive?: boolean): Promise<SessionAndUser>;
|
|
16
17
|
export declare function getSessions(userId: string): Promise<SessionInternal[]>;
|
|
17
18
|
export type VerificationRole = 'verify_email' | 'login';
|
|
18
19
|
export interface VerificationInternal extends Verification {
|
|
@@ -38,10 +39,14 @@ export interface UserAuthResult extends SessionAndUser {
|
|
|
38
39
|
accessor: UserInternal;
|
|
39
40
|
}
|
|
40
41
|
export declare function checkAuthForUser(request: Request, userId: string, sensitive?: boolean): Promise<UserAuthResult>;
|
|
41
|
-
export interface ItemAuthResult<
|
|
42
|
+
export interface ItemAuthResult<TB extends acl.TargetName> {
|
|
42
43
|
fromACL: boolean;
|
|
43
|
-
item:
|
|
44
|
+
item: Selectable<Schema[TB]>;
|
|
44
45
|
user?: UserInternal;
|
|
45
46
|
session?: SessionInternal;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Authenticate a request against an "item" which has an ACL table.
|
|
50
|
+
* This will fetch the item, ACLs, users, and the authenticating session.
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkAuthForItem<const TB extends acl.TargetName>(request: Request, itemType: TB, itemId: string, permissions: Partial<acl.PermissionsFor<`acl.${TB}`>>): Promise<ItemAuthResult<TB>>;
|