@axium/server 0.0.1 → 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/README.md +1 -0
- package/dist/auth.d.ts +28 -0
- package/dist/auth.js +136 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +153 -234
- package/dist/config.d.ts +270 -0
- package/dist/config.js +122 -0
- package/dist/database.d.ts +80 -0
- package/dist/database.js +193 -34
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/io.d.ts +21 -0
- package/dist/io.js +63 -0
- package/package.json +22 -7
- 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/database.js
CHANGED
|
@@ -51,31 +51,17 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { Kysely, PostgresDialect, sql } from 'kysely';
|
|
54
|
+
import { exec } from 'node:child_process';
|
|
55
|
+
import { randomBytes } from 'node:crypto';
|
|
54
56
|
import pg from 'pg';
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!config.port) {
|
|
58
|
-
const [hostname, port] = config.host.split(':');
|
|
59
|
-
config.port = port ? parseInt(port) : 5432;
|
|
60
|
-
config.host = hostname;
|
|
61
|
-
}
|
|
62
|
-
if (config.host.includes(':'))
|
|
63
|
-
config.host = config.host.split(':')[0];
|
|
64
|
-
return config;
|
|
65
|
-
}
|
|
57
|
+
import * as config from './config.js';
|
|
58
|
+
import { logger } from './io.js';
|
|
66
59
|
export let database;
|
|
67
|
-
export function connect(
|
|
60
|
+
export function connect() {
|
|
68
61
|
if (database)
|
|
69
62
|
return database;
|
|
70
|
-
normalizeConfig(config);
|
|
71
63
|
const _db = new Kysely({
|
|
72
|
-
dialect: new PostgresDialect({
|
|
73
|
-
pool: new pg.Pool({
|
|
74
|
-
...config,
|
|
75
|
-
user: 'axium',
|
|
76
|
-
database: 'axium',
|
|
77
|
-
}),
|
|
78
|
-
}),
|
|
64
|
+
dialect: new PostgresDialect({ pool: new pg.Pool(config.db) }),
|
|
79
65
|
});
|
|
80
66
|
database = Object.assign(_db, {
|
|
81
67
|
async [Symbol.asyncDispose]() {
|
|
@@ -84,16 +70,177 @@ export function connect(config) {
|
|
|
84
70
|
});
|
|
85
71
|
return database;
|
|
86
72
|
}
|
|
87
|
-
export async function status(
|
|
73
|
+
export async function status() {
|
|
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
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function statusText() {
|
|
82
|
+
try {
|
|
83
|
+
const stats = await status();
|
|
84
|
+
return `${stats.users} users, ${stats.accounts} accounts, ${stats.sessions} sessions`;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw typeof error == 'object' && 'message' in error ? error.message : error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* TS can't tell when we do this inline
|
|
92
|
+
*/
|
|
93
|
+
function _fixOutput(opt) {
|
|
94
|
+
opt.output ??= () => { };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Convenience function for `sudo -u postgres psql -c "${command}"`, plus `report` coolness.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
async function execSQL(opts, command, message) {
|
|
101
|
+
let stderr;
|
|
102
|
+
try {
|
|
103
|
+
opts.output('start', message);
|
|
104
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
105
|
+
exec(`sudo -u postgres psql -c "${command}"`, opts, (err, _, _stderr) => {
|
|
106
|
+
stderr = _stderr.startsWith('ERROR:') ? _stderr.slice(6).trim() : _stderr;
|
|
107
|
+
if (err)
|
|
108
|
+
reject('[command]');
|
|
109
|
+
else
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
await promise;
|
|
113
|
+
opts.output('done');
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw error == '[command]' ? stderr?.slice(0, 100) || 'failed.' : typeof error == 'object' && 'message' in error ? error.message : error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function shouldRecreate(opt) {
|
|
120
|
+
if (opt.skip) {
|
|
121
|
+
opt.output('warn', 'already exists. (skipped)\n');
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (opt.force) {
|
|
125
|
+
opt.output('warn', 'already exists. (re-creating)\n');
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
opt.output('warn', 'already exists. Use --skip to skip or --force to re-create.\n');
|
|
129
|
+
throw 2;
|
|
130
|
+
}
|
|
131
|
+
export async function init(opt) {
|
|
88
132
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
89
133
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
_fixOutput(opt);
|
|
135
|
+
if (!config.db.password) {
|
|
136
|
+
config.save({ db: { password: randomBytes(32).toString('base64') } }, true);
|
|
137
|
+
logger.debug('Generated password and wrote to global config');
|
|
138
|
+
}
|
|
139
|
+
const _sql = (command, message) => execSQL(opt, command, message);
|
|
140
|
+
await _sql('CREATE DATABASE axium', 'Creating database').catch(async (error) => {
|
|
141
|
+
if (error != 'database "axium" already exists')
|
|
142
|
+
throw error;
|
|
143
|
+
if (shouldRecreate(opt))
|
|
144
|
+
return;
|
|
145
|
+
await _sql('DROP DATABASE axium', 'Dropping database');
|
|
146
|
+
await _sql('CREATE DATABASE axium', 'Re-creating database');
|
|
147
|
+
});
|
|
148
|
+
const createQuery = `CREATE USER axium WITH ENCRYPTED PASSWORD '${config.db.password}' LOGIN`;
|
|
149
|
+
await _sql(createQuery, 'Creating user').catch(async (error) => {
|
|
150
|
+
if (error != 'role "axium" already exists')
|
|
151
|
+
throw error;
|
|
152
|
+
if (shouldRecreate(opt))
|
|
153
|
+
return;
|
|
154
|
+
await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
155
|
+
await _sql('DROP USER axium', 'Dropping user');
|
|
156
|
+
await _sql(createQuery, 'Re-creating user');
|
|
157
|
+
});
|
|
158
|
+
await _sql('GRANT ALL PRIVILEGES ON DATABASE axium TO axium', 'Granting database privileges');
|
|
159
|
+
await _sql('GRANT ALL PRIVILEGES ON SCHEMA public TO axium', 'Granting schema privileges');
|
|
160
|
+
await _sql('ALTER DATABASE axium OWNER TO axium', 'Setting database owner');
|
|
161
|
+
await _sql('SELECT pg_reload_conf()', 'Reloading configuration');
|
|
162
|
+
const db = __addDisposableResource(env_1, connect(), true);
|
|
163
|
+
const relationExists = (table) => (error) => {
|
|
164
|
+
error = typeof error == 'object' && 'message' in error ? error.message : error;
|
|
165
|
+
if (error == `relation "${table}" already exists`)
|
|
166
|
+
opt.output('warn', 'already exists.');
|
|
167
|
+
else
|
|
168
|
+
throw error;
|
|
96
169
|
};
|
|
170
|
+
opt.output('start', 'Creating table User');
|
|
171
|
+
await db.schema
|
|
172
|
+
.createTable('User')
|
|
173
|
+
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
174
|
+
.addColumn('name', 'text')
|
|
175
|
+
.addColumn('email', 'text', col => col.unique().notNull())
|
|
176
|
+
.addColumn('emailVerified', 'timestamptz')
|
|
177
|
+
.addColumn('image', 'text')
|
|
178
|
+
.addColumn('password', 'text')
|
|
179
|
+
.addColumn('salt', 'text')
|
|
180
|
+
.execute()
|
|
181
|
+
.catch(relationExists('User'));
|
|
182
|
+
opt.output('done');
|
|
183
|
+
opt.output('start', 'Creating table Account');
|
|
184
|
+
await db.schema
|
|
185
|
+
.createTable('Account')
|
|
186
|
+
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
187
|
+
.addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
|
|
188
|
+
.addColumn('type', 'text', col => col.notNull())
|
|
189
|
+
.addColumn('provider', 'text', col => col.notNull())
|
|
190
|
+
.addColumn('providerAccountId', 'text', col => col.notNull())
|
|
191
|
+
.addColumn('refresh_token', 'text')
|
|
192
|
+
.addColumn('access_token', 'text')
|
|
193
|
+
.addColumn('expires_at', 'bigint')
|
|
194
|
+
.addColumn('token_type', 'text')
|
|
195
|
+
.addColumn('scope', 'text')
|
|
196
|
+
.addColumn('id_token', 'text')
|
|
197
|
+
.addColumn('session_state', 'text')
|
|
198
|
+
.execute()
|
|
199
|
+
.catch(relationExists('Account'));
|
|
200
|
+
opt.output('done');
|
|
201
|
+
opt.output('start', 'Creating index for Account.userId');
|
|
202
|
+
db.schema.createIndex('Account_userId_index').on('Account').column('userId').execute().catch(relationExists('Account_userId_index'));
|
|
203
|
+
opt.output('done');
|
|
204
|
+
opt.output('start', 'Creating table Session');
|
|
205
|
+
await db.schema
|
|
206
|
+
.createTable('Session')
|
|
207
|
+
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
208
|
+
.addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
|
|
209
|
+
.addColumn('sessionToken', 'text', col => col.notNull().unique())
|
|
210
|
+
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
211
|
+
.execute()
|
|
212
|
+
.catch(relationExists('Session'));
|
|
213
|
+
opt.output('done');
|
|
214
|
+
opt.output('start', 'Creating index for Session.userId');
|
|
215
|
+
db.schema.createIndex('Session_userId_index').on('Session').column('userId').execute().catch(relationExists('Session_userId_index'));
|
|
216
|
+
opt.output('done');
|
|
217
|
+
opt.output('start', 'Creating table VerificationToken');
|
|
218
|
+
await db.schema
|
|
219
|
+
.createTable('VerificationToken')
|
|
220
|
+
.addColumn('identifier', 'text', col => col.notNull())
|
|
221
|
+
.addColumn('token', 'text', col => col.notNull().unique())
|
|
222
|
+
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
223
|
+
.execute()
|
|
224
|
+
.catch(relationExists('VerificationToken'));
|
|
225
|
+
opt.output('done');
|
|
226
|
+
opt.output('start', 'Creating table Authenticator');
|
|
227
|
+
await db.schema
|
|
228
|
+
.createTable('Authenticator')
|
|
229
|
+
.addColumn('credentialID', 'text', col => col.primaryKey().notNull())
|
|
230
|
+
.addColumn('userId', 'uuid', col => col.notNull().references('User.id').onDelete('cascade').onUpdate('cascade'))
|
|
231
|
+
.addColumn('providerAccountId', 'text', col => col.notNull())
|
|
232
|
+
.addColumn('credentialPublicKey', 'text', col => col.notNull())
|
|
233
|
+
.addColumn('counter', 'integer', col => col.notNull())
|
|
234
|
+
.addColumn('credentialDeviceType', 'text', col => col.notNull())
|
|
235
|
+
.addColumn('credentialBackedUp', 'boolean', col => col.notNull())
|
|
236
|
+
.addColumn('transports', 'text')
|
|
237
|
+
.execute()
|
|
238
|
+
.catch(relationExists('Authenticator'));
|
|
239
|
+
opt.output('done');
|
|
240
|
+
opt.output('start', 'Creating index for Authenticator.credentialID');
|
|
241
|
+
db.schema.createIndex('Authenticator_credentialID_key').on('Authenticator').column('credentialID').execute().catch(relationExists('Authenticator_credentialID_key'));
|
|
242
|
+
opt.output('done');
|
|
243
|
+
return config.db;
|
|
97
244
|
}
|
|
98
245
|
catch (e_1) {
|
|
99
246
|
env_1.error = e_1;
|
|
@@ -105,12 +252,24 @@ export async function status(config) {
|
|
|
105
252
|
await result_1;
|
|
106
253
|
}
|
|
107
254
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Completely remove Axium from the database.
|
|
257
|
+
*/
|
|
258
|
+
export async function uninstall(opt) {
|
|
259
|
+
_fixOutput(opt);
|
|
260
|
+
await execSQL(opt, 'DROP DATABASE axium', 'Dropping database');
|
|
261
|
+
await execSQL(opt, 'REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
262
|
+
await execSQL(opt, 'DROP USER axium', 'Dropping user');
|
|
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');
|
|
115
274
|
}
|
|
116
275
|
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
package/dist/io.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Logger } from 'logzen';
|
|
2
|
+
/**
|
|
3
|
+
* Find the Axium directory.
|
|
4
|
+
* This directory includes things like config files, secrets, etc.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findDir(global: boolean): string;
|
|
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
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Logger } from 'logzen';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path/posix';
|
|
5
|
+
import { styleText } from 'node:util';
|
|
6
|
+
/**
|
|
7
|
+
* Find the Axium directory.
|
|
8
|
+
* This directory includes things like config files, secrets, etc.
|
|
9
|
+
*/
|
|
10
|
+
export function findDir(global) {
|
|
11
|
+
if (process.env.AXIUM_DIR)
|
|
12
|
+
return process.env.AXIUM_DIR;
|
|
13
|
+
if (global && process.getuid?.() === 0)
|
|
14
|
+
return '/etc/axium';
|
|
15
|
+
if (global)
|
|
16
|
+
return join(homedir(), '.axium');
|
|
17
|
+
return '.axium';
|
|
18
|
+
}
|
|
19
|
+
if (process.getuid?.() === 0)
|
|
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,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"main": "dist/index.js",
|
|
3
|
+
"version": "0.0.3",
|
|
5
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
6
5
|
"funding": {
|
|
7
6
|
"type": "individual",
|
|
@@ -15,23 +14,39 @@
|
|
|
15
14
|
"bugs": {
|
|
16
15
|
"url": "https://github.com/james-pre/axium/issues"
|
|
17
16
|
},
|
|
18
|
-
"types": "dist/index.d.ts",
|
|
19
17
|
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"web"
|
|
22
23
|
],
|
|
23
24
|
"bin": {
|
|
24
|
-
"axium": "
|
|
25
|
+
"axium": "dist/cli.js"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "tsc"
|
|
28
29
|
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@axium/core": ">=0.0.1"
|
|
32
|
+
},
|
|
29
33
|
"dependencies": {
|
|
34
|
+
"@auth/core": "^0.38.0",
|
|
35
|
+
"@auth/kysely-adapter": "^1.8.0",
|
|
30
36
|
"@types/pg": "^8.11.11",
|
|
31
|
-
"
|
|
37
|
+
"bcryptjs": "^3.0.2",
|
|
32
38
|
"commander": "^13.1.0",
|
|
33
39
|
"kysely": "^0.27.5",
|
|
40
|
+
"logzen": "^0.6.2",
|
|
34
41
|
"pg": "^8.14.1",
|
|
35
|
-
"
|
|
42
|
+
"utilium": "^2.2.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@auth/sveltekit": "^1.8.0",
|
|
46
|
+
"@sveltejs/adapter-node": "^5.2.12",
|
|
47
|
+
"@sveltejs/kit": "^2.20.2",
|
|
48
|
+
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
49
|
+
"svelte": "^5.25.3",
|
|
50
|
+
"vite-plugin-mkcert": "^1.17.8"
|
|
36
51
|
}
|
|
37
52
|
}
|
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>
|