@axium/server 0.8.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/lib → assets}/styles.css +7 -1
- 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 -31
- package/dist/auth.js +20 -34
- package/dist/cli.js +200 -54
- package/dist/config.d.ts +52 -480
- package/dist/config.js +89 -56
- package/dist/database.d.ts +3 -3
- package/dist/database.js +57 -24
- package/dist/io.d.ts +6 -4
- package/dist/io.js +26 -19
- package/dist/plugins.d.ts +5 -2
- package/dist/plugins.js +16 -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 +18 -10
- package/{web/routes → routes}/account/+page.svelte +115 -48
- package/svelte.config.js +36 -0
- package/web/hooks.server.ts +15 -4
- package/web/lib/ClipboardCopy.svelte +42 -0
- package/web/lib/Dialog.svelte +0 -1
- package/web/lib/FormDialog.svelte +9 -2
- package/web/lib/icons/Icon.svelte +3 -12
- 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 -340
- 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/routes → routes}/_axium/default/+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
package/dist/routes.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { RequestMethod } from '@axium/core/requests';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
3
3
|
import type { Component } from 'svelte';
|
|
4
4
|
import type z from 'zod/v4';
|
|
5
5
|
type _Params = Partial<Record<string, string>>;
|
|
6
|
-
|
|
6
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
7
|
+
export type EndpointHandlers<Params extends _Params = _Params> = Partial<Record<RequestMethod, (event: RequestEvent<Params>) => MaybePromise<object | Response>>>;
|
|
7
8
|
export type RouteParamOptions = z.ZodType;
|
|
8
9
|
export interface CommonRouteOptions<Params extends _Params = _Params> {
|
|
9
10
|
path: string;
|
|
@@ -15,6 +16,7 @@ export interface CommonRouteOptions<Params extends _Params = _Params> {
|
|
|
15
16
|
* A route with server-side handlers for different HTTP methods.
|
|
16
17
|
*/
|
|
17
18
|
export interface ServerRouteOptions<Params extends _Params = _Params> extends CommonRouteOptions<Params>, EndpointHandlers<Params> {
|
|
19
|
+
api?: boolean;
|
|
18
20
|
}
|
|
19
21
|
export interface WebRouteOptions extends CommonRouteOptions {
|
|
20
22
|
load?(event: RequestEvent): object | Promise<object>;
|
|
@@ -25,31 +27,28 @@ export type RouteOptions = ServerRouteOptions | WebRouteOptions;
|
|
|
25
27
|
export interface RouteCommon {
|
|
26
28
|
path: string;
|
|
27
29
|
params?: Record<string, RouteParamOptions>;
|
|
28
|
-
[kBuiltin]: boolean;
|
|
29
30
|
}
|
|
30
31
|
export interface ServerRoute extends RouteCommon, EndpointHandlers {
|
|
32
|
+
api: boolean;
|
|
31
33
|
server: true;
|
|
32
34
|
}
|
|
33
35
|
export interface WebRoute extends RouteCommon {
|
|
34
36
|
server: false;
|
|
35
|
-
load?(event:
|
|
36
|
-
page
|
|
37
|
+
load?(event: RequestEvent): object | Promise<object>;
|
|
38
|
+
page: Component;
|
|
37
39
|
}
|
|
38
40
|
export type Route = ServerRoute | WebRoute;
|
|
39
41
|
/**
|
|
40
42
|
* @internal
|
|
41
43
|
*/
|
|
42
44
|
export declare const routes: Map<string, Route>;
|
|
43
|
-
declare
|
|
44
|
-
export declare function addRoute(opt: RouteOptions, _routeMap?: Map<string, Route>): void;
|
|
45
|
+
export declare function addRoute(opt: RouteOptions): void;
|
|
45
46
|
/**
|
|
46
47
|
* Resolve a request URL into a route.
|
|
47
48
|
* This handles parsing of parameters in the URL.
|
|
48
49
|
*/
|
|
49
|
-
export declare function resolveRoute
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
export declare function _markDefaults(): void;
|
|
50
|
+
export declare function resolveRoute(event: {
|
|
51
|
+
url: URL;
|
|
52
|
+
params?: object;
|
|
53
|
+
}): Route | undefined;
|
|
55
54
|
export {};
|
package/dist/routes.js
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
|
+
import { apps } from './apps.js';
|
|
2
|
+
import config from './config.js';
|
|
3
|
+
import { output } from './io.js';
|
|
4
|
+
import { _unique } from './state.js';
|
|
1
5
|
/**
|
|
2
6
|
* @internal
|
|
3
7
|
*/
|
|
4
|
-
export const routes = new Map();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const route = { ...opt, server: !('page' in opt), [kBuiltin]: false };
|
|
8
|
+
export const routes = _unique('routes', new Map());
|
|
9
|
+
export function addRoute(opt) {
|
|
10
|
+
const route = { ...opt, server: !('page' in opt) };
|
|
8
11
|
if (!route.path.startsWith('/')) {
|
|
9
12
|
throw new Error(`Route path must start with a slash: ${route.path}`);
|
|
10
13
|
}
|
|
11
|
-
if (route.path.startsWith('/api/')
|
|
14
|
+
if (route.path.startsWith('/api/'))
|
|
15
|
+
route.api = true;
|
|
16
|
+
if (route.api && !route.server)
|
|
12
17
|
throw new Error(`API routes cannot have a client page: ${route.path}`);
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
routes.set(route.path, route);
|
|
19
|
+
output.debug('Added route: ' + route.path);
|
|
15
20
|
}
|
|
16
21
|
/**
|
|
17
22
|
* Resolve a request URL into a route.
|
|
18
23
|
* This handles parsing of parameters in the URL.
|
|
19
24
|
*/
|
|
20
|
-
export function resolveRoute(event
|
|
25
|
+
export function resolveRoute(event) {
|
|
21
26
|
const { pathname } = event.url;
|
|
22
|
-
if (
|
|
23
|
-
return
|
|
27
|
+
if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
|
|
28
|
+
return routes.get(pathname);
|
|
24
29
|
// Otherwise we must have a parameterized route
|
|
25
|
-
|
|
30
|
+
_routes: for (const route of routes.values()) {
|
|
26
31
|
const params = {};
|
|
27
32
|
// Split the path and route into parts, zipped together
|
|
28
33
|
const pathParts = pathname.split('/').filter(Boolean);
|
|
34
|
+
// Skips routes in disabled apps
|
|
35
|
+
if (apps.has(pathParts[0]) && config.apps.disabled.includes(pathParts[0]))
|
|
36
|
+
continue;
|
|
29
37
|
for (const routePart of route.path.split('/').filter(Boolean)) {
|
|
30
38
|
const pathPart = pathParts.shift();
|
|
31
39
|
if (!pathPart)
|
|
32
|
-
continue
|
|
40
|
+
continue _routes;
|
|
33
41
|
if (pathPart == routePart)
|
|
34
42
|
continue;
|
|
35
43
|
if (!routePart.startsWith(':'))
|
|
36
|
-
continue
|
|
44
|
+
continue _routes;
|
|
37
45
|
params[routePart.slice(1)] = pathPart;
|
|
38
46
|
}
|
|
39
47
|
// we didn't find a match, since an exact match would have been found already
|
|
@@ -43,12 +51,3 @@ export function resolveRoute(event, _routeMap = routes) {
|
|
|
43
51
|
return route;
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
|
-
/**
|
|
47
|
-
* This function marks all existing routes as built-in.
|
|
48
|
-
* @internal
|
|
49
|
-
*/
|
|
50
|
-
export function _markDefaults() {
|
|
51
|
-
for (const route of routes.values()) {
|
|
52
|
-
route[kBuiltin] = true;
|
|
53
|
-
}
|
|
54
|
-
}
|
package/dist/serve.d.ts
ADDED
package/dist/serve.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { createServer as createSecureServer } from 'node:https';
|
|
4
|
+
import config from './config.js';
|
|
5
|
+
const _handlerPath = '../build/handler.js';
|
|
6
|
+
export async function serve(opt) {
|
|
7
|
+
const { handler } = await import(_handlerPath);
|
|
8
|
+
if (!opt.secure && !config.web.secure)
|
|
9
|
+
return createServer(handler);
|
|
10
|
+
return createSecureServer({ key: readFileSync(opt.ssl_key || config.web.ssl_key), cert: readFileSync(opt.ssl_cert || config.web.ssl_cert) }, handler);
|
|
11
|
+
}
|
package/dist/state.d.ts
ADDED
package/dist/state.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
const sym = Symbol.for('Axium:state');
|
|
3
|
+
globalThis[sym] ||= Object.create({ _errored: false });
|
|
4
|
+
/**
|
|
5
|
+
* Prevent duplicate shared state.
|
|
6
|
+
*/
|
|
7
|
+
export function _unique(id, value) {
|
|
8
|
+
const state = globalThis[sym];
|
|
9
|
+
const _err = new Error();
|
|
10
|
+
Error.captureStackTrace(_err, _unique);
|
|
11
|
+
const stack = _err.stack.slice(6);
|
|
12
|
+
if (!(id in state)) {
|
|
13
|
+
state[id] = { value, stack };
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (!state._errored) {
|
|
17
|
+
console.error(styleText('red', 'Duplicate Axium server state! You might have multiple instances of the same module loaded.'));
|
|
18
|
+
state._errored = true;
|
|
19
|
+
}
|
|
20
|
+
console.warn(styleText('yellow', `Mitigating duplicate state! (${id})\n${stack}\nFrom original\n${state[id].stack}`));
|
|
21
|
+
return state[id].value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RequestEvent, ResolveOptions } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export declare function handle({ event, resolve, }: {
|
|
6
|
+
event: RequestEvent;
|
|
7
|
+
resolve: (event: RequestEvent, opts?: ResolveOptions) => Promise<Response>;
|
|
8
|
+
}): Promise<Response>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { error, json, redirect } from '@sveltejs/kit';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path/posix';
|
|
4
|
+
import { render } from 'svelte/server';
|
|
5
|
+
import z from 'zod/v4';
|
|
6
|
+
import { config } from './config.js';
|
|
7
|
+
import { resolveRoute } from './routes.js';
|
|
8
|
+
async function handleAPIRequest(event, route) {
|
|
9
|
+
const method = event.request.method;
|
|
10
|
+
const _warnings = [];
|
|
11
|
+
if (route.api && !event.request.headers.get('Accept')?.includes('application/json')) {
|
|
12
|
+
_warnings.push('Only application/json is supported');
|
|
13
|
+
event.request.headers.set('Accept', 'application/json');
|
|
14
|
+
}
|
|
15
|
+
for (const [key, type] of Object.entries(route.params || {})) {
|
|
16
|
+
if (!type)
|
|
17
|
+
continue;
|
|
18
|
+
try {
|
|
19
|
+
event.params[key] = type.parse(event.params[key]);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
error(400, `Invalid parameter: ${z.prettifyError(e)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (typeof route[method] != 'function')
|
|
26
|
+
error(405, `Method ${method} not allowed for ${route.path}`);
|
|
27
|
+
const result = await route[method](event);
|
|
28
|
+
if (result instanceof Response)
|
|
29
|
+
return result;
|
|
30
|
+
result._warnings ||= [];
|
|
31
|
+
result._warnings.push(..._warnings);
|
|
32
|
+
return json(result);
|
|
33
|
+
}
|
|
34
|
+
function handleError(e) {
|
|
35
|
+
if ('body' in e)
|
|
36
|
+
return json(e.body, { status: e.status });
|
|
37
|
+
console.error(e);
|
|
38
|
+
return json({ message: 'Internal Error' + (config.debug ? ': ' + e.message : '') }, { status: 500 });
|
|
39
|
+
}
|
|
40
|
+
const templatePath = join(import.meta.dirname, '../web/template.html');
|
|
41
|
+
const template = readFileSync(templatePath, 'utf-8');
|
|
42
|
+
function fillTemplate({ head, body }, env = {}, nonce = '') {
|
|
43
|
+
return (template
|
|
44
|
+
.replace('%sveltekit.head%', head)
|
|
45
|
+
.replace('%sveltekit.body%', body)
|
|
46
|
+
.replace(/%sveltekit\.assets%/g, config.web.assets)
|
|
47
|
+
// Unused for now.
|
|
48
|
+
.replace(/%sveltekit\.nonce%/g, nonce)
|
|
49
|
+
.replace(/%sveltekit\.env\.([^%]+)%/g, (_match, capture) => env[capture] ?? ''));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export async function handle({ event, resolve, }) {
|
|
55
|
+
const route = resolveRoute(event);
|
|
56
|
+
if (!route && event.url.pathname === '/')
|
|
57
|
+
redirect(303, '/_axium/default');
|
|
58
|
+
if (config.debug)
|
|
59
|
+
console.log(event.request.method.padEnd(7), route ? route.path : event.url.pathname);
|
|
60
|
+
if (!route)
|
|
61
|
+
return await resolve(event).catch(handleError);
|
|
62
|
+
if (route.server == true) {
|
|
63
|
+
if (route.api)
|
|
64
|
+
return await handleAPIRequest(event, route).catch(handleError);
|
|
65
|
+
const run = route[event.request.method];
|
|
66
|
+
if (typeof run !== 'function') {
|
|
67
|
+
error(405, `Method ${event.request.method} not allowed for ${route.path}`);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = await run(event);
|
|
71
|
+
if (result instanceof Response)
|
|
72
|
+
return result;
|
|
73
|
+
return json(result);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return handleError(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const data = await route.load?.(event);
|
|
80
|
+
const body = fillTemplate(render(route.page));
|
|
81
|
+
return new Response(body, {
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
84
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
85
|
+
Pragma: 'no-cache',
|
|
86
|
+
Expires: '0',
|
|
87
|
+
},
|
|
88
|
+
status: 200,
|
|
89
|
+
});
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -19,13 +19,17 @@
|
|
|
19
19
|
"types": "dist/index.d.ts",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./dist/index.js",
|
|
22
|
-
"./*": "./dist
|
|
23
|
-
"./web": "./web
|
|
24
|
-
"
|
|
22
|
+
"./*": "./dist/*.js",
|
|
23
|
+
"./web/*": "./web/*",
|
|
24
|
+
"./$routes": "./routes",
|
|
25
|
+
"./svelte.config.js": "./svelte.config.js"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
28
|
+
"assets",
|
|
27
29
|
"dist",
|
|
28
|
-
"
|
|
30
|
+
"routes",
|
|
31
|
+
"web",
|
|
32
|
+
"svelte.config.js"
|
|
29
33
|
],
|
|
30
34
|
"bin": {
|
|
31
35
|
"axium": "dist/cli.js"
|
|
@@ -33,18 +37,22 @@
|
|
|
33
37
|
"scripts": {
|
|
34
38
|
"build": "tsc"
|
|
35
39
|
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@axium/client": ">=0.0.2",
|
|
42
|
+
"@axium/core": ">=0.3.0",
|
|
43
|
+
"utilium": "^2.3.8",
|
|
44
|
+
"zod": "^3.25.61"
|
|
45
|
+
},
|
|
36
46
|
"dependencies": {
|
|
47
|
+
"@axium/server": "file:.",
|
|
37
48
|
"@simplewebauthn/server": "^13.1.1",
|
|
38
49
|
"@sveltejs/kit": "^2.20.2",
|
|
39
50
|
"@types/pg": "^8.11.11",
|
|
40
51
|
"commander": "^13.1.0",
|
|
41
|
-
"kysely": "^0.
|
|
52
|
+
"kysely": "^0.28.0",
|
|
42
53
|
"logzen": "^0.7.0",
|
|
43
54
|
"mime": "^4.0.7",
|
|
44
|
-
"pg": "^8.14.1"
|
|
45
|
-
"utilium": "^2.3.8",
|
|
46
|
-
"zod": "^3.25.61",
|
|
47
|
-
"@axium/core": ">=0.2.0"
|
|
55
|
+
"pg": "^8.14.1"
|
|
48
56
|
},
|
|
49
57
|
"devDependencies": {
|
|
50
58
|
"@sveltejs/adapter-node": "^5.2.12",
|
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import ClipboardCopy from '$lib/ClipboardCopy.svelte';
|
|
2
4
|
import FormDialog from '$lib/FormDialog.svelte';
|
|
3
5
|
import Icon from '$lib/icons/Icon.svelte';
|
|
4
6
|
import {
|
|
5
|
-
|
|
7
|
+
createPasskey,
|
|
6
8
|
deletePasskey,
|
|
9
|
+
deleteUser,
|
|
10
|
+
emailVerificationEnabled,
|
|
11
|
+
getCurrentSession,
|
|
7
12
|
getPasskeys,
|
|
13
|
+
getSessions,
|
|
14
|
+
logout,
|
|
15
|
+
logoutAll,
|
|
8
16
|
sendVerificationEmail,
|
|
9
17
|
updatePasskey,
|
|
10
18
|
updateUser,
|
|
11
|
-
createPasskey,
|
|
12
|
-
deleteUser,
|
|
13
19
|
} from '@axium/client/user';
|
|
14
|
-
import type { Passkey } from '@axium/core/api';
|
|
20
|
+
import type { Passkey, Session } from '@axium/core/api';
|
|
15
21
|
import { getUserImage, type User } from '@axium/core/user';
|
|
16
22
|
|
|
17
23
|
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
18
24
|
|
|
19
25
|
let verificationSent = $state(false);
|
|
26
|
+
let currentSession = $state<Session & { user: User }>();
|
|
20
27
|
let user = $state<User>();
|
|
28
|
+
let canVerify = $state(false);
|
|
29
|
+
let passkeys = $state<Passkey[]>([]);
|
|
30
|
+
let sessions = $state<Session[]>([]);
|
|
21
31
|
|
|
22
32
|
async function ready() {
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
currentSession = await getCurrentSession().catch(() => {
|
|
34
|
+
goto('/login?after=/account');
|
|
35
|
+
return null;
|
|
36
|
+
})!;
|
|
37
|
+
user = currentSession.user;
|
|
25
38
|
|
|
26
39
|
passkeys = await getPasskeys(user.id);
|
|
27
|
-
}
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
sessions = await getSessions(user.id);
|
|
42
|
+
|
|
43
|
+
canVerify = await emailVerificationEnabled(user.id);
|
|
44
|
+
}
|
|
30
45
|
|
|
31
46
|
async function _editUser(data) {
|
|
32
47
|
const result = await updateUser(user.id, data);
|
|
@@ -39,14 +54,8 @@
|
|
|
39
54
|
</svelte:head>
|
|
40
55
|
|
|
41
56
|
{#snippet action(name: string, i: string = 'pen')}
|
|
42
|
-
<button
|
|
43
|
-
|
|
44
|
-
style:cursor="pointer"
|
|
45
|
-
onclick={() => {
|
|
46
|
-
dialogs[name].showModal();
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
<Icon {i} />
|
|
57
|
+
<button style:display="contents" onclick={() => dialogs[name].showModal()}>
|
|
58
|
+
<Icon {i} --size="16px" />
|
|
50
59
|
</button>
|
|
51
60
|
{/snippet}
|
|
52
61
|
|
|
@@ -56,9 +65,10 @@
|
|
|
56
65
|
<p class="greeting">Welcome, {user.name}</p>
|
|
57
66
|
|
|
58
67
|
<div class="section main">
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
<
|
|
68
|
+
<h3>Personal Information</h3>
|
|
69
|
+
<div class="item info">
|
|
70
|
+
<p class="subtle">Name</p>
|
|
71
|
+
<p>{user.name}</p>
|
|
62
72
|
{@render action('edit_name')}
|
|
63
73
|
</div>
|
|
64
74
|
<FormDialog bind:dialog={dialogs.edit_name} submit={_editUser} submitText="Change">
|
|
@@ -67,20 +77,20 @@
|
|
|
67
77
|
<input name="name" type="text" value={user.name || ''} required />
|
|
68
78
|
</div>
|
|
69
79
|
</FormDialog>
|
|
70
|
-
<div class="item">
|
|
71
|
-
<
|
|
72
|
-
<
|
|
80
|
+
<div class="item info">
|
|
81
|
+
<p class="subtle">Email</p>
|
|
82
|
+
<p>
|
|
73
83
|
{user.email}
|
|
74
84
|
{#if user.emailVerified}
|
|
75
|
-
<dfn title="Email verified on {
|
|
85
|
+
<dfn title="Email verified on {user.emailVerified.toLocaleDateString()}">
|
|
76
86
|
<Icon i="regular/circle-check" />
|
|
77
87
|
</dfn>
|
|
78
|
-
{:else}
|
|
88
|
+
{:else if canVerify}
|
|
79
89
|
<button onclick={() => sendVerificationEmail(user.id).then(() => (verificationSent = true))}>
|
|
80
90
|
{verificationSent ? 'Verification email sent' : 'Verify'}
|
|
81
91
|
</button>
|
|
82
92
|
{/if}
|
|
83
|
-
</
|
|
93
|
+
</p>
|
|
84
94
|
{@render action('edit_email')}
|
|
85
95
|
</div>
|
|
86
96
|
<FormDialog bind:dialog={dialogs.edit_email} submit={_editUser} submitText="Change">
|
|
@@ -90,16 +100,20 @@
|
|
|
90
100
|
</div>
|
|
91
101
|
</FormDialog>
|
|
92
102
|
|
|
93
|
-
<div class="item">
|
|
94
|
-
<p class="subtle">User ID <dfn title="This is your UUID."><Icon i="regular/circle-info" /></dfn></p>
|
|
103
|
+
<div class="item info">
|
|
104
|
+
<p class="subtle">User ID <dfn title="This is your UUID. It can't be changed."><Icon i="regular/circle-info" /></dfn></p>
|
|
95
105
|
<p>{user.id}</p>
|
|
106
|
+
<ClipboardCopy value={user.id} --size="16px" />
|
|
96
107
|
</div>
|
|
97
108
|
<span>
|
|
98
109
|
<a class="signout" href="/logout"><button>Sign out</button></a>
|
|
99
|
-
<button style:cursor="pointer" onclick={() => dialogs.delete.showModal()}
|
|
100
|
-
|
|
110
|
+
<button style:cursor="pointer" onclick={() => dialogs.delete.showModal()} class="danger">Delete Account</button>
|
|
111
|
+
<FormDialog
|
|
112
|
+
bind:dialog={dialogs.delete}
|
|
113
|
+
submit={() => deleteUser(user.id).then(() => goto('/'))}
|
|
114
|
+
submitText="Delete Account"
|
|
115
|
+
submitDanger
|
|
101
116
|
>
|
|
102
|
-
<FormDialog bind:dialog={dialogs.delete} submit={() => deleteUser(user.id)} submitText="Delete Account" submitDanger>
|
|
103
117
|
<p>Are you sure you want to delete your account?<br />This action can't be undone.</p>
|
|
104
118
|
</FormDialog>
|
|
105
119
|
</span>
|
|
@@ -108,25 +122,25 @@
|
|
|
108
122
|
<div class="section main">
|
|
109
123
|
<h3>Passkeys</h3>
|
|
110
124
|
{#each passkeys as passkey}
|
|
111
|
-
<div class="passkey">
|
|
125
|
+
<div class="item passkey">
|
|
112
126
|
<dfn title={passkey.deviceType == 'multiDevice' ? 'Multiple devices' : 'Single device'}>
|
|
113
|
-
<Icon i={passkey.deviceType == 'multiDevice' ? 'laptop-mobile' : 'mobile'} />
|
|
127
|
+
<Icon i={passkey.deviceType == 'multiDevice' ? 'laptop-mobile' : 'mobile'} --size="16px" />
|
|
114
128
|
</dfn>
|
|
115
129
|
<dfn title="This passkey is {passkey.backedUp ? '' : 'not '}backed up">
|
|
116
|
-
<Icon i={passkey.backedUp ? 'circle-check' : 'circle-xmark'} />
|
|
130
|
+
<Icon i={passkey.backedUp ? 'circle-check' : 'circle-xmark'} --size="16px" />
|
|
117
131
|
</dfn>
|
|
118
|
-
<p>Created {new Date(passkey.createdAt).toLocaleString()}</p>
|
|
119
132
|
{#if passkey.name}
|
|
120
133
|
<p>{passkey.name}</p>
|
|
121
134
|
{:else}
|
|
122
135
|
<p class="subtle"><i>Unnamed</i></p>
|
|
123
136
|
{/if}
|
|
137
|
+
<p>Created {passkey.createdAt.toLocaleString()}</p>
|
|
124
138
|
{@render action('edit_passkey#' + passkey.id)}
|
|
125
139
|
{#if passkeys.length > 1}
|
|
126
140
|
{@render action('delete_passkey#' + passkey.id, 'trash')}
|
|
127
141
|
{:else}
|
|
128
142
|
<dfn title="You must have at least one passkey" class="disabled">
|
|
129
|
-
<Icon i="trash-slash" --fill="#888" />
|
|
143
|
+
<Icon i="trash-slash" --fill="#888" --size="16px" />
|
|
130
144
|
</dfn>
|
|
131
145
|
{/if}
|
|
132
146
|
</div>
|
|
@@ -153,12 +167,56 @@
|
|
|
153
167
|
<p>Are you sure you want to delete this passkey?<br />This action can't be undone.</p>
|
|
154
168
|
</FormDialog>
|
|
155
169
|
{/each}
|
|
156
|
-
<
|
|
170
|
+
<span>
|
|
171
|
+
<button onclick={() => createPasskey(user.id).then(passkeys.push.bind(passkeys))}><Icon i="plus" /> Create</button>
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="section main">
|
|
176
|
+
<h3>Sessions</h3>
|
|
177
|
+
{#each sessions as session}
|
|
178
|
+
<div class="item session">
|
|
179
|
+
<p>
|
|
180
|
+
{session.id.slice(0, 4)}...{session.id.slice(-4)}
|
|
181
|
+
{#if session.id == currentSession.id}
|
|
182
|
+
<span class="current">Current</span>
|
|
183
|
+
{/if}
|
|
184
|
+
{#if session.elevated}
|
|
185
|
+
<span class="elevated">Elevated</span>
|
|
186
|
+
{/if}
|
|
187
|
+
</p>
|
|
188
|
+
<p>Created {session.created.toLocaleString()}</p>
|
|
189
|
+
<p>Expires {session.expires.toLocaleString()}</p>
|
|
190
|
+
{@render action('logout#' + session.id, 'right-from-bracket')}
|
|
191
|
+
</div>
|
|
192
|
+
<FormDialog
|
|
193
|
+
bind:dialog={dialogs['logout#' + session.id]}
|
|
194
|
+
submit={() =>
|
|
195
|
+
logout(user.id, session.id).then(() => {
|
|
196
|
+
if (session.id == currentSession.id) goto('/');
|
|
197
|
+
else sessions.splice(sessions.indexOf(session), 1);
|
|
198
|
+
})}
|
|
199
|
+
submitText="Logout"
|
|
200
|
+
>
|
|
201
|
+
<p>Are you sure you want to log out this session?</p>
|
|
202
|
+
</FormDialog>
|
|
203
|
+
{/each}
|
|
204
|
+
<span>
|
|
205
|
+
<button onclick={() => dialogs.logout_all.showModal()} class="danger">Logout All</button>
|
|
206
|
+
</span>
|
|
207
|
+
<FormDialog
|
|
208
|
+
bind:dialog={dialogs['logout_all']}
|
|
209
|
+
submit={() => logoutAll(user.id).then(() => goto('/'))}
|
|
210
|
+
submitText="Logout All Sessions"
|
|
211
|
+
submitDanger
|
|
212
|
+
>
|
|
213
|
+
<p>Are you sure you want to log out all sessions?</p>
|
|
214
|
+
</FormDialog>
|
|
157
215
|
</div>
|
|
158
216
|
</div>
|
|
159
217
|
{:catch error}
|
|
160
218
|
<div class="error">
|
|
161
|
-
<h3>Failed to load
|
|
219
|
+
<h3>Failed to load account</h3>
|
|
162
220
|
<p>{'message' in error ? error.message : error}</p>
|
|
163
221
|
</div>
|
|
164
222
|
{/await}
|
|
@@ -191,12 +249,16 @@
|
|
|
191
249
|
|
|
192
250
|
.section .item {
|
|
193
251
|
display: grid;
|
|
194
|
-
grid-template-columns: 10em 1fr 2em;
|
|
195
252
|
align-items: center;
|
|
196
253
|
width: 100%;
|
|
197
254
|
gap: 1em;
|
|
198
255
|
text-wrap: nowrap;
|
|
256
|
+
border-top: 1px solid #8888;
|
|
199
257
|
padding-bottom: 1em;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.info {
|
|
261
|
+
grid-template-columns: 10em 1fr 2em;
|
|
200
262
|
|
|
201
263
|
> :first-child {
|
|
202
264
|
margin-left: 1em;
|
|
@@ -204,21 +266,26 @@
|
|
|
204
266
|
}
|
|
205
267
|
|
|
206
268
|
.passkey {
|
|
207
|
-
display: grid;
|
|
208
269
|
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
270
|
|
|
216
|
-
dfn {
|
|
271
|
+
dfn:not(.disabled) {
|
|
217
272
|
cursor: help;
|
|
218
273
|
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.session {
|
|
277
|
+
grid-template-columns: 1fr 1fr 1fr 1em;
|
|
278
|
+
|
|
279
|
+
.current {
|
|
280
|
+
border-radius: 2em;
|
|
281
|
+
padding: 0 0.5em;
|
|
282
|
+
background-color: #337;
|
|
283
|
+
}
|
|
219
284
|
|
|
220
|
-
|
|
221
|
-
|
|
285
|
+
.elevated {
|
|
286
|
+
border-radius: 2em;
|
|
287
|
+
padding: 0 0.5em;
|
|
288
|
+
background-color: #733;
|
|
222
289
|
}
|
|
223
290
|
}
|
|
224
291
|
</style>
|
package/svelte.config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import node from '@sveltejs/adapter-node';
|
|
2
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
+
import { join } from 'node:path/posix';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Paths relative to the directory of this file.
|
|
7
|
+
* This allows this file to be imported from other projects and still resolve to the correct paths.
|
|
8
|
+
*/
|
|
9
|
+
const fixed = p => join(import.meta.dirname, p);
|
|
10
|
+
|
|
11
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
12
|
+
export default {
|
|
13
|
+
compilerOptions: {
|
|
14
|
+
runes: true,
|
|
15
|
+
},
|
|
16
|
+
preprocess: vitePreprocess({ script: true }),
|
|
17
|
+
vitePlugin: {
|
|
18
|
+
exclude: '@axium/server/**',
|
|
19
|
+
},
|
|
20
|
+
kit: {
|
|
21
|
+
adapter: node(),
|
|
22
|
+
alias: {
|
|
23
|
+
$stores: fixed('web/stores'),
|
|
24
|
+
$lib: fixed('web/lib'),
|
|
25
|
+
},
|
|
26
|
+
files: {
|
|
27
|
+
routes: 'routes',
|
|
28
|
+
lib: fixed('web/lib'),
|
|
29
|
+
assets: fixed('assets'),
|
|
30
|
+
appTemplate: fixed('web/template.html'),
|
|
31
|
+
hooks: {
|
|
32
|
+
server: fixed('web/hooks.server.ts'),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|