@axium/server 0.20.6 → 0.21.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/build/client/_app/immutable/chunks/BT6JVi7A.js +3 -0
- package/build/client/_app/immutable/chunks/BT6JVi7A.js.br +0 -0
- package/build/client/_app/immutable/chunks/BT6JVi7A.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.DN-14WEp.js → app.ZFo0Iz7Y.js} +2 -2
- package/build/client/_app/immutable/entry/app.ZFo0Iz7Y.js.br +0 -0
- package/build/client/_app/immutable/entry/app.ZFo0Iz7Y.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BeJCp-7l.js +1 -0
- package/build/client/_app/immutable/entry/start.BeJCp-7l.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BeJCp-7l.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.CYGWa5Ht.js → 1.jmd85CY0.js} +1 -1
- package/build/client/_app/immutable/nodes/1.jmd85CY0.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.jmd85CY0.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{1-ChHp-lcj.js → 1-IUsQDUky.js} +2 -2
- package/build/server/chunks/{1-ChHp-lcj.js.map → 1-IUsQDUky.js.map} +1 -1
- package/build/server/chunks/{3-qz4gvEJj.js → 3-BfZcxicd.js} +2 -2
- package/build/server/chunks/{3-qz4gvEJj.js.map → 3-BfZcxicd.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-CNZCbT6U.js → _page.svelte-DnC0rUFy.js} +2 -2
- package/build/server/chunks/{_page.svelte-CNZCbT6U.js.map → _page.svelte-DnC0rUFy.js.map} +1 -1
- package/build/server/chunks/{hooks.server-D31Irsqw.js → hooks.server-DHvv3JCJ.js} +162 -29
- package/build/server/chunks/hooks.server-DHvv3JCJ.js.map +1 -0
- package/build/server/chunks/{string-CafUlmcI.js → string-SLyGnEy-.js} +6 -1
- package/build/server/chunks/{string-CafUlmcI.js.map → string-SLyGnEy-.js.map} +1 -1
- package/build/server/index.js +2 -2
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +3 -3
- package/build/server/manifest.js.map +1 -1
- package/dist/api/metadata.js +15 -4
- package/dist/api/register.js +2 -0
- package/dist/api/session.js +2 -0
- package/dist/api/users.js +6 -1
- package/dist/audit.d.ts +84 -0
- package/dist/audit.js +125 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +10 -2
- package/dist/cli.js +95 -31
- package/dist/config.d.ts +14 -8
- package/dist/config.js +20 -10
- package/dist/database.d.ts +12 -0
- package/dist/database.js +28 -2
- package/dist/io.d.ts +4 -0
- package/dist/io.js +9 -0
- package/dist/requests.d.ts +1 -2
- package/dist/requests.js +1 -1
- package/package.json +1 -1
- package/build/client/_app/immutable/chunks/LvObEX8u.js +0 -3
- package/build/client/_app/immutable/chunks/LvObEX8u.js.br +0 -0
- package/build/client/_app/immutable/chunks/LvObEX8u.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.DN-14WEp.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DN-14WEp.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.AjI7jPvA.js +0 -1
- package/build/client/_app/immutable/entry/start.AjI7jPvA.js.br +0 -2
- package/build/client/_app/immutable/entry/start.AjI7jPvA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.CYGWa5Ht.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.CYGWa5Ht.js.gz +0 -0
- package/build/server/chunks/hooks.server-D31Irsqw.js.map +0 -1
package/build/server/manifest.js
CHANGED
|
@@ -10,12 +10,12 @@ return {
|
|
|
10
10
|
assets: new Set(["icons/brands.svg","icons/light.svg","icons/regular.svg","icons/solid.svg","styles.css"]),
|
|
11
11
|
mimeTypes: {".svg":"image/svg+xml",".css":"text/css"},
|
|
12
12
|
_: {
|
|
13
|
-
client: {start:"_app/immutable/entry/start.
|
|
13
|
+
client: {start:"_app/immutable/entry/start.BeJCp-7l.js",app:"_app/immutable/entry/app.ZFo0Iz7Y.js",imports:["_app/immutable/entry/start.BeJCp-7l.js","_app/immutable/chunks/BT6JVi7A.js","_app/immutable/chunks/BZOe2Jko.js","_app/immutable/chunks/uU8Mt6Mg.js","_app/immutable/chunks/BK1-xGGj.js","_app/immutable/chunks/sfKJ2mhH.js","_app/immutable/entry/app.ZFo0Iz7Y.js","_app/immutable/chunks/uU8Mt6Mg.js","_app/immutable/chunks/BK1-xGGj.js","_app/immutable/chunks/DsnmJJEf.js","_app/immutable/chunks/BZOe2Jko.js","_app/immutable/chunks/sfKJ2mhH.js","_app/immutable/chunks/DGhIdZ_j.js"],stylesheets:[],fonts:[],uses_env_dynamic_public:false},
|
|
14
14
|
nodes: [
|
|
15
15
|
__memo(() => import('./chunks/0-B8fDicB8.js')),
|
|
16
|
-
__memo(() => import('./chunks/1-
|
|
16
|
+
__memo(() => import('./chunks/1-IUsQDUky.js')),
|
|
17
17
|
__memo(() => import('./chunks/2-CVTt4PZM.js')),
|
|
18
|
-
__memo(() => import('./chunks/3-
|
|
18
|
+
__memo(() => import('./chunks/3-BfZcxicd.js')),
|
|
19
19
|
__memo(() => import('./chunks/4-3AO_VRm0.js')),
|
|
20
20
|
__memo(() => import('./chunks/5-sUeA0D7s.js')),
|
|
21
21
|
__memo(() => import('./chunks/6-BqCt3ZcB.js'))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sources":["../../.svelte-kit/adapter-node/manifest.js"],"sourcesContent":["export const manifest = (() => {\nfunction __memo(fn) {\n\tlet value;\n\treturn () => value ??= (value = fn());\n}\n\nreturn {\n\tappDir: \"_app\",\n\tappPath: \"_app\",\n\tassets: new Set([\"icons/brands.svg\",\"icons/light.svg\",\"icons/regular.svg\",\"icons/solid.svg\",\"styles.css\"]),\n\tmimeTypes: {\".svg\":\"image/svg+xml\",\".css\":\"text/css\"},\n\t_: {\n\t\tclient: {start:\"_app/immutable/entry/start.
|
|
1
|
+
{"version":3,"file":"manifest.js","sources":["../../.svelte-kit/adapter-node/manifest.js"],"sourcesContent":["export const manifest = (() => {\nfunction __memo(fn) {\n\tlet value;\n\treturn () => value ??= (value = fn());\n}\n\nreturn {\n\tappDir: \"_app\",\n\tappPath: \"_app\",\n\tassets: new Set([\"icons/brands.svg\",\"icons/light.svg\",\"icons/regular.svg\",\"icons/solid.svg\",\"styles.css\"]),\n\tmimeTypes: {\".svg\":\"image/svg+xml\",\".css\":\"text/css\"},\n\t_: {\n\t\tclient: {start:\"_app/immutable/entry/start.BeJCp-7l.js\",app:\"_app/immutable/entry/app.ZFo0Iz7Y.js\",imports:[\"_app/immutable/entry/start.BeJCp-7l.js\",\"_app/immutable/chunks/BT6JVi7A.js\",\"_app/immutable/chunks/BZOe2Jko.js\",\"_app/immutable/chunks/uU8Mt6Mg.js\",\"_app/immutable/chunks/BK1-xGGj.js\",\"_app/immutable/chunks/sfKJ2mhH.js\",\"_app/immutable/entry/app.ZFo0Iz7Y.js\",\"_app/immutable/chunks/uU8Mt6Mg.js\",\"_app/immutable/chunks/BK1-xGGj.js\",\"_app/immutable/chunks/DsnmJJEf.js\",\"_app/immutable/chunks/BZOe2Jko.js\",\"_app/immutable/chunks/sfKJ2mhH.js\",\"_app/immutable/chunks/DGhIdZ_j.js\"],stylesheets:[],fonts:[],uses_env_dynamic_public:false},\n\t\tnodes: [\n\t\t\t__memo(() => import('./nodes/0.js')),\n\t\t\t__memo(() => import('./nodes/1.js')),\n\t\t\t__memo(() => import('./nodes/2.js')),\n\t\t\t__memo(() => import('./nodes/3.js')),\n\t\t\t__memo(() => import('./nodes/4.js')),\n\t\t\t__memo(() => import('./nodes/5.js')),\n\t\t\t__memo(() => import('./nodes/6.js'))\n\t\t],\n\t\tremotes: {\n\t\t\t\n\t\t},\n\t\troutes: [\n\t\t\t{\n\t\t\t\tid: \"/_axium/default\",\n\t\t\t\tpattern: /^\\/_axium\\/default\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 2 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/account\",\n\t\t\t\tpattern: /^\\/account\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 3 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/login\",\n\t\t\t\tpattern: /^\\/login\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 4 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/logout\",\n\t\t\t\tpattern: /^\\/logout\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 5 },\n\t\t\t\tendpoint: null\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"/register\",\n\t\t\t\tpattern: /^\\/register\\/?$/,\n\t\t\t\tparams: [],\n\t\t\t\tpage: { layouts: [0,], errors: [1,], leaf: 6 },\n\t\t\t\tendpoint: null\n\t\t\t}\n\t\t],\n\t\tprerendered_routes: new Set([]),\n\t\tmatchers: async () => {\n\t\t\t\n\t\t\treturn { };\n\t\t},\n\t\tserver_assets: {}\n\t}\n}\n})();\n\nexport const prerendered = new Set([]);\n\nexport const base = \"\";"],"names":[],"mappings":"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"}
|
package/dist/api/metadata.js
CHANGED
|
@@ -2,13 +2,24 @@ import { requestMethods } from '@axium/core/requests';
|
|
|
2
2
|
import pkg from '../../package.json' with { type: 'json' };
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import { plugins } from '../plugins.js';
|
|
5
|
-
import { error } from '../requests.js';
|
|
5
|
+
import { error, getToken } from '../requests.js';
|
|
6
6
|
import { addRoute, routes } from '../routes.js';
|
|
7
|
+
import { getSessionAndUser } from '../auth.js';
|
|
7
8
|
addRoute({
|
|
8
9
|
path: '/api/metadata',
|
|
9
|
-
async GET() {
|
|
10
|
-
if (config.
|
|
11
|
-
|
|
10
|
+
async GET(event) {
|
|
11
|
+
if (!config.debug) {
|
|
12
|
+
const token = getToken(event);
|
|
13
|
+
if (!token)
|
|
14
|
+
error(401, 'Missing session token');
|
|
15
|
+
const session = await getSessionAndUser(token);
|
|
16
|
+
if (!session)
|
|
17
|
+
error(401, 'Invalid session');
|
|
18
|
+
if (!session.user.isAdmin)
|
|
19
|
+
error(403, 'User is not an administrator');
|
|
20
|
+
if (session.user.isSuspended)
|
|
21
|
+
error(403, 'User is suspended');
|
|
22
|
+
}
|
|
12
23
|
return {
|
|
13
24
|
version: pkg.version,
|
|
14
25
|
routes: Object.fromEntries(routes
|
package/dist/api/register.js
CHANGED
|
@@ -7,6 +7,7 @@ import config from '../config.js';
|
|
|
7
7
|
import { database as db } from '../database.js';
|
|
8
8
|
import { createSessionData, error, parseBody, withError } from '../requests.js';
|
|
9
9
|
import { addRoute } from '../routes.js';
|
|
10
|
+
import { audit } from '../audit.js';
|
|
10
11
|
// Map of user ID => challenge
|
|
11
12
|
const registrations = new Map();
|
|
12
13
|
async function OPTIONS(event) {
|
|
@@ -56,6 +57,7 @@ async function POST(event) {
|
|
|
56
57
|
.values({ id: userId, name, email: email.toLowerCase() })
|
|
57
58
|
.executeTakeFirstOrThrow()
|
|
58
59
|
.catch(withError('Failed to create user'));
|
|
60
|
+
await audit('user_created', userId);
|
|
59
61
|
await createPasskey({
|
|
60
62
|
transports: [],
|
|
61
63
|
...registrationInfo.credential,
|
package/dist/api/session.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getSessionAndUser } from '../auth.js';
|
|
|
3
3
|
import { database as db } from '../database.js';
|
|
4
4
|
import { error, getToken, stripUser, withError } from '../requests.js';
|
|
5
5
|
import { addRoute } from '../routes.js';
|
|
6
|
+
import { audit } from '../audit.js';
|
|
6
7
|
addRoute({
|
|
7
8
|
path: '/api/session',
|
|
8
9
|
async GET(event) {
|
|
@@ -25,6 +26,7 @@ addRoute({
|
|
|
25
26
|
.returningAll()
|
|
26
27
|
.executeTakeFirstOrThrow()
|
|
27
28
|
.catch((e) => (e.message == 'no result' ? error(404, 'Session does not exist') : error(400, 'Invalid session')));
|
|
29
|
+
await audit('logout', result.userId, { sessions: [result.id] });
|
|
28
30
|
return omit(result, 'token');
|
|
29
31
|
},
|
|
30
32
|
});
|
package/dist/api/users.js
CHANGED
|
@@ -8,6 +8,7 @@ import { config } from '../config.js';
|
|
|
8
8
|
import { database as db } from '../database.js';
|
|
9
9
|
import { createSessionData, error, parseBody, stripUser, withError } from '../requests.js';
|
|
10
10
|
import { addRoute } from '../routes.js';
|
|
11
|
+
import { audit } from '../audit.js';
|
|
11
12
|
const challenges = new Map();
|
|
12
13
|
const params = { id: z.uuid() };
|
|
13
14
|
/**
|
|
@@ -59,6 +60,7 @@ addRoute({
|
|
|
59
60
|
.returningAll()
|
|
60
61
|
.executeTakeFirstOrThrow()
|
|
61
62
|
.catch(withError('Failed to delete user'));
|
|
63
|
+
await audit('user_deleted', userId);
|
|
62
64
|
return result;
|
|
63
65
|
},
|
|
64
66
|
});
|
|
@@ -81,7 +83,9 @@ addRoute({
|
|
|
81
83
|
async OPTIONS(event) {
|
|
82
84
|
const userId = event.params.id;
|
|
83
85
|
const { type } = await parseBody(event, UserAuthOptions);
|
|
84
|
-
await getUser(userId).catch(withError('User does not exist', 404));
|
|
86
|
+
const user = await getUser(userId).catch(withError('User does not exist', 404));
|
|
87
|
+
if (user.isSuspended)
|
|
88
|
+
error(403, 'User is suspended');
|
|
85
89
|
const passkeys = await getPasskeysByUserId(userId);
|
|
86
90
|
if (!passkeys)
|
|
87
91
|
error(409, 'No passkeys exists for this user');
|
|
@@ -211,6 +215,7 @@ addRoute({
|
|
|
211
215
|
.returningAll()
|
|
212
216
|
.execute()
|
|
213
217
|
.catch(withError('Failed to delete one or more sessions'));
|
|
218
|
+
await audit('logout', userId, { sessions: result.map(s => s.id) });
|
|
214
219
|
return result.map(s => omit(s, 'token'));
|
|
215
220
|
},
|
|
216
221
|
});
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Insertable } from 'kysely';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import { type Schema } from './database.js';
|
|
4
|
+
export declare enum Severity {
|
|
5
|
+
Emergency = 0,
|
|
6
|
+
Alert = 1,
|
|
7
|
+
Critical = 2,
|
|
8
|
+
Error = 3,
|
|
9
|
+
Warning = 4,
|
|
10
|
+
Notice = 5,
|
|
11
|
+
Info = 6,
|
|
12
|
+
Debug = 7
|
|
13
|
+
}
|
|
14
|
+
export declare function styleSeverity(sev: Severity, align?: boolean): string;
|
|
15
|
+
export interface AuditEvent<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
16
|
+
/** UUID of the event */
|
|
17
|
+
id: string;
|
|
18
|
+
/** UUID of the user that triggered the event. `null` for events triggered via the server CLI */
|
|
19
|
+
userID?: string | null;
|
|
20
|
+
/** When the event happened */
|
|
21
|
+
timestamp: Date;
|
|
22
|
+
/** How severe the event is */
|
|
23
|
+
severity: Severity;
|
|
24
|
+
/** Snake case name for the event */
|
|
25
|
+
name: string;
|
|
26
|
+
/** The source of the event. This should be a package name */
|
|
27
|
+
source: string;
|
|
28
|
+
/** Tags for the event. For example, `auth` for authentication events */
|
|
29
|
+
tags: string[];
|
|
30
|
+
/** Additional event specific data. */
|
|
31
|
+
extra: T;
|
|
32
|
+
}
|
|
33
|
+
export interface AuditEventInit extends Insertable<Schema['audit_log']> {
|
|
34
|
+
}
|
|
35
|
+
export declare function audit_raw(event: AuditEventInit): Promise<void>;
|
|
36
|
+
export interface $EventTypes {
|
|
37
|
+
user_created: never;
|
|
38
|
+
user_deleted: never;
|
|
39
|
+
new_session: {
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
42
|
+
logout: {
|
|
43
|
+
sessions: string[];
|
|
44
|
+
};
|
|
45
|
+
acl_id_mismatch: {
|
|
46
|
+
item: string;
|
|
47
|
+
};
|
|
48
|
+
admin_change: {
|
|
49
|
+
user: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export type EventName = keyof $EventTypes;
|
|
53
|
+
export type EventExtra<T extends EventName> = $EventTypes[T];
|
|
54
|
+
export interface AuditEventConfigInit {
|
|
55
|
+
name: EventName;
|
|
56
|
+
source: string;
|
|
57
|
+
severity: Severity;
|
|
58
|
+
tags: string[];
|
|
59
|
+
/** Schema for the extra data */
|
|
60
|
+
extra?: z.core.$ZodShape;
|
|
61
|
+
noAutoSuspend?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface AuditEventConfig {
|
|
64
|
+
name: EventName;
|
|
65
|
+
source: string;
|
|
66
|
+
severity: Severity;
|
|
67
|
+
tags: string[];
|
|
68
|
+
/** Schema for the extra data */
|
|
69
|
+
extra?: z.ZodObject;
|
|
70
|
+
noAutoSuspend?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export declare function addEvent(init: AuditEventConfigInit): void;
|
|
73
|
+
export declare function audit<T extends EventName>(eventName: T, userId?: string, extra?: EventExtra<T>): Promise<void>;
|
|
74
|
+
export interface AuditFilter {
|
|
75
|
+
since?: Date;
|
|
76
|
+
until?: Date;
|
|
77
|
+
user?: string;
|
|
78
|
+
severity?: Severity;
|
|
79
|
+
source?: string;
|
|
80
|
+
tags?: string[];
|
|
81
|
+
event?: string;
|
|
82
|
+
cli?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export declare function getEvents(filter: AuditFilter): Promise<AuditEvent[]>;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { capitalize, omit } from 'utilium';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
import config from './config.js';
|
|
5
|
+
import { database } from './database.js';
|
|
6
|
+
import * as io from './io.js';
|
|
7
|
+
export var Severity;
|
|
8
|
+
(function (Severity) {
|
|
9
|
+
Severity[Severity["Emergency"] = 0] = "Emergency";
|
|
10
|
+
Severity[Severity["Alert"] = 1] = "Alert";
|
|
11
|
+
Severity[Severity["Critical"] = 2] = "Critical";
|
|
12
|
+
Severity[Severity["Error"] = 3] = "Error";
|
|
13
|
+
Severity[Severity["Warning"] = 4] = "Warning";
|
|
14
|
+
Severity[Severity["Notice"] = 5] = "Notice";
|
|
15
|
+
Severity[Severity["Info"] = 6] = "Info";
|
|
16
|
+
Severity[Severity["Debug"] = 7] = "Debug";
|
|
17
|
+
})(Severity || (Severity = {}));
|
|
18
|
+
const severityFormat = {
|
|
19
|
+
[Severity.Emergency]: ['bgRedBright', 'white', 'underline'],
|
|
20
|
+
[Severity.Alert]: ['bgRedBright', 'white'],
|
|
21
|
+
[Severity.Critical]: ['bold', 'redBright'],
|
|
22
|
+
[Severity.Error]: 'redBright',
|
|
23
|
+
[Severity.Warning]: 'yellowBright',
|
|
24
|
+
[Severity.Notice]: 'cyanBright',
|
|
25
|
+
[Severity.Info]: [],
|
|
26
|
+
[Severity.Debug]: ['dim'],
|
|
27
|
+
};
|
|
28
|
+
export function styleSeverity(sev, align = false) {
|
|
29
|
+
const text = align ? Severity[sev].padEnd(9) : Severity[sev];
|
|
30
|
+
return styleText(severityFormat[sev], text.toUpperCase());
|
|
31
|
+
}
|
|
32
|
+
function output(event) {
|
|
33
|
+
if (event.severity > Severity[capitalize(config.audit.min_severity)])
|
|
34
|
+
return;
|
|
35
|
+
console.error('[audit]', styleText('dim', io.prettyDate(event.timestamp)), styleSeverity(event.severity), event.name);
|
|
36
|
+
}
|
|
37
|
+
export async function audit_raw(event) {
|
|
38
|
+
if (!config.audit.allow_raw) {
|
|
39
|
+
io.warn('[audit] Ignoring raw event (disabled)');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const result = await database.insertInto('audit_log').values(event).returningAll().executeTakeFirstOrThrow();
|
|
43
|
+
output(result);
|
|
44
|
+
}
|
|
45
|
+
const events = new Map();
|
|
46
|
+
export function addEvent(init) {
|
|
47
|
+
if (events.has(init.name))
|
|
48
|
+
throw io.error(`Can not register multiple events with the same name ("${init.name}")`);
|
|
49
|
+
const config = {
|
|
50
|
+
...init,
|
|
51
|
+
extra: init.extra ? z.object(init.extra) : undefined,
|
|
52
|
+
};
|
|
53
|
+
events.set(init.name, config);
|
|
54
|
+
}
|
|
55
|
+
export async function audit(eventName, userId, extra) {
|
|
56
|
+
const cfg = events.get(eventName);
|
|
57
|
+
if (!cfg) {
|
|
58
|
+
io.warn('Ignoring audit event with unknown event name: ' + eventName);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (cfg.extra)
|
|
63
|
+
extra = cfg.extra.parse(extra);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
io.error('Audit event has invalid extra data: ' + eventName);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const event = await database
|
|
70
|
+
.insertInto('audit_log')
|
|
71
|
+
.values({ ...omit(cfg, 'extra'), extra, userId })
|
|
72
|
+
.returningAll()
|
|
73
|
+
.executeTakeFirstOrThrow();
|
|
74
|
+
output(event);
|
|
75
|
+
if (userId && !cfg.noAutoSuspend && event.severity < Severity[capitalize(config.audit.auto_suspend)]) {
|
|
76
|
+
await database
|
|
77
|
+
.updateTable('users')
|
|
78
|
+
.set({ isSuspended: true })
|
|
79
|
+
.where('id', '=', userId)
|
|
80
|
+
.returningAll()
|
|
81
|
+
.executeTakeFirstOrThrow()
|
|
82
|
+
.then(user => console.error('[audit] Auto-suspended user:', user.id, `<${user.email}>`))
|
|
83
|
+
.catch(() => null);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function getEvents(filter) {
|
|
87
|
+
let query = database.selectFrom('audit_log').selectAll();
|
|
88
|
+
if (filter.cli)
|
|
89
|
+
query = query.where('userId', 'is', null);
|
|
90
|
+
else if (filter.user)
|
|
91
|
+
query = query.where('userId', '=', filter.user);
|
|
92
|
+
if (filter.source)
|
|
93
|
+
query = query.where('source', '=', filter.source);
|
|
94
|
+
if (filter.event)
|
|
95
|
+
query = query.where('name', '=', filter.event);
|
|
96
|
+
if (filter.tags)
|
|
97
|
+
query = query.where('tags', '@>', filter.tags);
|
|
98
|
+
if (filter.severity)
|
|
99
|
+
query = query.where('severity', '<=', filter.severity);
|
|
100
|
+
if (filter.since)
|
|
101
|
+
query = query.where('timestamp', '>=', filter.since);
|
|
102
|
+
if (filter.until)
|
|
103
|
+
query = query.where('timestamp', '<=', filter.until);
|
|
104
|
+
return await query.execute();
|
|
105
|
+
}
|
|
106
|
+
// Register built-ins
|
|
107
|
+
addEvent({ source: '@axium/server', name: 'user_created', severity: Severity.Info, tags: ['user'] });
|
|
108
|
+
addEvent({ source: '@axium/server', name: 'user_deleted', severity: Severity.Info, tags: ['user'] });
|
|
109
|
+
addEvent({ source: '@axium/server', name: 'new_session', severity: Severity.Info, tags: ['user'], extra: { id: z.string() } });
|
|
110
|
+
addEvent({ source: '@axium/server', name: 'logout', severity: Severity.Info, tags: ['user'], extra: { sessions: z.array(z.string()) } });
|
|
111
|
+
addEvent({
|
|
112
|
+
source: '@axium/server',
|
|
113
|
+
name: 'admin_change',
|
|
114
|
+
severity: Severity.Notice,
|
|
115
|
+
tags: ['cli'],
|
|
116
|
+
extra: { user: z.string() },
|
|
117
|
+
});
|
|
118
|
+
addEvent({
|
|
119
|
+
source: '@axium/server',
|
|
120
|
+
name: 'acl_id_mismatch',
|
|
121
|
+
severity: Severity.Critical,
|
|
122
|
+
tags: ['acl', 'auth'],
|
|
123
|
+
extra: { item: z.string() },
|
|
124
|
+
noAutoSuspend: true,
|
|
125
|
+
});
|
package/dist/auth.d.ts
CHANGED
package/dist/auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { omit } from 'utilium';
|
|
|
3
3
|
import * as acl from './acl.js';
|
|
4
4
|
import { database as db, userFromId } from './database.js';
|
|
5
5
|
import { error, getToken, withError } from './requests.js';
|
|
6
|
+
import { audit } from './audit.js';
|
|
6
7
|
export async function getUser(id) {
|
|
7
8
|
return await db.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
|
|
8
9
|
}
|
|
@@ -22,6 +23,7 @@ export async function createSession(userId, elevated = false) {
|
|
|
22
23
|
created: new Date(),
|
|
23
24
|
};
|
|
24
25
|
await db.insertInto('sessions').values(session).execute();
|
|
26
|
+
await audit('new_session', userId, { id: session.id });
|
|
25
27
|
return session;
|
|
26
28
|
}
|
|
27
29
|
export async function getSessionAndUser(token) {
|
|
@@ -90,6 +92,8 @@ export async function checkAuthForUser(event, userId, sensitive = false) {
|
|
|
90
92
|
if (!token)
|
|
91
93
|
throw error(401, 'Missing token');
|
|
92
94
|
const session = await getSessionAndUser(token).catch(withError('Invalid or expired session', 401));
|
|
95
|
+
if (session.user.isSuspended)
|
|
96
|
+
error(403, 'User is suspended');
|
|
93
97
|
if (session.userId !== userId) {
|
|
94
98
|
if (!session.user?.isAdmin)
|
|
95
99
|
error(403, 'User ID mismatch');
|
|
@@ -125,14 +129,18 @@ export async function checkAuthForItem(event, itemType, itemId, permission) {
|
|
|
125
129
|
return result;
|
|
126
130
|
if (!session)
|
|
127
131
|
error(403, 'Access denied');
|
|
132
|
+
if (session.user.isSuspended)
|
|
133
|
+
error(403, 'User is suspended');
|
|
128
134
|
if (session.userId == item.userId)
|
|
129
135
|
return result;
|
|
130
136
|
result.fromACL = true;
|
|
131
137
|
if (!item.acl || !item.acl.length)
|
|
132
138
|
error(403, 'Access denied');
|
|
133
139
|
const [control] = item.acl;
|
|
134
|
-
if (control.userId !== session.userId)
|
|
135
|
-
|
|
140
|
+
if (control.userId !== session.userId) {
|
|
141
|
+
await audit('acl_id_mismatch', session.userId, { item: itemId });
|
|
142
|
+
error(500, 'Access control entry does not match session user');
|
|
143
|
+
}
|
|
136
144
|
if (control.permission >= permission)
|
|
137
145
|
return result;
|
|
138
146
|
error(403, 'Access denied');
|