@axium/client 0.5.0 → 0.6.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/assets/styles.css +21 -0
- package/axium-client.service +22 -0
- package/dist/cli/config.d.ts +23 -0
- package/dist/cli/config.js +52 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +244 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +33 -0
- package/dist/user.d.ts +1 -1
- package/lib/AccessControlDialog.svelte +2 -3
- package/lib/FormDialog.svelte +1 -10
- package/lib/Login.svelte +4 -2
- package/lib/Register.svelte +7 -1
- package/lib/auth_redirect.ts +28 -0
- package/package.json +8 -3
package/assets/styles.css
CHANGED
|
@@ -65,6 +65,27 @@ textarea {
|
|
|
65
65
|
outline: none;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
select,
|
|
69
|
+
::picker(select) {
|
|
70
|
+
appearance: base-select;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
::picker(select) {
|
|
74
|
+
border: 1px solid var(--border-accent);
|
|
75
|
+
padding: 1em;
|
|
76
|
+
border-radius: 0.5em;
|
|
77
|
+
background-color: var(--bg-menu);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
option {
|
|
81
|
+
padding: 0.25em 0;
|
|
82
|
+
border-radius: 0.5em;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
option:hover {
|
|
86
|
+
background-color: var(--bg-alt);
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
button {
|
|
69
90
|
cursor: pointer;
|
|
70
91
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Axium Client Daemon
|
|
3
|
+
Wants=network-online.target
|
|
4
|
+
After=network-online.target
|
|
5
|
+
RequiresMountsFor=%Y
|
|
6
|
+
StartLimitIntervalSec=1min
|
|
7
|
+
StartLimitBurst=5
|
|
8
|
+
StartLimitAction=none
|
|
9
|
+
FailureAction=none
|
|
10
|
+
|
|
11
|
+
[Service]
|
|
12
|
+
Type=simple
|
|
13
|
+
ExecStartPre=/usr/bin/env cd "%Y/../../.."
|
|
14
|
+
ExecStart=/usr/bin/env npx --prefix "%Y/../../.." axium-client run
|
|
15
|
+
ExecReload=kill -HUP $MAINPID
|
|
16
|
+
SyslogIdentifier=axium-client
|
|
17
|
+
Restart=on-failure
|
|
18
|
+
RestartSec=3s
|
|
19
|
+
TimeoutStopSec=15s
|
|
20
|
+
|
|
21
|
+
[Install]
|
|
22
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const configDir: string;
|
|
2
|
+
export declare function session(): {
|
|
3
|
+
id: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
expires: Date;
|
|
6
|
+
created: Date;
|
|
7
|
+
elevated: boolean;
|
|
8
|
+
user: {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
preferences: Record<string, any>;
|
|
13
|
+
roles: string[];
|
|
14
|
+
registeredAt: Date;
|
|
15
|
+
isAdmin: boolean;
|
|
16
|
+
emailVerified?: Date | null | undefined;
|
|
17
|
+
image?: string | null | undefined;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function loadConfig(safe: boolean): Promise<void>;
|
|
21
|
+
export declare function saveConfig(): void;
|
|
22
|
+
export declare const _dayMs: number;
|
|
23
|
+
export declare function updateCache(force: boolean): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as io from '@axium/core/node/io';
|
|
2
|
+
import { loadPlugin } from '@axium/core/node/plugins';
|
|
3
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path/posix';
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { fetchAPI, setPrefix, setToken } from '../requests.js';
|
|
8
|
+
import { getCurrentSession } from '../user.js';
|
|
9
|
+
import { ClientConfig, config } from '../config.js';
|
|
10
|
+
export const configDir = join(homedir(), '.config/axium');
|
|
11
|
+
mkdirSync(configDir, { recursive: true });
|
|
12
|
+
const axcConfig = join(configDir, 'config.json');
|
|
13
|
+
export function session() {
|
|
14
|
+
if (!config.token)
|
|
15
|
+
io.exit('Not logged in.', 4);
|
|
16
|
+
if (!config.cache)
|
|
17
|
+
io.exit('No session data available.', 3);
|
|
18
|
+
return config.cache.session;
|
|
19
|
+
}
|
|
20
|
+
export async function loadConfig(safe) {
|
|
21
|
+
try {
|
|
22
|
+
Object.assign(config, ClientConfig.parse(JSON.parse(readFileSync(axcConfig, 'utf-8'))));
|
|
23
|
+
if (config.server)
|
|
24
|
+
setPrefix(config.server);
|
|
25
|
+
if (config.token)
|
|
26
|
+
setToken(config.token);
|
|
27
|
+
for (const plugin of config.plugins ?? [])
|
|
28
|
+
await loadPlugin('client', plugin, axcConfig, safe);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
io.warn('Failed to load config: ' + (e instanceof z.core.$ZodError ? z.prettifyError(e) : io._debugOutput ? e.stack : e.message));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function saveConfig() {
|
|
35
|
+
io.writeJSON(axcConfig, config);
|
|
36
|
+
io.debug('Saved config to ' + axcConfig);
|
|
37
|
+
}
|
|
38
|
+
export const _dayMs = 24 * 3600_000;
|
|
39
|
+
export async function updateCache(force) {
|
|
40
|
+
if (!force && config.cache && config.cache.fetched + _dayMs > Date.now())
|
|
41
|
+
return;
|
|
42
|
+
io.start('Fetching metadata');
|
|
43
|
+
const [session, apps] = await Promise.all([getCurrentSession(), fetchAPI('GET', 'apps')]).catch(err => io.exit(err.message));
|
|
44
|
+
try {
|
|
45
|
+
config.cache = { fetched: Date.now(), session, apps };
|
|
46
|
+
saveConfig();
|
|
47
|
+
io.done();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
3
|
+
if (value !== null && value !== void 0) {
|
|
4
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
5
|
+
var dispose, inner;
|
|
6
|
+
if (async) {
|
|
7
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
8
|
+
dispose = value[Symbol.asyncDispose];
|
|
9
|
+
}
|
|
10
|
+
if (dispose === void 0) {
|
|
11
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
12
|
+
dispose = value[Symbol.dispose];
|
|
13
|
+
if (async) inner = dispose;
|
|
14
|
+
}
|
|
15
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
16
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
17
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
18
|
+
}
|
|
19
|
+
else if (async) {
|
|
20
|
+
env.stack.push({ async: true });
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
25
|
+
return function (env) {
|
|
26
|
+
function fail(e) {
|
|
27
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
28
|
+
env.hasError = true;
|
|
29
|
+
}
|
|
30
|
+
var r, s = 0;
|
|
31
|
+
function next() {
|
|
32
|
+
while (r = env.stack.pop()) {
|
|
33
|
+
try {
|
|
34
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
35
|
+
if (r.dispose) {
|
|
36
|
+
var result = r.dispose.call(r.value);
|
|
37
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
38
|
+
}
|
|
39
|
+
else s |= 1;
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
fail(e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
46
|
+
if (env.hasError) throw env.error;
|
|
47
|
+
}
|
|
48
|
+
return next();
|
|
49
|
+
};
|
|
50
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
51
|
+
var e = new Error(message);
|
|
52
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
53
|
+
});
|
|
54
|
+
import { outputDaemonStatus, io, pluginText } from '@axium/core/node';
|
|
55
|
+
import { _findPlugin, plugins } from '@axium/core/plugins';
|
|
56
|
+
import { program } from 'commander';
|
|
57
|
+
import { createServer } from 'node:http';
|
|
58
|
+
import { createInterface } from 'node:readline/promises';
|
|
59
|
+
import { styleText } from 'node:util';
|
|
60
|
+
import * as z from 'zod';
|
|
61
|
+
import $pkg from '../../package.json' with { type: 'json' };
|
|
62
|
+
import { config, resolveServerURL } from '../config.js';
|
|
63
|
+
import { prefix, setPrefix, setToken } from '../requests.js';
|
|
64
|
+
import { getCurrentSession, logout } from '../user.js';
|
|
65
|
+
import { loadConfig, saveConfig, updateCache } from './config.js';
|
|
66
|
+
const safe = z.stringbool().default(false).parse(process.env.SAFE?.toLowerCase()) || process.argv.includes('--safe');
|
|
67
|
+
await loadConfig(safe);
|
|
68
|
+
process.on('SIGHUP', () => {
|
|
69
|
+
io.info('Reloading configuration due to SIGHUP.');
|
|
70
|
+
void loadConfig(safe);
|
|
71
|
+
});
|
|
72
|
+
var rl, axiumPlugin;
|
|
73
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
74
|
+
try {
|
|
75
|
+
rl = __addDisposableResource(env_1, createInterface({
|
|
76
|
+
input: process.stdin,
|
|
77
|
+
output: process.stdout,
|
|
78
|
+
}), false);
|
|
79
|
+
rl.on('SIGINT', () => io.exit('Aborted.', 7));
|
|
80
|
+
program
|
|
81
|
+
.version($pkg.version)
|
|
82
|
+
.name('axium-client')
|
|
83
|
+
.alias('axc')
|
|
84
|
+
.description('Axium client CLI')
|
|
85
|
+
.configureHelp({ showGlobalOptions: true })
|
|
86
|
+
.option('--debug', 'override debug mode')
|
|
87
|
+
.option('--no-debug', 'override debug mode')
|
|
88
|
+
.option('--refresh-session', 'Force a refresh of session and user metadata from server')
|
|
89
|
+
.option('--cache-only', 'Run entirely from local cache, even if it is expired.')
|
|
90
|
+
.option('--safe', 'do not execute code from plugins');
|
|
91
|
+
program.on('option:debug', () => io._setDebugOutput(true));
|
|
92
|
+
program.hook('preAction', async (_, action) => {
|
|
93
|
+
const opt = action.optsWithGlobals();
|
|
94
|
+
if (!config.token)
|
|
95
|
+
return;
|
|
96
|
+
if (!opt.cacheOnly)
|
|
97
|
+
await updateCache(opt.refreshSession);
|
|
98
|
+
});
|
|
99
|
+
program
|
|
100
|
+
.command('login')
|
|
101
|
+
.description('Log in to your account on an Axium server')
|
|
102
|
+
.argument('[server]', 'Axium server URL')
|
|
103
|
+
.action(async (url) => {
|
|
104
|
+
if (prefix[0] != '/')
|
|
105
|
+
url ||= prefix;
|
|
106
|
+
url ||= await rl.question('Axium server URL: ');
|
|
107
|
+
url = resolveServerURL(url);
|
|
108
|
+
setPrefix(url);
|
|
109
|
+
const sessionReady = Promise.withResolvers();
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
111
|
+
const server = createServer(async (req, res) => {
|
|
112
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
113
|
+
res.setHeader('access-control-allow-methods', '*');
|
|
114
|
+
res.setHeader('access-control-allow-headers', '*');
|
|
115
|
+
if (req.method == 'HEAD' || req.method == 'OPTIONS') {
|
|
116
|
+
res.writeHead(200).end();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!req.headers['content-type']?.endsWith('/json')) {
|
|
120
|
+
res.writeHead(415).end('Unexpected content type');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (req.method !== 'POST') {
|
|
124
|
+
res.writeHead(405).end('Unexpected request method');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const { promise: bodyReady, resolve, reject } = Promise.withResolvers();
|
|
128
|
+
let body = '';
|
|
129
|
+
req.on('data', chunk => (body += chunk.toString()));
|
|
130
|
+
req.on('end', resolve);
|
|
131
|
+
req.on('error', reject);
|
|
132
|
+
try {
|
|
133
|
+
await bodyReady;
|
|
134
|
+
sessionReady.resolve(JSON.parse(body));
|
|
135
|
+
res.writeHead(200).end();
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
res.statusCode = 500;
|
|
139
|
+
res.end('Internal server error: ' + e.message);
|
|
140
|
+
sessionReady.reject(e.message);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const serverReady = Promise.withResolvers();
|
|
144
|
+
server.listen(() => {
|
|
145
|
+
const { port } = server.address();
|
|
146
|
+
serverReady.resolve(port);
|
|
147
|
+
});
|
|
148
|
+
server.on('error', e => io.exit('Failed to start local callback server: ' + e.message, 5));
|
|
149
|
+
const port = await serverReady.promise;
|
|
150
|
+
const authURL = new URL('/login/client?port=' + port, url).href;
|
|
151
|
+
console.log('Authenticate by visiting this URL in your browser: ' + styleText('underline', authURL));
|
|
152
|
+
const { token } = await sessionReady.promise.catch(e => io.exit('Failed to obtain session: ' + e, 6));
|
|
153
|
+
setToken(token);
|
|
154
|
+
server.close();
|
|
155
|
+
io.start('Verifying session');
|
|
156
|
+
const session = await getCurrentSession().catch(e => io.exit(e.message, 6));
|
|
157
|
+
io.done();
|
|
158
|
+
io.debug('Session UUID: ' + session.id);
|
|
159
|
+
console.log(`Welcome ${session.user.name}! Your session is valid until ${session.expires.toLocaleDateString()}.`);
|
|
160
|
+
config.token = token;
|
|
161
|
+
config.server = url;
|
|
162
|
+
saveConfig();
|
|
163
|
+
await updateCache(true);
|
|
164
|
+
});
|
|
165
|
+
program.command('logout').action(async () => {
|
|
166
|
+
if (!config.token)
|
|
167
|
+
io.exit('Not logged in.', 4);
|
|
168
|
+
if (!config.cache)
|
|
169
|
+
io.exit('No session data available.', 3);
|
|
170
|
+
await logout(config.cache.session.userId, config.cache.session.id);
|
|
171
|
+
});
|
|
172
|
+
program.command('status').action(() => {
|
|
173
|
+
if (!config.token)
|
|
174
|
+
return console.log('Not logged in.');
|
|
175
|
+
if (!config.cache)
|
|
176
|
+
return console.log('No session data available.');
|
|
177
|
+
console.log('Logged in to', new URL(prefix).host);
|
|
178
|
+
console.log(styleText('whiteBright', 'Session ID:'), config.cache.session.id);
|
|
179
|
+
const { user } = config.cache.session;
|
|
180
|
+
console.log(styleText('whiteBright', 'User:'), user.name, `<${user.email}>`, styleText('dim', `(${user.id})`));
|
|
181
|
+
outputDaemonStatus('axium-client');
|
|
182
|
+
});
|
|
183
|
+
program
|
|
184
|
+
.command('run')
|
|
185
|
+
.argument('[plugin]', 'The plugin to run')
|
|
186
|
+
.action(async (name) => {
|
|
187
|
+
if (name) {
|
|
188
|
+
const plugin = _findPlugin(name);
|
|
189
|
+
await plugin._client?.run();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const plugin of plugins.values())
|
|
193
|
+
await plugin._client?.run();
|
|
194
|
+
});
|
|
195
|
+
axiumPlugin = program.command('plugin').alias('plugins').description('Manage plugins');
|
|
196
|
+
axiumPlugin
|
|
197
|
+
.command('list')
|
|
198
|
+
.alias('ls')
|
|
199
|
+
.description('List loaded plugins')
|
|
200
|
+
.option('-l, --long', 'use the long listing format')
|
|
201
|
+
.option('--no-versions', 'do not show plugin versions')
|
|
202
|
+
.action((opt) => {
|
|
203
|
+
if (!plugins.size) {
|
|
204
|
+
console.log('No plugins loaded.');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!opt.long) {
|
|
208
|
+
console.log(Array.from(plugins.keys()).join(', '));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.log(styleText('whiteBright', plugins.size + ' plugin(s) loaded:'));
|
|
212
|
+
for (const plugin of plugins.values()) {
|
|
213
|
+
console.log(plugin.name, opt.versions ? plugin.version : '');
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
axiumPlugin
|
|
217
|
+
.command('info')
|
|
218
|
+
.description('Get information about a plugin')
|
|
219
|
+
.argument('<plugin>', 'the plugin to get information about')
|
|
220
|
+
.action((search) => {
|
|
221
|
+
const plugin = _findPlugin(search);
|
|
222
|
+
for (const line of pluginText(plugin))
|
|
223
|
+
console.log(line);
|
|
224
|
+
});
|
|
225
|
+
axiumPlugin
|
|
226
|
+
.command('remove')
|
|
227
|
+
.alias('rm')
|
|
228
|
+
.description('Remove a plugin')
|
|
229
|
+
.argument('<plugin>', 'the plugin to remove')
|
|
230
|
+
.action((search) => {
|
|
231
|
+
const plugin = _findPlugin(search);
|
|
232
|
+
config.plugins = config.plugins.filter(p => p !== plugin.specifier);
|
|
233
|
+
plugins.delete(plugin.name);
|
|
234
|
+
saveConfig();
|
|
235
|
+
});
|
|
236
|
+
await program.parseAsync();
|
|
237
|
+
}
|
|
238
|
+
catch (e_1) {
|
|
239
|
+
env_1.error = e_1;
|
|
240
|
+
env_1.hasError = true;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
__disposeResources(env_1);
|
|
244
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
export declare const ClientConfig: z.ZodObject<{
|
|
3
|
+
token: z.ZodOptional<z.ZodNullable<z.ZodBase64>>;
|
|
4
|
+
server: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
|
|
5
|
+
cache: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
6
|
+
fetched: z.ZodInt;
|
|
7
|
+
session: z.ZodObject<{
|
|
8
|
+
id: z.ZodUUID;
|
|
9
|
+
userId: z.ZodUUID;
|
|
10
|
+
expires: z.ZodCoercedDate<unknown>;
|
|
11
|
+
created: z.ZodCoercedDate<unknown>;
|
|
12
|
+
elevated: z.ZodBoolean;
|
|
13
|
+
user: z.ZodObject<{
|
|
14
|
+
id: z.ZodUUID;
|
|
15
|
+
name: z.ZodString;
|
|
16
|
+
email: z.ZodEmail;
|
|
17
|
+
emailVerified: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
|
|
18
|
+
image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
|
|
19
|
+
preferences: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
20
|
+
roles: z.ZodArray<z.ZodString>;
|
|
21
|
+
registeredAt: z.ZodCoercedDate<unknown>;
|
|
22
|
+
isAdmin: z.ZodBoolean;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
apps: z.ZodArray<z.ZodObject<{
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
name: z.ZodOptional<z.ZodString>;
|
|
28
|
+
image: z.ZodOptional<z.ZodString>;
|
|
29
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
}, z.core.$loose>>>;
|
|
32
|
+
plugins: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
33
|
+
}, z.core.$loose>;
|
|
34
|
+
export interface ClientConfig extends z.infer<typeof ClientConfig> {
|
|
35
|
+
}
|
|
36
|
+
export declare const config: ClientConfig;
|
|
37
|
+
export declare function resolveServerURL(server: string): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { debug, warn } from '@axium/core/io';
|
|
2
|
+
import { App, Session, User } from '@axium/core';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
export const ClientConfig = z.looseObject({
|
|
5
|
+
token: z.base64().nullish(),
|
|
6
|
+
server: z.url().nullish(),
|
|
7
|
+
// Cache to reduce server load:
|
|
8
|
+
cache: z
|
|
9
|
+
.looseObject({
|
|
10
|
+
fetched: z.int(),
|
|
11
|
+
session: Session.extend({ user: User }),
|
|
12
|
+
apps: App.array(),
|
|
13
|
+
})
|
|
14
|
+
.nullish(),
|
|
15
|
+
plugins: z.string().array().default([]),
|
|
16
|
+
});
|
|
17
|
+
export const config = {
|
|
18
|
+
plugins: [],
|
|
19
|
+
};
|
|
20
|
+
export function resolveServerURL(server) {
|
|
21
|
+
if (!server.startsWith('http://') && !server.startsWith('https://'))
|
|
22
|
+
server = 'https://' + server;
|
|
23
|
+
const url = new URL(server);
|
|
24
|
+
if (url.pathname.endsWith('/api'))
|
|
25
|
+
url.pathname += '/';
|
|
26
|
+
else if (url.pathname.at(-1) == '/' && !url.pathname.endsWith('/api/'))
|
|
27
|
+
url.pathname += 'api/';
|
|
28
|
+
if (url.pathname != '/api/')
|
|
29
|
+
warn('Resolved server URL is not at the top level: ' + url.href);
|
|
30
|
+
else
|
|
31
|
+
debug('Resolved server URL: ' + url.href);
|
|
32
|
+
return url.href;
|
|
33
|
+
}
|
package/dist/user.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function getSessions(userId: string): Promise<Session[]>;
|
|
|
13
13
|
export declare function logout(userId: string, ...sessionId: string[]): Promise<Session[]>;
|
|
14
14
|
export declare function logoutAll(userId: string): Promise<Session[]>;
|
|
15
15
|
export declare function logoutCurrentSession(): Promise<Session>;
|
|
16
|
-
export declare function register(_data: Record<string,
|
|
16
|
+
export declare function register(_data: Record<string, unknown>): Promise<void>;
|
|
17
17
|
export declare function userInfo(userId: string): Promise<UserPublic & Partial<User>>;
|
|
18
18
|
export declare function updateUser(userId: string, data: Record<string, FormDataEntryValue>): Promise<User>;
|
|
19
19
|
export declare function fullUserInfo(userId: string): Promise<User & {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import type { User } from '@axium/core';
|
|
3
3
|
import { permissionNames, type AccessControllable } from '@axium/core/access';
|
|
4
4
|
import type { Entries } from 'utilium';
|
|
5
|
+
import FormDialog from './FormDialog.svelte';
|
|
5
6
|
import UserCard from './UserCard.svelte';
|
|
6
|
-
import type { Permission, AccessControl } from '@axium/core/access';
|
|
7
|
-
import type { User } from '@axium/core';
|
|
8
7
|
|
|
9
8
|
let {
|
|
10
9
|
item = $bindable(),
|
package/lib/FormDialog.svelte
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Dialog from './Dialog.svelte';
|
|
3
3
|
|
|
4
|
-
function resolveRedirectAfter() {
|
|
5
|
-
const url = new URL(location.href);
|
|
6
|
-
const maybe = url.searchParams.get('after');
|
|
7
|
-
if (!maybe || maybe == url.pathname) return '/';
|
|
8
|
-
for (const prefix of ['/api/']) if (maybe.startsWith(prefix)) return '/';
|
|
9
|
-
return maybe || '/';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
4
|
let {
|
|
13
5
|
children,
|
|
14
6
|
dialog = $bindable(),
|
|
@@ -52,8 +44,7 @@
|
|
|
52
44
|
const data = Object.fromEntries(new FormData(e.currentTarget));
|
|
53
45
|
submit(data)
|
|
54
46
|
.then(result => {
|
|
55
|
-
if (pageMode)
|
|
56
|
-
else dialog!.close();
|
|
47
|
+
if (!pageMode) dialog!.close();
|
|
57
48
|
})
|
|
58
49
|
.catch((e: any) => {
|
|
59
50
|
if (!e) error = 'An unknown error occurred';
|
package/lib/Login.svelte
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { loginByEmail } from '@axium/client/user';
|
|
3
3
|
import FormDialog from './FormDialog.svelte';
|
|
4
|
+
import redirectAfter from './auth_redirect.js';
|
|
4
5
|
|
|
5
6
|
let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
|
|
6
7
|
|
|
7
|
-
function submit(data: { email: string }) {
|
|
8
|
+
async function submit(data: { email: string }) {
|
|
8
9
|
if (typeof data.email != 'string') {
|
|
9
10
|
throw 'Tried to upload a file for an email. Huh?!';
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
await loginByEmail(data.email);
|
|
14
|
+
if (fullPage && redirectAfter) location.href = redirectAfter;
|
|
13
15
|
}
|
|
14
16
|
</script>
|
|
15
17
|
|
package/lib/Register.svelte
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { register } from '@axium/client/user';
|
|
3
3
|
import FormDialog from './FormDialog.svelte';
|
|
4
|
+
import redirectAfter from './auth_redirect.js';
|
|
4
5
|
|
|
5
6
|
let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
|
|
7
|
+
|
|
8
|
+
async function submit(data: Record<string, FormDataEntryValue>) {
|
|
9
|
+
await register(data);
|
|
10
|
+
if (fullPage && redirectAfter) location.href = redirectAfter;
|
|
11
|
+
}
|
|
6
12
|
</script>
|
|
7
13
|
|
|
8
|
-
<FormDialog bind:dialog submitText="Register" submit
|
|
14
|
+
<FormDialog bind:dialog submitText="Register" {submit} pageMode={fullPage}>
|
|
9
15
|
<div>
|
|
10
16
|
<label for="name">Display Name</label>
|
|
11
17
|
<input name="name" type="text" required />
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getCurrentSession } from '@axium/client/user';
|
|
2
|
+
|
|
3
|
+
function resolveRedirect(): string | false {
|
|
4
|
+
const url = new URL(location.href);
|
|
5
|
+
const maybe = url.searchParams.get('after');
|
|
6
|
+
if (!['/login', '/register'].includes(url.pathname)) return false;
|
|
7
|
+
if (!maybe || maybe == url.pathname) return '/';
|
|
8
|
+
|
|
9
|
+
if (maybe[0] != '/' || maybe[1] == '/') {
|
|
10
|
+
console.error('Ignoring potentially malicious redirect:', maybe);
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const redirect = new URL(maybe, location.origin);
|
|
15
|
+
|
|
16
|
+
return redirect.pathname + redirect.search || '/';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const redirect = resolveRedirect();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (!redirect) throw 'No redirect';
|
|
23
|
+
// Auto-redirect if already logged in.
|
|
24
|
+
const session = await getCurrentSession();
|
|
25
|
+
if (session) location.href = redirect;
|
|
26
|
+
} catch {}
|
|
27
|
+
|
|
28
|
+
export default redirect;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/james-pre/axium/issues"
|
|
17
17
|
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"axium-client": "dist/cli/index.js",
|
|
20
|
+
"axc": "dist/cli/index.js"
|
|
21
|
+
},
|
|
18
22
|
"type": "module",
|
|
19
23
|
"main": "dist/index.js",
|
|
20
24
|
"types": "dist/index.d.ts",
|
|
@@ -22,7 +26,8 @@
|
|
|
22
26
|
"assets",
|
|
23
27
|
"dist",
|
|
24
28
|
"lib",
|
|
25
|
-
"styles"
|
|
29
|
+
"styles",
|
|
30
|
+
"axium-client.service"
|
|
26
31
|
],
|
|
27
32
|
"exports": {
|
|
28
33
|
".": "./dist/index.js",
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
"build": "tsc"
|
|
36
41
|
},
|
|
37
42
|
"peerDependencies": {
|
|
38
|
-
"@axium/core": ">=0.
|
|
43
|
+
"@axium/core": ">=0.9.0",
|
|
39
44
|
"utilium": "^2.3.8",
|
|
40
45
|
"zod": "^4.0.5",
|
|
41
46
|
"svelte": "^5.36.0"
|