@axium/server 0.0.2 → 0.0.3
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/auth.d.ts +16 -10
- package/dist/auth.js +49 -23
- package/dist/cli.js +46 -18
- package/dist/config.d.ts +48 -15
- package/dist/config.js +20 -8
- package/dist/database.d.ts +5 -1
- package/dist/database.js +29 -29
- package/dist/io.d.ts +16 -10
- package/dist/io.js +48 -40
- package/package.json +4 -3
- package/web/app.html +15 -0
- package/web/auth.ts +12 -0
- package/web/hooks.server.ts +1 -0
- package/web/lib/Icon.svelte +42 -0
- package/web/routes/+page.server.ts +14 -0
- package/web/routes/+page.svelte +99 -0
- package/web/routes/edit/email/+page.server.ts +35 -0
- package/web/routes/edit/email/+page.svelte +25 -0
- package/web/routes/edit/name/+page.server.ts +35 -0
- package/web/routes/edit/name/+page.svelte +25 -0
- package/web/routes/signup/+page.server.ts +33 -0
- package/web/routes/signup/+page.svelte +27 -0
- package/web/static/axium.css +67 -0
- package/web/tsconfig.json +7 -0
package/dist/auth.d.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import type { Adapter } from '@auth/core/adapters';
|
|
2
2
|
import type { Provider } from '@auth/core/providers';
|
|
3
3
|
import type { AuthConfig } from '@auth/core/types';
|
|
4
|
-
import { Registration } from '@axium/core/
|
|
4
|
+
import { Registration } from '@axium/core/schemas';
|
|
5
5
|
declare module '@auth/core/adapters' {
|
|
6
6
|
interface AdapterUser {
|
|
7
|
-
password: string;
|
|
8
|
-
salt: string;
|
|
7
|
+
password: string | null;
|
|
8
|
+
salt: string | null;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
export declare let adapter: Adapter;
|
|
12
|
-
export declare function createAdapter():
|
|
12
|
+
export declare function createAdapter(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Login using credentials
|
|
15
|
+
*/
|
|
13
16
|
export declare function register(credentials: Registration): Promise<{
|
|
14
|
-
user: import("@auth/core/adapters").AdapterUser
|
|
15
|
-
session: import("@auth/core/adapters").AdapterSession
|
|
17
|
+
user: import("@auth/core/adapters").AdapterUser;
|
|
18
|
+
session: import("@auth/core/adapters").AdapterSession;
|
|
16
19
|
}>;
|
|
17
|
-
export declare function authorize(credentials: Partial<Record<string, unknown>>): Promise<Omit<import("@auth/core/adapters").AdapterUser, "password" | "salt"> | null>;
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
21
|
+
* Authorize using credentials
|
|
20
22
|
*/
|
|
21
|
-
export declare function
|
|
22
|
-
|
|
23
|
+
export declare function authorize(credentials: Partial<Record<string, unknown>>): Promise<Omit<import("@auth/core/adapters").AdapterUser, "password" | "salt"> | null>;
|
|
24
|
+
type Providers = Exclude<Provider, (...args: any[]) => any>[];
|
|
25
|
+
export declare function getConfig(): AuthConfig & {
|
|
26
|
+
providers: Providers;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
package/dist/auth.js
CHANGED
|
@@ -2,17 +2,19 @@ import { CredentialsSignin } from '@auth/core/errors';
|
|
|
2
2
|
import Credentials from '@auth/core/providers/credentials';
|
|
3
3
|
import Passkey from '@auth/core/providers/passkey';
|
|
4
4
|
import { KyselyAdapter } from '@auth/kysely-adapter';
|
|
5
|
-
import { Login, Registration } from '@axium/core/
|
|
5
|
+
import { Login, Registration } from '@axium/core/schemas';
|
|
6
6
|
import { genSaltSync, hashSync } from 'bcryptjs';
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
8
|
import { omit } from 'utilium';
|
|
9
9
|
import * as config from './config.js';
|
|
10
10
|
import * as db from './database.js';
|
|
11
|
+
import { logger } from './io.js';
|
|
11
12
|
export let adapter;
|
|
12
13
|
export function createAdapter() {
|
|
14
|
+
if (adapter)
|
|
15
|
+
return;
|
|
13
16
|
const conn = db.connect();
|
|
14
17
|
adapter = Object.assign(KyselyAdapter(conn), {
|
|
15
|
-
_axium: { config },
|
|
16
18
|
async getAccount(providerAccountId, provider) {
|
|
17
19
|
const result = await conn.selectFrom('Account').selectAll().where('providerAccountId', '=', providerAccountId).where('provider', '=', provider).executeTakeFirst();
|
|
18
20
|
return result ?? null;
|
|
@@ -37,8 +39,10 @@ export function createAdapter() {
|
|
|
37
39
|
return authenticator;
|
|
38
40
|
},
|
|
39
41
|
});
|
|
40
|
-
return adapter;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Login using credentials
|
|
45
|
+
*/
|
|
42
46
|
export async function register(credentials) {
|
|
43
47
|
const { email, password, name } = Registration.parse(credentials);
|
|
44
48
|
const existing = await adapter.getUserByEmail?.(email);
|
|
@@ -48,41 +52,40 @@ export async function register(credentials) {
|
|
|
48
52
|
while (await adapter.getUser?.(id))
|
|
49
53
|
id = crypto.randomUUID();
|
|
50
54
|
const salt = genSaltSync(10);
|
|
51
|
-
const user = await adapter.createUser
|
|
55
|
+
const user = await adapter.createUser({
|
|
52
56
|
id,
|
|
53
57
|
name,
|
|
54
58
|
email,
|
|
55
59
|
emailVerified: null,
|
|
56
|
-
salt,
|
|
57
|
-
password: hashSync(password, salt),
|
|
60
|
+
salt: password ? salt : null,
|
|
61
|
+
password: password ? hashSync(password, salt) : null,
|
|
58
62
|
});
|
|
59
63
|
const expires = new Date();
|
|
60
64
|
expires.setMonth(expires.getMonth() + 1);
|
|
61
|
-
const session = await adapter.createSession
|
|
65
|
+
const session = await adapter.createSession({
|
|
62
66
|
sessionToken: randomBytes(64).toString('base64'),
|
|
63
67
|
userId: id,
|
|
64
68
|
expires,
|
|
65
69
|
});
|
|
66
70
|
return { user, session };
|
|
67
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Authorize using credentials
|
|
74
|
+
*/
|
|
68
75
|
export async function authorize(credentials) {
|
|
69
76
|
const { success, error, data } = Login.safeParse(credentials);
|
|
70
77
|
if (!success)
|
|
71
78
|
throw new CredentialsSignin(error);
|
|
72
79
|
const user = await adapter.getUserByEmail?.(data.email);
|
|
73
|
-
if (!user)
|
|
80
|
+
if (!user || !data.password || !user.salt)
|
|
74
81
|
return null;
|
|
75
82
|
if (user.password !== hashSync(data.password, user.salt))
|
|
76
83
|
return null;
|
|
77
84
|
return omit(user, 'password', 'salt');
|
|
78
85
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
export function getProviders() {
|
|
83
|
-
const providers = [];
|
|
84
|
-
if (config.auth.passkeys)
|
|
85
|
-
providers.push(Passkey);
|
|
86
|
+
export function getConfig() {
|
|
87
|
+
createAdapter();
|
|
88
|
+
const providers = [Passkey({})];
|
|
86
89
|
if (config.auth.credentials) {
|
|
87
90
|
providers.push(Credentials({
|
|
88
91
|
credentials: {
|
|
@@ -92,19 +95,42 @@ export function getProviders() {
|
|
|
92
95
|
authorize,
|
|
93
96
|
}));
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
export function getConfig() {
|
|
98
|
-
createAdapter();
|
|
98
|
+
const debug = config.auth.debug ?? config.debug;
|
|
99
99
|
return {
|
|
100
100
|
adapter,
|
|
101
|
-
providers
|
|
102
|
-
debug
|
|
101
|
+
providers,
|
|
102
|
+
debug,
|
|
103
103
|
experimental: { enableWebAuthn: true },
|
|
104
104
|
secret: config.auth.secret,
|
|
105
105
|
useSecureCookies: config.auth.secure_cookies,
|
|
106
|
-
session: {
|
|
107
|
-
|
|
106
|
+
session: { strategy: 'database' },
|
|
107
|
+
logger: {
|
|
108
|
+
error(error) {
|
|
109
|
+
logger.error('[auth] ' + error.message);
|
|
110
|
+
},
|
|
111
|
+
warn(code) {
|
|
112
|
+
switch (code) {
|
|
113
|
+
case 'experimental-webauthn':
|
|
114
|
+
case 'debug-enabled':
|
|
115
|
+
return;
|
|
116
|
+
case 'csrf-disabled':
|
|
117
|
+
logger.warn('CSRF protection is disabled.');
|
|
118
|
+
break;
|
|
119
|
+
case 'env-url-basepath-redundant':
|
|
120
|
+
case 'env-url-basepath-mismatch':
|
|
121
|
+
default:
|
|
122
|
+
logger.warn('[auth] ' + code);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
debug(message, metadata) {
|
|
126
|
+
debug && logger.debug('[auth]', message, metadata ? JSON.stringify(metadata, (k, v) => (k && JSON.stringify(v).length > 100 ? '...' : v)) : '');
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
callbacks: {
|
|
130
|
+
signIn({ user }) {
|
|
131
|
+
logger.info('[auth] signin', user.id ?? '', user.email ? `(${user.email})` : '');
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
108
134
|
},
|
|
109
135
|
};
|
|
110
136
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import chalk from 'chalk';
|
|
3
2
|
import { Option, program } from 'commander';
|
|
3
|
+
import { styleText } from 'node:util';
|
|
4
4
|
import { getByString, isJSON, pick, setByString } from 'utilium';
|
|
5
5
|
import $pkg from '../package.json' with { type: 'json' };
|
|
6
6
|
import * as config from './config.js';
|
|
7
7
|
import * as db from './database.js';
|
|
8
|
-
import {
|
|
8
|
+
import { exit, output } from './io.js';
|
|
9
9
|
program
|
|
10
10
|
.version($pkg.version)
|
|
11
11
|
.name('axium')
|
|
@@ -18,7 +18,7 @@ program.on('option:config', () => config.load(program.opts().config));
|
|
|
18
18
|
program.hook('preAction', function (_, action) {
|
|
19
19
|
config.loadDefaults();
|
|
20
20
|
const opt = action.optsWithGlobals();
|
|
21
|
-
opt.force &&
|
|
21
|
+
opt.force && output.warn('--force: Protections disabled.');
|
|
22
22
|
});
|
|
23
23
|
// Options shared by multiple (sub)commands
|
|
24
24
|
const opts = {
|
|
@@ -36,17 +36,17 @@ const axiumDB = program
|
|
|
36
36
|
.description('manage the database')
|
|
37
37
|
.option('-t, --timeout <ms>', 'how long to wait for commands to complete.', '1000')
|
|
38
38
|
.addOption(opts.host);
|
|
39
|
-
function db_output(state, message) {
|
|
39
|
+
function db_output(state, message = '') {
|
|
40
40
|
switch (state) {
|
|
41
41
|
case 'start':
|
|
42
42
|
process.stdout.write(message + '... ');
|
|
43
43
|
break;
|
|
44
44
|
case 'log':
|
|
45
45
|
case 'warn':
|
|
46
|
-
process.stdout.write(
|
|
46
|
+
process.stdout.write(styleText('yellow', message));
|
|
47
47
|
break;
|
|
48
48
|
case 'error':
|
|
49
|
-
process.stdout.write(
|
|
49
|
+
process.stdout.write(styleText('red', message));
|
|
50
50
|
break;
|
|
51
51
|
case 'done':
|
|
52
52
|
console.log('done.');
|
|
@@ -70,10 +70,18 @@ axiumDB
|
|
|
70
70
|
.command('status')
|
|
71
71
|
.alias('stats')
|
|
72
72
|
.description('check the status of the database')
|
|
73
|
-
.action(async () =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
.action(async () => {
|
|
74
|
+
try {
|
|
75
|
+
console.log(await db.statusText());
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
output.error('Unavailable');
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await db.database.destroy();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
77
85
|
axiumDB
|
|
78
86
|
.command('drop')
|
|
79
87
|
.description('drop the database')
|
|
@@ -84,10 +92,27 @@ axiumDB
|
|
|
84
92
|
for (const key of ['users', 'accounts', 'sessions']) {
|
|
85
93
|
if (stats[key] == 0)
|
|
86
94
|
continue;
|
|
87
|
-
|
|
95
|
+
output.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
await db.uninstall({ ...opt, output: db_output }).catch(exit);
|
|
99
|
+
await db.database.destroy();
|
|
100
|
+
});
|
|
101
|
+
axiumDB
|
|
102
|
+
.command('wipe')
|
|
103
|
+
.description('wipe the database')
|
|
104
|
+
.addOption(opts.force)
|
|
105
|
+
.action(async (opt) => {
|
|
106
|
+
const stats = await db.status().catch(exit);
|
|
107
|
+
if (!opt.force)
|
|
108
|
+
for (const key of ['users', 'accounts', 'sessions']) {
|
|
109
|
+
if (stats[key] == 0)
|
|
110
|
+
continue;
|
|
111
|
+
output.warn(`Database has existing ${key}. Use --force if you really want to wipe the database.`);
|
|
88
112
|
process.exit(2);
|
|
89
113
|
}
|
|
90
|
-
await db.
|
|
114
|
+
await db.wipe({ ...opt, output: db_output }).catch(exit);
|
|
115
|
+
await db.database.destroy();
|
|
91
116
|
});
|
|
92
117
|
const axiumConfig = program
|
|
93
118
|
.command('config')
|
|
@@ -139,14 +164,17 @@ program
|
|
|
139
164
|
.addOption(opts.host)
|
|
140
165
|
.action(async () => {
|
|
141
166
|
console.log('Axium Server v' + program.version());
|
|
142
|
-
console.log('Debug mode:', config.debug ?
|
|
167
|
+
console.log('Debug mode:', config.debug ? styleText('yellow', 'enabled') : 'disabled');
|
|
143
168
|
console.log('Loaded config files:', config.files.keys().toArray().join(', '));
|
|
144
169
|
process.stdout.write('Database: ');
|
|
145
|
-
|
|
146
|
-
.statusText()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
try {
|
|
171
|
+
console.log(await db.statusText());
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
output.error('Unavailable');
|
|
175
|
+
}
|
|
176
|
+
await db.database.destroy();
|
|
177
|
+
console.log('Credentials authentication:', config.auth.credentials ? styleText('yellow', 'enabled') : 'disabled');
|
|
150
178
|
});
|
|
151
179
|
program
|
|
152
180
|
.command('init')
|
package/dist/config.d.ts
CHANGED
|
@@ -24,43 +24,47 @@ export declare const db: Database;
|
|
|
24
24
|
export declare const Auth: z.ZodObject<{
|
|
25
25
|
credentials: z.ZodBoolean;
|
|
26
26
|
debug: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
-
passkeys: z.ZodBoolean;
|
|
28
27
|
secret: z.ZodString;
|
|
29
28
|
secure_cookies: z.ZodBoolean;
|
|
30
29
|
}, "strip", z.ZodTypeAny, {
|
|
31
30
|
credentials: boolean;
|
|
32
|
-
passkeys: boolean;
|
|
33
31
|
secret: string;
|
|
34
32
|
secure_cookies: boolean;
|
|
35
33
|
debug?: boolean | undefined;
|
|
36
34
|
}, {
|
|
37
35
|
credentials: boolean;
|
|
38
|
-
passkeys: boolean;
|
|
39
36
|
secret: string;
|
|
40
37
|
secure_cookies: boolean;
|
|
41
38
|
debug?: boolean | undefined;
|
|
42
39
|
}>;
|
|
43
40
|
export type Auth = z.infer<typeof Auth>;
|
|
44
41
|
export declare const auth: Auth;
|
|
45
|
-
export declare const
|
|
46
|
-
|
|
42
|
+
export declare const Log: z.ZodObject<{
|
|
43
|
+
level: z.ZodEnum<["error", "warn", "notice", "info", "debug"]>;
|
|
44
|
+
console: z.ZodBoolean;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
level: "debug" | "error" | "warn" | "notice" | "info";
|
|
47
|
+
console: boolean;
|
|
48
|
+
}, {
|
|
49
|
+
level: "debug" | "error" | "warn" | "notice" | "info";
|
|
50
|
+
console: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
export type Log = z.infer<typeof Log>;
|
|
53
|
+
export declare const log: Log;
|
|
47
54
|
export declare const Config: z.ZodObject<{
|
|
48
55
|
auth: z.ZodObject<{
|
|
49
56
|
credentials: z.ZodOptional<z.ZodBoolean>;
|
|
50
57
|
debug: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
51
|
-
passkeys: z.ZodOptional<z.ZodBoolean>;
|
|
52
58
|
secret: z.ZodOptional<z.ZodString>;
|
|
53
59
|
secure_cookies: z.ZodOptional<z.ZodBoolean>;
|
|
54
60
|
}, "strip", z.ZodTypeAny, {
|
|
55
61
|
credentials?: boolean | undefined;
|
|
56
62
|
debug?: boolean | undefined;
|
|
57
|
-
passkeys?: boolean | undefined;
|
|
58
63
|
secret?: string | undefined;
|
|
59
64
|
secure_cookies?: boolean | undefined;
|
|
60
65
|
}, {
|
|
61
66
|
credentials?: boolean | undefined;
|
|
62
67
|
debug?: boolean | undefined;
|
|
63
|
-
passkeys?: boolean | undefined;
|
|
64
68
|
secret?: string | undefined;
|
|
65
69
|
secure_cookies?: boolean | undefined;
|
|
66
70
|
}>;
|
|
@@ -84,12 +88,21 @@ export declare const Config: z.ZodObject<{
|
|
|
84
88
|
user?: string | undefined;
|
|
85
89
|
database?: string | undefined;
|
|
86
90
|
}>;
|
|
91
|
+
log: z.ZodObject<{
|
|
92
|
+
level: z.ZodOptional<z.ZodEnum<["error", "warn", "notice", "info", "debug"]>>;
|
|
93
|
+
console: z.ZodOptional<z.ZodBoolean>;
|
|
94
|
+
}, "strip", z.ZodTypeAny, {
|
|
95
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
96
|
+
console?: boolean | undefined;
|
|
97
|
+
}, {
|
|
98
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
99
|
+
console?: boolean | undefined;
|
|
100
|
+
}>;
|
|
87
101
|
}, "strip", z.ZodTypeAny, {
|
|
88
102
|
debug: boolean;
|
|
89
103
|
auth: {
|
|
90
104
|
credentials?: boolean | undefined;
|
|
91
105
|
debug?: boolean | undefined;
|
|
92
|
-
passkeys?: boolean | undefined;
|
|
93
106
|
secret?: string | undefined;
|
|
94
107
|
secure_cookies?: boolean | undefined;
|
|
95
108
|
};
|
|
@@ -100,12 +113,15 @@ export declare const Config: z.ZodObject<{
|
|
|
100
113
|
user?: string | undefined;
|
|
101
114
|
database?: string | undefined;
|
|
102
115
|
};
|
|
116
|
+
log: {
|
|
117
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
118
|
+
console?: boolean | undefined;
|
|
119
|
+
};
|
|
103
120
|
}, {
|
|
104
121
|
debug: boolean;
|
|
105
122
|
auth: {
|
|
106
123
|
credentials?: boolean | undefined;
|
|
107
124
|
debug?: boolean | undefined;
|
|
108
|
-
passkeys?: boolean | undefined;
|
|
109
125
|
secret?: string | undefined;
|
|
110
126
|
secure_cookies?: boolean | undefined;
|
|
111
127
|
};
|
|
@@ -116,24 +132,25 @@ export declare const Config: z.ZodObject<{
|
|
|
116
132
|
user?: string | undefined;
|
|
117
133
|
database?: string | undefined;
|
|
118
134
|
};
|
|
135
|
+
log: {
|
|
136
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
137
|
+
console?: boolean | undefined;
|
|
138
|
+
};
|
|
119
139
|
}>;
|
|
120
140
|
export declare const File: z.ZodObject<z.objectUtil.extendShape<{
|
|
121
141
|
auth: z.ZodOptional<z.ZodObject<{
|
|
122
142
|
credentials: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
123
143
|
debug: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodBoolean>>>;
|
|
124
|
-
passkeys: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
125
144
|
secret: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
126
145
|
secure_cookies: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
127
146
|
}, "strip", z.ZodTypeAny, {
|
|
128
147
|
credentials?: boolean | undefined;
|
|
129
148
|
debug?: boolean | undefined;
|
|
130
|
-
passkeys?: boolean | undefined;
|
|
131
149
|
secret?: string | undefined;
|
|
132
150
|
secure_cookies?: boolean | undefined;
|
|
133
151
|
}, {
|
|
134
152
|
credentials?: boolean | undefined;
|
|
135
153
|
debug?: boolean | undefined;
|
|
136
|
-
passkeys?: boolean | undefined;
|
|
137
154
|
secret?: string | undefined;
|
|
138
155
|
secure_cookies?: boolean | undefined;
|
|
139
156
|
}>>;
|
|
@@ -157,6 +174,16 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
|
|
|
157
174
|
user?: string | undefined;
|
|
158
175
|
database?: string | undefined;
|
|
159
176
|
}>>;
|
|
177
|
+
log: z.ZodOptional<z.ZodObject<{
|
|
178
|
+
level: z.ZodOptional<z.ZodOptional<z.ZodEnum<["error", "warn", "notice", "info", "debug"]>>>;
|
|
179
|
+
console: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
180
|
+
}, "strip", z.ZodTypeAny, {
|
|
181
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
182
|
+
console?: boolean | undefined;
|
|
183
|
+
}, {
|
|
184
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
185
|
+
console?: boolean | undefined;
|
|
186
|
+
}>>;
|
|
160
187
|
}, {
|
|
161
188
|
include: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
162
189
|
}>, "strip", z.ZodTypeAny, {
|
|
@@ -164,7 +191,6 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
|
|
|
164
191
|
auth?: {
|
|
165
192
|
credentials?: boolean | undefined;
|
|
166
193
|
debug?: boolean | undefined;
|
|
167
|
-
passkeys?: boolean | undefined;
|
|
168
194
|
secret?: string | undefined;
|
|
169
195
|
secure_cookies?: boolean | undefined;
|
|
170
196
|
} | undefined;
|
|
@@ -175,13 +201,16 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
|
|
|
175
201
|
user?: string | undefined;
|
|
176
202
|
database?: string | undefined;
|
|
177
203
|
} | undefined;
|
|
204
|
+
log?: {
|
|
205
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
206
|
+
console?: boolean | undefined;
|
|
207
|
+
} | undefined;
|
|
178
208
|
include?: string[] | undefined;
|
|
179
209
|
}, {
|
|
180
210
|
debug?: boolean | undefined;
|
|
181
211
|
auth?: {
|
|
182
212
|
credentials?: boolean | undefined;
|
|
183
213
|
debug?: boolean | undefined;
|
|
184
|
-
passkeys?: boolean | undefined;
|
|
185
214
|
secret?: string | undefined;
|
|
186
215
|
secure_cookies?: boolean | undefined;
|
|
187
216
|
} | undefined;
|
|
@@ -192,6 +221,10 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
|
|
|
192
221
|
user?: string | undefined;
|
|
193
222
|
database?: string | undefined;
|
|
194
223
|
} | undefined;
|
|
224
|
+
log?: {
|
|
225
|
+
level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
|
|
226
|
+
console?: boolean | undefined;
|
|
227
|
+
} | undefined;
|
|
195
228
|
include?: string[] | undefined;
|
|
196
229
|
}>;
|
|
197
230
|
export type File = z.infer<typeof File>;
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
import
|
|
2
|
+
import { levelText } from 'logzen';
|
|
3
3
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path/posix';
|
|
5
5
|
import { assignWithDefaults } from 'utilium';
|
|
6
6
|
import * as z from 'zod';
|
|
7
|
-
import { findDir } from './io.js';
|
|
7
|
+
import { findDir, logger, output } from './io.js';
|
|
8
8
|
export const Database = z.object({
|
|
9
9
|
host: z.string(),
|
|
10
10
|
port: z.number(),
|
|
@@ -22,22 +22,29 @@ export const db = {
|
|
|
22
22
|
export const Auth = z.object({
|
|
23
23
|
credentials: z.boolean(),
|
|
24
24
|
debug: z.boolean().optional(),
|
|
25
|
-
passkeys: z.boolean(),
|
|
26
25
|
secret: z.string(),
|
|
27
26
|
secure_cookies: z.boolean(),
|
|
28
27
|
});
|
|
29
28
|
export const auth = {
|
|
30
|
-
credentials:
|
|
31
|
-
passkeys: true,
|
|
29
|
+
credentials: false,
|
|
32
30
|
secret: '',
|
|
33
31
|
secure_cookies: false,
|
|
34
32
|
};
|
|
35
|
-
export const
|
|
33
|
+
export const Log = z.object({
|
|
34
|
+
level: z.enum(levelText),
|
|
35
|
+
console: z.boolean(),
|
|
36
|
+
});
|
|
37
|
+
export const log = {
|
|
38
|
+
level: 'info',
|
|
39
|
+
console: true,
|
|
40
|
+
};
|
|
36
41
|
export const Config = z.object({
|
|
37
42
|
auth: Auth.partial(),
|
|
38
43
|
debug: z.boolean(),
|
|
39
44
|
db: Database.partial(),
|
|
45
|
+
log: Log.partial(),
|
|
40
46
|
});
|
|
47
|
+
// config from file
|
|
41
48
|
export const File = Config.deepPartial().extend({
|
|
42
49
|
include: z.array(z.string()).optional(),
|
|
43
50
|
});
|
|
@@ -47,6 +54,7 @@ export function get() {
|
|
|
47
54
|
auth,
|
|
48
55
|
db,
|
|
49
56
|
debug,
|
|
57
|
+
log,
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
60
|
/**
|
|
@@ -56,6 +64,10 @@ export function set(config) {
|
|
|
56
64
|
assignWithDefaults(auth, config.auth ?? {});
|
|
57
65
|
debug = config.debug ?? debug;
|
|
58
66
|
assignWithDefaults(db, config.db ?? {});
|
|
67
|
+
assignWithDefaults(log, config.log ?? {});
|
|
68
|
+
logger.detach(output);
|
|
69
|
+
if (log.console)
|
|
70
|
+
logger.attach(output, { output: log.level });
|
|
59
71
|
}
|
|
60
72
|
export const files = new Map();
|
|
61
73
|
/**
|
|
@@ -69,7 +81,7 @@ export function load(path, options = {}) {
|
|
|
69
81
|
catch (e) {
|
|
70
82
|
if (!options.optional)
|
|
71
83
|
throw e;
|
|
72
|
-
|
|
84
|
+
logger.debug(`Skipping config at ${path} (${e.message})`);
|
|
73
85
|
return;
|
|
74
86
|
}
|
|
75
87
|
const config = options.strict ? File.parse(json) : json;
|
|
@@ -95,7 +107,7 @@ export function saveTo(path, changed) {
|
|
|
95
107
|
set(changed);
|
|
96
108
|
const config = files.get(path) ?? {};
|
|
97
109
|
Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
|
|
98
|
-
|
|
110
|
+
logger.debug(`Wrote config to ${path}`);
|
|
99
111
|
writeFileSync(path, JSON.stringify(config));
|
|
100
112
|
}
|
|
101
113
|
/**
|
package/dist/database.d.ts
CHANGED
|
@@ -73,4 +73,8 @@ export declare function init(opt: InitOptions): Promise<config.Database>;
|
|
|
73
73
|
/**
|
|
74
74
|
* Completely remove Axium from the database.
|
|
75
75
|
*/
|
|
76
|
-
export declare function
|
|
76
|
+
export declare function uninstall(opt: OpOptions): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Removes all data from tables.
|
|
79
|
+
*/
|
|
80
|
+
export declare function wipe(opt: OpOptions): Promise<void>;
|
package/dist/database.js
CHANGED
|
@@ -55,7 +55,7 @@ import { exec } from 'node:child_process';
|
|
|
55
55
|
import { randomBytes } from 'node:crypto';
|
|
56
56
|
import pg from 'pg';
|
|
57
57
|
import * as config from './config.js';
|
|
58
|
-
import {
|
|
58
|
+
import { logger } from './io.js';
|
|
59
59
|
export let database;
|
|
60
60
|
export function connect() {
|
|
61
61
|
if (database)
|
|
@@ -71,24 +71,12 @@ export function connect() {
|
|
|
71
71
|
return database;
|
|
72
72
|
}
|
|
73
73
|
export async function status() {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
sessions: (await db.selectFrom('Session').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
catch (e_1) {
|
|
84
|
-
env_1.error = e_1;
|
|
85
|
-
env_1.hasError = true;
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
const result_1 = __disposeResources(env_1);
|
|
89
|
-
if (result_1)
|
|
90
|
-
await result_1;
|
|
91
|
-
}
|
|
74
|
+
const db = connect();
|
|
75
|
+
return {
|
|
76
|
+
users: (await db.selectFrom('User').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
|
|
77
|
+
accounts: (await db.selectFrom('Account').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
|
|
78
|
+
sessions: (await db.selectFrom('Session').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
|
|
79
|
+
};
|
|
92
80
|
}
|
|
93
81
|
export async function statusText() {
|
|
94
82
|
try {
|
|
@@ -141,12 +129,12 @@ function shouldRecreate(opt) {
|
|
|
141
129
|
throw 2;
|
|
142
130
|
}
|
|
143
131
|
export async function init(opt) {
|
|
144
|
-
const
|
|
132
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
145
133
|
try {
|
|
146
134
|
_fixOutput(opt);
|
|
147
135
|
if (!config.db.password) {
|
|
148
136
|
config.save({ db: { password: randomBytes(32).toString('base64') } }, true);
|
|
149
|
-
|
|
137
|
+
logger.debug('Generated password and wrote to global config');
|
|
150
138
|
}
|
|
151
139
|
const _sql = (command, message) => execSQL(opt, command, message);
|
|
152
140
|
await _sql('CREATE DATABASE axium', 'Creating database').catch(async (error) => {
|
|
@@ -171,7 +159,7 @@ export async function init(opt) {
|
|
|
171
159
|
await _sql('GRANT ALL PRIVILEGES ON SCHEMA public TO axium', 'Granting schema privileges');
|
|
172
160
|
await _sql('ALTER DATABASE axium OWNER TO axium', 'Setting database owner');
|
|
173
161
|
await _sql('SELECT pg_reload_conf()', 'Reloading configuration');
|
|
174
|
-
const db = __addDisposableResource(
|
|
162
|
+
const db = __addDisposableResource(env_1, connect(), true);
|
|
175
163
|
const relationExists = (table) => (error) => {
|
|
176
164
|
error = typeof error == 'object' && 'message' in error ? error.message : error;
|
|
177
165
|
if (error == `relation "${table}" already exists`)
|
|
@@ -254,22 +242,34 @@ export async function init(opt) {
|
|
|
254
242
|
opt.output('done');
|
|
255
243
|
return config.db;
|
|
256
244
|
}
|
|
257
|
-
catch (
|
|
258
|
-
|
|
259
|
-
|
|
245
|
+
catch (e_1) {
|
|
246
|
+
env_1.error = e_1;
|
|
247
|
+
env_1.hasError = true;
|
|
260
248
|
}
|
|
261
249
|
finally {
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
await
|
|
250
|
+
const result_1 = __disposeResources(env_1);
|
|
251
|
+
if (result_1)
|
|
252
|
+
await result_1;
|
|
265
253
|
}
|
|
266
254
|
}
|
|
267
255
|
/**
|
|
268
256
|
* Completely remove Axium from the database.
|
|
269
257
|
*/
|
|
270
|
-
export async function
|
|
258
|
+
export async function uninstall(opt) {
|
|
271
259
|
_fixOutput(opt);
|
|
272
260
|
await execSQL(opt, 'DROP DATABASE axium', 'Dropping database');
|
|
273
261
|
await execSQL(opt, 'REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
274
262
|
await execSQL(opt, 'DROP USER axium', 'Dropping user');
|
|
275
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Removes all data from tables.
|
|
266
|
+
*/
|
|
267
|
+
export async function wipe(opt) {
|
|
268
|
+
_fixOutput(opt);
|
|
269
|
+
const db = connect();
|
|
270
|
+
for (const table of ['User', 'Account', 'Session', 'VerificationToken', 'Authenticator']) {
|
|
271
|
+
opt.output('start', `Removing data from ${table}`);
|
|
272
|
+
await db.deleteFrom(table).execute();
|
|
273
|
+
opt.output('done');
|
|
274
|
+
}
|
|
275
|
+
}
|
package/dist/io.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function report<T>(promise: Promise<T>, message: string, success?: string): Promise<T>;
|
|
3
|
-
export declare function err(message: string | Error): void;
|
|
4
|
-
/** Yet another convenience function */
|
|
5
|
-
export declare function exit(message: string | Error, code?: number): never;
|
|
6
|
-
export declare function verbose(...message: any[]): void;
|
|
1
|
+
import { Logger } from 'logzen';
|
|
7
2
|
/**
|
|
8
3
|
* Find the Axium directory.
|
|
9
4
|
* This directory includes things like config files, secrets, etc.
|
|
10
5
|
*/
|
|
11
6
|
export declare function findDir(global: boolean): string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
export declare const logger: Logger;
|
|
8
|
+
export declare const output: {
|
|
9
|
+
constructor: {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
error(message: string): void;
|
|
13
|
+
warn(message: string): void;
|
|
14
|
+
info(message: string): void;
|
|
15
|
+
log(message: string): void;
|
|
16
|
+
debug(message: string): void;
|
|
17
|
+
};
|
|
18
|
+
/** Yet another convenience function */
|
|
19
|
+
export declare function exit(message: string | Error, code?: number): never;
|
|
20
|
+
/** Convenience function for `example... [done. / error]` */
|
|
21
|
+
export declare function report<T>(promise: Promise<T>, message: string, success?: string): Promise<T>;
|
package/dist/io.js
CHANGED
|
@@ -1,35 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { Logger } from 'logzen';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path/posix';
|
|
5
|
-
import {
|
|
6
|
-
/** Convenience function for `example... [Done. / error]` */
|
|
7
|
-
export async function report(promise, message, success = 'done.') {
|
|
8
|
-
process.stdout.write(message + '... ');
|
|
9
|
-
try {
|
|
10
|
-
const result = await promise;
|
|
11
|
-
console.log(success);
|
|
12
|
-
return result;
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
throw typeof error == 'object' && 'message' in error ? error.message : error;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export function err(message) {
|
|
19
|
-
if (message instanceof Error)
|
|
20
|
-
message = message.message;
|
|
21
|
-
console.error(message.startsWith('\x1b') ? message : chalk.red(message));
|
|
22
|
-
}
|
|
23
|
-
/** Yet another convenience function */
|
|
24
|
-
export function exit(message, code = 1) {
|
|
25
|
-
err(message);
|
|
26
|
-
process.exit(code);
|
|
27
|
-
}
|
|
28
|
-
export function verbose(...message) {
|
|
29
|
-
if (!debug)
|
|
30
|
-
return;
|
|
31
|
-
console.debug(chalk.gray(message));
|
|
32
|
-
}
|
|
5
|
+
import { styleText } from 'node:util';
|
|
33
6
|
/**
|
|
34
7
|
* Find the Axium directory.
|
|
35
8
|
* This directory includes things like config files, secrets, etc.
|
|
@@ -37,19 +10,54 @@ export function verbose(...message) {
|
|
|
37
10
|
export function findDir(global) {
|
|
38
11
|
if (process.env.AXIUM_DIR)
|
|
39
12
|
return process.env.AXIUM_DIR;
|
|
40
|
-
if (process.getuid?.() === 0)
|
|
13
|
+
if (global && process.getuid?.() === 0)
|
|
41
14
|
return '/etc/axium';
|
|
42
15
|
if (global)
|
|
43
16
|
return join(homedir(), '.axium');
|
|
44
17
|
return '.axium';
|
|
45
18
|
}
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*/
|
|
49
|
-
export function checkDir(path) {
|
|
50
|
-
if (existsSync(path))
|
|
51
|
-
return;
|
|
52
|
-
mkdirSync(path, { recursive: true });
|
|
53
|
-
}
|
|
54
19
|
if (process.getuid?.() === 0)
|
|
55
|
-
|
|
20
|
+
mkdirSync('/etc/axium', { recursive: true });
|
|
21
|
+
mkdirSync(findDir(false), { recursive: true });
|
|
22
|
+
export const logger = new Logger({
|
|
23
|
+
hideWarningStack: true,
|
|
24
|
+
noGlobalConsole: true,
|
|
25
|
+
});
|
|
26
|
+
export const output = {
|
|
27
|
+
constructor: { name: 'Console' },
|
|
28
|
+
error(message) {
|
|
29
|
+
console.error(message.startsWith('\x1b') ? message : styleText('red', message));
|
|
30
|
+
},
|
|
31
|
+
warn(message) {
|
|
32
|
+
console.warn(message.startsWith('\x1b') ? message : styleText('yellow', message));
|
|
33
|
+
},
|
|
34
|
+
info(message) {
|
|
35
|
+
console.info(message.startsWith('\x1b') ? message : styleText('blue', message));
|
|
36
|
+
},
|
|
37
|
+
log(message) {
|
|
38
|
+
console.log(message);
|
|
39
|
+
},
|
|
40
|
+
debug(message) {
|
|
41
|
+
console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
logger.attach(output);
|
|
45
|
+
/** Yet another convenience function */
|
|
46
|
+
export function exit(message, code = 1) {
|
|
47
|
+
if (message instanceof Error)
|
|
48
|
+
message = message.message;
|
|
49
|
+
output.error(message);
|
|
50
|
+
process.exit(code);
|
|
51
|
+
}
|
|
52
|
+
/** Convenience function for `example... [done. / error]` */
|
|
53
|
+
export async function report(promise, message, success = 'done.') {
|
|
54
|
+
process.stdout.write(message + '... ');
|
|
55
|
+
try {
|
|
56
|
+
const result = await promise;
|
|
57
|
+
console.log(success);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw typeof error == 'object' && 'message' in error ? error.message : error;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"types": "dist/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"web"
|
|
22
23
|
],
|
|
23
24
|
"bin": {
|
|
24
25
|
"axium": "dist/cli.js"
|
|
@@ -34,9 +35,9 @@
|
|
|
34
35
|
"@auth/kysely-adapter": "^1.8.0",
|
|
35
36
|
"@types/pg": "^8.11.11",
|
|
36
37
|
"bcryptjs": "^3.0.2",
|
|
37
|
-
"chalk": "^5.4.1",
|
|
38
38
|
"commander": "^13.1.0",
|
|
39
39
|
"kysely": "^0.27.5",
|
|
40
|
+
"logzen": "^0.6.2",
|
|
40
41
|
"pg": "^8.14.1",
|
|
41
42
|
"utilium": "^2.2.3"
|
|
42
43
|
},
|
package/web/app.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="color-scheme" content="dark light" />
|
|
8
|
+
<link rel="stylesheet" href="/axium.css" />
|
|
9
|
+
%sveltekit.head%
|
|
10
|
+
</head>
|
|
11
|
+
|
|
12
|
+
<body>
|
|
13
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
package/web/auth.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SvelteKitAuth } from '@auth/sveltekit';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path/posix';
|
|
4
|
+
import { getConfig } from '../src/auth.js';
|
|
5
|
+
import { loadDefaults as loadDefaultConfigs } from '../src/config.js';
|
|
6
|
+
import { findDir, logger } from '../src/io.js';
|
|
7
|
+
import { allLogLevels } from 'logzen';
|
|
8
|
+
|
|
9
|
+
logger.attach(createWriteStream(join(findDir(false), 'server.log')), { output: allLogLevels });
|
|
10
|
+
loadDefaultConfigs();
|
|
11
|
+
|
|
12
|
+
export const { handle, signIn, signOut } = SvelteKitAuth(getConfig());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handle } from './auth.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
const { id, style = 'solid' } = $props();
|
|
5
|
+
|
|
6
|
+
const href = `https://site-assets.fontawesome.com/releases/v6.7.2/svgs/${style}/${id}.svg`;
|
|
7
|
+
|
|
8
|
+
let content = $state('');
|
|
9
|
+
|
|
10
|
+
// Fetch and inline the SVG content on component mount
|
|
11
|
+
onMount(async () => {
|
|
12
|
+
const res = await fetch(href);
|
|
13
|
+
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
console.error('Failed to fetch icon:', res.statusText);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
|
|
21
|
+
const doc = new DOMParser().parseFromString(text, 'image/svg+xml');
|
|
22
|
+
const errorNode = doc.querySelector('parsererror');
|
|
23
|
+
|
|
24
|
+
if (errorNode || doc.documentElement?.nodeName != 'svg') {
|
|
25
|
+
console.error('Invalid SVG');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
content = text;
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<span>{@html content}</span>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
span {
|
|
37
|
+
width: 1em;
|
|
38
|
+
height: 1em;
|
|
39
|
+
display: inline-block;
|
|
40
|
+
fill: #bbb;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit';
|
|
2
|
+
import { adapter } from '../../src/auth.js';
|
|
3
|
+
import type { PageServerLoadEvent } from './$types';
|
|
4
|
+
|
|
5
|
+
export async function load(event: PageServerLoadEvent) {
|
|
6
|
+
const session = await event.locals.auth();
|
|
7
|
+
|
|
8
|
+
if (!session) redirect(307, '/auth/signin');
|
|
9
|
+
if (!session.user.name) redirect(307, '/edit/name');
|
|
10
|
+
|
|
11
|
+
const user = await adapter.getUserByEmail(session.user.email);
|
|
12
|
+
|
|
13
|
+
return { session, user };
|
|
14
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import Icon from '$lib/Icon.svelte';
|
|
4
|
+
import { getUserImage } from '@axium/core';
|
|
5
|
+
|
|
6
|
+
const { user } = page.data;
|
|
7
|
+
|
|
8
|
+
const image = getUserImage(user);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<svelte:head>
|
|
12
|
+
<title>Axium Account</title>
|
|
13
|
+
</svelte:head>
|
|
14
|
+
|
|
15
|
+
<div id="content">
|
|
16
|
+
<img id="pfp" src={image} alt="User profile" />
|
|
17
|
+
<p id="greeting">Welcome, {user.name}</p>
|
|
18
|
+
<div class="main">
|
|
19
|
+
<div>
|
|
20
|
+
<p>Name</p>
|
|
21
|
+
<p>{user.name}</p>
|
|
22
|
+
<a href="/edit/name"><Icon id="chevron-right" /></a>
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<p>Email</p>
|
|
26
|
+
<p>{user.email}</p>
|
|
27
|
+
<a href="/edit/email"><Icon id="chevron-right" /></a>
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
<p>User ID <dfn title="This is your UUID."><Icon id="circle-info" style="regular" /></dfn></p>
|
|
31
|
+
<p>{user.id}</p>
|
|
32
|
+
</div>
|
|
33
|
+
<a id="signout" href="/auth/signout"><button>Sign out</button></a>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<style>
|
|
38
|
+
#content {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#pfp {
|
|
45
|
+
width: 100px;
|
|
46
|
+
height: 100px;
|
|
47
|
+
border-radius: 50%;
|
|
48
|
+
border: 1px solid #8888;
|
|
49
|
+
margin: 3em auto 2em;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#greeting {
|
|
53
|
+
font-size: 2em;
|
|
54
|
+
margin-bottom: 1em;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.main {
|
|
58
|
+
width: 50%;
|
|
59
|
+
padding-top: 4em;
|
|
60
|
+
|
|
61
|
+
> div:has(+ div) {
|
|
62
|
+
border-bottom: 1px solid #8888;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.main > div {
|
|
67
|
+
display: grid;
|
|
68
|
+
grid-template-columns: 10em 1fr 2em;
|
|
69
|
+
align-items: center;
|
|
70
|
+
width: 100%;
|
|
71
|
+
gap: 1em;
|
|
72
|
+
text-wrap: nowrap;
|
|
73
|
+
padding-bottom: 1em;
|
|
74
|
+
|
|
75
|
+
> :first-child {
|
|
76
|
+
margin: 0 5em 0 1em;
|
|
77
|
+
color: #bbbb;
|
|
78
|
+
font-size: 0.9em;
|
|
79
|
+
grid-column: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
> :nth-child(2) {
|
|
83
|
+
margin: 0;
|
|
84
|
+
grid-column: 2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
> a:last-child {
|
|
88
|
+
margin: 0;
|
|
89
|
+
display: inline;
|
|
90
|
+
grid-column: 3;
|
|
91
|
+
font-size: 0.75em;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#signout {
|
|
97
|
+
margin-top: 2em;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
|
2
|
+
import { adapter } from '../../../../src/auth';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
import type { PageServerLoadEvent } from './$types';
|
|
5
|
+
|
|
6
|
+
export async function load(event: PageServerLoadEvent) {
|
|
7
|
+
const session = await event.locals.auth();
|
|
8
|
+
if (!session) redirect(307, '/auth/signin');
|
|
9
|
+
return { session };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const actions = {
|
|
13
|
+
async default(event) {
|
|
14
|
+
const session = await event.locals.auth();
|
|
15
|
+
|
|
16
|
+
const rawEmail = (await event.request.formData()).get('email');
|
|
17
|
+
const { data: email, success, error } = z.string().email().safeParse(rawEmail);
|
|
18
|
+
|
|
19
|
+
if (!success)
|
|
20
|
+
return fail(400, {
|
|
21
|
+
email,
|
|
22
|
+
error: error.flatten().formErrors[0] || Object.values(error.flatten().fieldErrors).flat()[0],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const user = await adapter.getUserByEmail(session.user.email);
|
|
26
|
+
if (!user) return fail(500, { email, error: 'User does not exist' });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await adapter.updateUser({ id: user.id, email, image: user.image });
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
return fail(400, { email, error: typeof error === 'string' ? error : error.message });
|
|
32
|
+
}
|
|
33
|
+
redirect(303, '/');
|
|
34
|
+
},
|
|
35
|
+
} satisfies Actions;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { enhance } from '$app/forms';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
const { user } = page.data.session;
|
|
5
|
+
let { form } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<svelte:head>
|
|
9
|
+
<title>Edit Email</title>
|
|
10
|
+
</svelte:head>
|
|
11
|
+
|
|
12
|
+
<div>
|
|
13
|
+
<form method="POST" class="main" use:enhance>
|
|
14
|
+
{#if form?.error}
|
|
15
|
+
<div class="error">
|
|
16
|
+
{typeof form.error === 'string' ? form.error : JSON.stringify(form.error)}
|
|
17
|
+
</div>
|
|
18
|
+
{/if}
|
|
19
|
+
<div>
|
|
20
|
+
<label for="email">Email Address</label>
|
|
21
|
+
<input name="email" type="email" value={form?.email || user.email || ''} required />
|
|
22
|
+
</div>
|
|
23
|
+
<button type="submit">Continue</button>
|
|
24
|
+
</form>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Name } from '@axium/core/schemas';
|
|
2
|
+
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
|
3
|
+
import { adapter } from '../../../../src/auth';
|
|
4
|
+
import type { PageServerLoadEvent } from './$types';
|
|
5
|
+
|
|
6
|
+
export async function load(event: PageServerLoadEvent) {
|
|
7
|
+
const session = await event.locals.auth();
|
|
8
|
+
if (!session) redirect(307, '/auth/signin');
|
|
9
|
+
return { session };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const actions = {
|
|
13
|
+
async default(event) {
|
|
14
|
+
const session = await event.locals.auth();
|
|
15
|
+
|
|
16
|
+
const rawName = (await event.request.formData()).get('name');
|
|
17
|
+
const { data: name, success, error } = Name.safeParse(rawName);
|
|
18
|
+
|
|
19
|
+
if (!success)
|
|
20
|
+
return fail(400, {
|
|
21
|
+
name,
|
|
22
|
+
error: error.flatten().formErrors[0] || Object.values(error.flatten().fieldErrors).flat()[0],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const user = await adapter.getUserByEmail(session.user.email);
|
|
26
|
+
if (!user) return fail(500, { name, error: 'User does not exist' });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await adapter.updateUser({ id: user.id, name, image: user.image });
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
return fail(400, { name, error: typeof error === 'string' ? error : error.message });
|
|
32
|
+
}
|
|
33
|
+
redirect(303, '/');
|
|
34
|
+
},
|
|
35
|
+
} satisfies Actions;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { enhance } from '$app/forms';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
const { user } = page.data.session;
|
|
5
|
+
let { form } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<svelte:head>
|
|
9
|
+
<title>Edit Name</title>
|
|
10
|
+
</svelte:head>
|
|
11
|
+
|
|
12
|
+
<div>
|
|
13
|
+
<form method="POST" class="main" use:enhance>
|
|
14
|
+
{#if form?.error}
|
|
15
|
+
<div class="error">
|
|
16
|
+
{typeof form.error === 'string' ? form.error : JSON.stringify(form.error)}
|
|
17
|
+
</div>
|
|
18
|
+
{/if}
|
|
19
|
+
<div>
|
|
20
|
+
<label for="name">What do you want to be called?</label>
|
|
21
|
+
<input name="name" type="text" value={form?.name || user.name || ''} required />
|
|
22
|
+
</div>
|
|
23
|
+
<button type="submit">Continue</button>
|
|
24
|
+
</form>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Registration } from '@axium/core/schemas';
|
|
2
|
+
import { fail, redirect } from '@sveltejs/kit';
|
|
3
|
+
import * as auth from '../../../src/auth.js';
|
|
4
|
+
import * as config from '../../../src/config.js';
|
|
5
|
+
import type { Actions } from './$types';
|
|
6
|
+
|
|
7
|
+
export function load() {
|
|
8
|
+
if (!config.auth.credentials) return redirect(307, '/auth/signin');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const actions = {
|
|
12
|
+
async default(event) {
|
|
13
|
+
const { data, success, error } = Registration.safeParse(Object.fromEntries(await event.request.formData()));
|
|
14
|
+
|
|
15
|
+
if (!success)
|
|
16
|
+
return fail(400, {
|
|
17
|
+
...data,
|
|
18
|
+
error: error.flatten().formErrors[0] || Object.values(error.flatten().fieldErrors).flat()[0],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const { session } = await auth.register(data);
|
|
23
|
+
event.cookies.set('session', session.sessionToken, {
|
|
24
|
+
path: '/',
|
|
25
|
+
expires: session.expires,
|
|
26
|
+
httpOnly: true,
|
|
27
|
+
});
|
|
28
|
+
return { ...data, success: true, data: session.sessionToken };
|
|
29
|
+
} catch (error: any) {
|
|
30
|
+
return fail(400, { ...data, error: typeof error === 'string' ? error : error.message });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
} satisfies Actions;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { enhance } from '$app/forms';
|
|
3
|
+
let { form } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div>
|
|
7
|
+
<form method="POST" class="main" use:enhance>
|
|
8
|
+
{#if form?.error}
|
|
9
|
+
<div class="error">
|
|
10
|
+
{typeof form.error === 'string' ? form.error : JSON.stringify(form.error)}
|
|
11
|
+
</div>
|
|
12
|
+
{/if}
|
|
13
|
+
<div>
|
|
14
|
+
<label for="name">Display Name</label>
|
|
15
|
+
<input name="name" type="text" value={form?.name || ''} required />
|
|
16
|
+
</div>
|
|
17
|
+
<div>
|
|
18
|
+
<label for="email">Email</label>
|
|
19
|
+
<input name="email" type="email" value={form?.email || ''} required />
|
|
20
|
+
</div>
|
|
21
|
+
<div>
|
|
22
|
+
<label for="password">Password</label>
|
|
23
|
+
<input name="password" type="password" />
|
|
24
|
+
</div>
|
|
25
|
+
<button type="submit">Register</button>
|
|
26
|
+
</form>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
body {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
font-family: sans-serif;
|
|
5
|
+
font-size: 16px;
|
|
6
|
+
background-color: #222;
|
|
7
|
+
color: #bbb;
|
|
8
|
+
accent-color: #bbb;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.main {
|
|
12
|
+
padding: 2em;
|
|
13
|
+
border-radius: 1em;
|
|
14
|
+
background-color: #111;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: 1em;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
div:has(form.main) {
|
|
21
|
+
position: absolute;
|
|
22
|
+
inset: 0;
|
|
23
|
+
display: flex;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
align-items: center;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
form.main {
|
|
29
|
+
width: max-content;
|
|
30
|
+
height: max-content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
form {
|
|
34
|
+
div {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
input,
|
|
42
|
+
button {
|
|
43
|
+
border-radius: 0.5em;
|
|
44
|
+
border: 1px solid #aaa;
|
|
45
|
+
background-color: #222;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
input {
|
|
49
|
+
padding: 0.5em 1em;
|
|
50
|
+
outline: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button {
|
|
54
|
+
padding: 0.5em 1em;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
button:hover {
|
|
59
|
+
background-color: #334;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.error {
|
|
63
|
+
padding: 1em;
|
|
64
|
+
border-radius: 0.5em;
|
|
65
|
+
background-color: #733;
|
|
66
|
+
color: #ccc;
|
|
67
|
+
}
|