@axium/client 0.8.0 → 0.9.1
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 +14 -1
- package/dist/access.js +0 -2
- package/lib/AccessControlDialog.svelte +27 -32
- package/lib/Dialog.svelte +4 -2
- package/lib/FormDialog.svelte +3 -2
- package/lib/Login.svelte +2 -1
- package/lib/Logout.svelte +4 -3
- package/lib/Popover.svelte +7 -1
- package/lib/Register.svelte +2 -1
- package/lib/SessionList.svelte +4 -7
- package/lib/UserMenu.svelte +7 -7
- package/lib/auth_redirect.ts +13 -10
- package/package.json +2 -2
package/assets/styles.css
CHANGED
|
@@ -151,7 +151,8 @@ pre {
|
|
|
151
151
|
background-color: hsl(from var(--bg-menu) h s l / 95%);
|
|
152
152
|
border-radius: 0.5em;
|
|
153
153
|
padding: 0.25em;
|
|
154
|
-
|
|
154
|
+
margin: 0;
|
|
155
|
+
inset: auto;
|
|
155
156
|
display: flex;
|
|
156
157
|
flex-direction: column;
|
|
157
158
|
gap: 0.1em;
|
|
@@ -166,3 +167,15 @@ pre {
|
|
|
166
167
|
a:has(button, div) {
|
|
167
168
|
display: contents;
|
|
168
169
|
}
|
|
170
|
+
|
|
171
|
+
.version {
|
|
172
|
+
font-family: monospace;
|
|
173
|
+
font-size: 0.9em;
|
|
174
|
+
color: #aaa;
|
|
175
|
+
margin-left: 1em;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.version::before {
|
|
179
|
+
content: 'v';
|
|
180
|
+
color: #888;
|
|
181
|
+
}
|
package/dist/access.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { fetchAPI } from './requests.js';
|
|
2
2
|
export async function setACL(itemType, itemId, data) {
|
|
3
|
-
if ('public' in data)
|
|
4
|
-
data.public = parseInt(data.public.toString());
|
|
5
3
|
return await fetchAPI('POST', 'acl/:itemType/:itemId', data, itemType, itemId);
|
|
6
4
|
}
|
|
7
5
|
export async function getACL(itemType, itemId) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { getACL, setACL } from '@axium/client/access';
|
|
3
|
+
import { userInfo } from '@axium/client/user';
|
|
2
4
|
import type { AccessMap, User } from '@axium/core';
|
|
3
|
-
import {
|
|
4
|
-
import type { Entries } from 'utilium';
|
|
5
|
+
import { pickPermissions, type AccessControl, type AccessControllable } from '@axium/core/access';
|
|
5
6
|
import FormDialog from './FormDialog.svelte';
|
|
6
7
|
import UserCard from './UserCard.svelte';
|
|
7
|
-
import
|
|
8
|
-
import { getACL, setACL } from '@axium/client/access';
|
|
8
|
+
import Icon from './Icon.svelte';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
editable: boolean;
|
|
@@ -17,10 +17,6 @@
|
|
|
17
17
|
let { item = $bindable(), itemType, editable, dialog = $bindable(), acl = $bindable(item?.acl) }: Props = $props();
|
|
18
18
|
|
|
19
19
|
if (!acl && item) getACL(itemType, item.id).then(fetched => (acl = item.acl = fetched));
|
|
20
|
-
|
|
21
|
-
const permEntries = Object.entries(permissionNames) as any as Entries<typeof permissionNames>;
|
|
22
|
-
|
|
23
|
-
const publicPerm = $derived(permissionNames[item?.publicPermission || 0]);
|
|
24
20
|
</script>
|
|
25
21
|
|
|
26
22
|
<FormDialog
|
|
@@ -49,34 +45,33 @@
|
|
|
49
45
|
|
|
50
46
|
{#each acl ?? [] as control}
|
|
51
47
|
<div class="AccessControl">
|
|
52
|
-
{#if
|
|
53
|
-
{:else}
|
|
48
|
+
{#if control.user}
|
|
54
49
|
<UserCard user={control.user} />
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
{:else if control.role}
|
|
51
|
+
<span class="icon-text">
|
|
52
|
+
<Icon i="at" />
|
|
53
|
+
<span>{control.role}</span>
|
|
54
|
+
</span>
|
|
55
|
+
<!-- {:else if control.tag} -->
|
|
56
|
+
{:else}
|
|
57
|
+
<i>Unknown</i>
|
|
58
|
+
{/if}
|
|
59
|
+
{#if editable}
|
|
60
|
+
<select name={control.userId ?? (control.role ? '@' + control.role : 'public')} multiple>
|
|
61
|
+
{#each Object.entries(pickPermissions(control)) as [key, value]}
|
|
62
|
+
<option value={key} selected={!!value}>{key}</option>
|
|
63
|
+
{/each}
|
|
64
|
+
</select>
|
|
65
|
+
{:else}
|
|
66
|
+
<span
|
|
67
|
+
>{Object.entries(pickPermissions(control))
|
|
68
|
+
.filter(([, value]) => value)
|
|
69
|
+
.map(([key]) => key)
|
|
70
|
+
.join(', ')}</span
|
|
71
|
+
>
|
|
64
72
|
{/if}
|
|
65
73
|
</div>
|
|
66
74
|
{/each}
|
|
67
|
-
|
|
68
|
-
<div class="AccessControl public">
|
|
69
|
-
<strong>Public Access</strong>
|
|
70
|
-
{#if editable && item}
|
|
71
|
-
<select name="public">
|
|
72
|
-
{#each permEntries as [key, name]}
|
|
73
|
-
<option value={key} selected={key == item.publicPermission}>{name}</option>
|
|
74
|
-
{/each}
|
|
75
|
-
</select>
|
|
76
|
-
{:else}
|
|
77
|
-
<span>{publicPerm}</span>
|
|
78
|
-
{/if}
|
|
79
|
-
</div>
|
|
80
75
|
</FormDialog>
|
|
81
76
|
|
|
82
77
|
<style>
|
package/lib/Dialog.svelte
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
let { children, dialog = $bindable(), ...rest }: { children(): any; dialog?: HTMLDialogElement } & HTMLDialogAttributes = $props();
|
|
3
5
|
</script>
|
|
4
6
|
|
|
5
7
|
<dialog bind:this={dialog} {...rest}>
|
package/lib/FormDialog.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Dialog from './Dialog.svelte';
|
|
3
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
children,
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
submitDanger?: boolean;
|
|
27
28
|
header?(): any;
|
|
28
29
|
footer?(): any;
|
|
29
|
-
} = $props();
|
|
30
|
+
} & HTMLDialogAttributes = $props();
|
|
30
31
|
|
|
31
32
|
let error = $state<string>();
|
|
32
33
|
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
if (pageMode) dialog!.showModal();
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
function onclose(e:
|
|
38
|
+
function onclose(e: Event) {
|
|
38
39
|
e.preventDefault();
|
|
39
40
|
cancel();
|
|
40
41
|
}
|
package/lib/Login.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { loginByEmail } from '@axium/client/user';
|
|
3
3
|
import FormDialog from './FormDialog.svelte';
|
|
4
|
-
import
|
|
4
|
+
import authRedirect from './auth_redirect.js';
|
|
5
5
|
|
|
6
6
|
let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
|
|
7
7
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
await loginByEmail(data.email);
|
|
14
|
+
const redirectAfter = await authRedirect();
|
|
14
15
|
if (fullPage && redirectAfter) location.href = redirectAfter;
|
|
15
16
|
}
|
|
16
17
|
</script>
|
package/lib/Logout.svelte
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
import { logoutCurrentSession } from '@axium/client/user';
|
|
3
3
|
import FormDialog from './FormDialog.svelte';
|
|
4
4
|
|
|
5
|
-
let {
|
|
5
|
+
let { fullPage = false }: { fullPage?: boolean } = $props();
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<FormDialog
|
|
9
9
|
pageMode={fullPage}
|
|
10
|
-
|
|
10
|
+
id="logout"
|
|
11
11
|
submitText="Log Out"
|
|
12
12
|
submit={() => logoutCurrentSession().then(() => (window.location.href = '/'))}
|
|
13
13
|
>
|
|
14
14
|
<p>Are you sure you want to log out?</p>
|
|
15
15
|
{#if fullPage}
|
|
16
16
|
<button
|
|
17
|
+
command="close"
|
|
18
|
+
commandfor="logout"
|
|
17
19
|
onclick={e => {
|
|
18
20
|
e.preventDefault();
|
|
19
|
-
dialog!.close();
|
|
20
21
|
history.back();
|
|
21
22
|
}}>Take me back</button
|
|
22
23
|
>
|
package/lib/Popover.svelte
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
function onclick(e: MouseEvent) {
|
|
8
8
|
e.stopPropagation();
|
|
9
|
-
|
|
9
|
+
// @ts-expect-error 2345
|
|
10
|
+
popover?.togglePopover({ source: e.currentTarget });
|
|
10
11
|
}
|
|
11
12
|
</script>
|
|
12
13
|
|
|
@@ -31,6 +32,11 @@
|
|
|
31
32
|
cursor: pointer;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
.popover-toggle + [popover] {
|
|
36
|
+
position-area: bottom right;
|
|
37
|
+
position-try: most-width flip-inline;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
[popover] :global(.menu-item) {
|
|
35
41
|
display: inline-flex;
|
|
36
42
|
align-items: center;
|
package/lib/Register.svelte
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { register } from '@axium/client/user';
|
|
3
3
|
import FormDialog from './FormDialog.svelte';
|
|
4
|
-
import
|
|
4
|
+
import authRedirect from './auth_redirect.js';
|
|
5
5
|
|
|
6
6
|
let { dialog = $bindable(), fullPage = false }: { dialog?: HTMLDialogElement; fullPage?: boolean } = $props();
|
|
7
7
|
|
|
8
8
|
async function submit(data: Record<string, FormDataEntryValue>) {
|
|
9
9
|
await register(data);
|
|
10
|
+
const redirectAfter = await authRedirect();
|
|
10
11
|
if (fullPage && redirectAfter) location.href = redirectAfter;
|
|
11
12
|
}
|
|
12
13
|
</script>
|
package/lib/SessionList.svelte
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
user,
|
|
11
11
|
redirectAfterLogoutAll = false,
|
|
12
12
|
}: { sessions: Session[]; currentSession?: Session; user: User; redirectAfterLogoutAll?: boolean } = $props();
|
|
13
|
-
|
|
14
|
-
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
15
13
|
</script>
|
|
16
14
|
|
|
17
15
|
{#each sessions as session}
|
|
@@ -27,15 +25,14 @@
|
|
|
27
25
|
</p>
|
|
28
26
|
<p>Created {session.created.toLocaleString()}</p>
|
|
29
27
|
<p>Expires {session.expires.toLocaleString()}</p>
|
|
30
|
-
<button style:display="contents"
|
|
28
|
+
<button style:display="contents" command="show-modal" commandfor={'logout-session:' + session.id}>
|
|
31
29
|
<Icon i="right-from-bracket" --size="16px" />
|
|
32
30
|
</button>
|
|
33
31
|
</div>
|
|
34
32
|
<FormDialog
|
|
35
|
-
|
|
33
|
+
id={'logout-session:' + session.id}
|
|
36
34
|
submit={async () => {
|
|
37
35
|
await logout(user.id, session.id);
|
|
38
|
-
dialogs['logout#' + session.id].remove();
|
|
39
36
|
sessions.splice(sessions.indexOf(session), 1);
|
|
40
37
|
if (session.id == currentSession?.id) window.location.href = '/';
|
|
41
38
|
}}
|
|
@@ -45,10 +42,10 @@
|
|
|
45
42
|
</FormDialog>
|
|
46
43
|
{/each}
|
|
47
44
|
<span>
|
|
48
|
-
<button
|
|
45
|
+
<button command="show-modal" commandfor="logout-all" class="danger">Logout All</button>
|
|
49
46
|
</span>
|
|
50
47
|
<FormDialog
|
|
51
|
-
|
|
48
|
+
id="logout-all"
|
|
52
49
|
submit={() => logoutAll(user.id).then(() => (redirectAfterLogoutAll ? (window.location.href = '/') : null))}
|
|
53
50
|
submitText="Logout All Sessions"
|
|
54
51
|
submitDanger
|
package/lib/UserMenu.svelte
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
import Logout from './Logout.svelte';
|
|
8
8
|
|
|
9
9
|
const { user }: { user: Partial<User> } = $props();
|
|
10
|
-
|
|
11
|
-
let logout = $state<HTMLDialogElement>()!;
|
|
12
10
|
</script>
|
|
13
11
|
|
|
14
12
|
<Popover>
|
|
@@ -52,13 +50,15 @@
|
|
|
52
50
|
<i>Couldn't load apps.</i>
|
|
53
51
|
{/await}
|
|
54
52
|
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
<button style:display="contents" command="show-modal" commandfor="logout">
|
|
54
|
+
<span class="menu-item logout">
|
|
55
|
+
<Icon i="right-from-bracket" --size="1.5em" --fill="hsl(0 33 var(--fg-light))" />
|
|
56
|
+
<span>Logout</span>
|
|
57
|
+
</span>
|
|
58
|
+
</button>
|
|
59
59
|
</Popover>
|
|
60
60
|
|
|
61
|
-
<Logout
|
|
61
|
+
<Logout />
|
|
62
62
|
|
|
63
63
|
<style>
|
|
64
64
|
img {
|
package/lib/auth_redirect.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { getCurrentSession } from '@axium/client/user';
|
|
|
2
2
|
|
|
3
3
|
function resolveRedirect(): string | false {
|
|
4
4
|
const url = new URL(location.href);
|
|
5
|
+
if (!['/login', '/register'].includes(url.pathname)) {
|
|
6
|
+
console.warn('Not on login or register page, not redirecting:', url.pathname);
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
5
9
|
const maybe = url.searchParams.get('after');
|
|
6
|
-
if (!['/login', '/register'].includes(url.pathname)) return false;
|
|
7
10
|
if (!maybe || maybe == url.pathname) return '/';
|
|
8
11
|
|
|
9
12
|
if (maybe[0] != '/' || maybe[1] == '/') {
|
|
@@ -16,13 +19,13 @@ function resolveRedirect(): string | false {
|
|
|
16
19
|
return redirect.pathname + redirect.search || '/';
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
export default async function authRedirect() {
|
|
23
|
+
const redirect = resolveRedirect();
|
|
20
24
|
|
|
21
|
-
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} catch {}
|
|
27
|
-
|
|
28
|
-
export default redirect;
|
|
25
|
+
try {
|
|
26
|
+
if (!redirect) throw 'No redirect';
|
|
27
|
+
// Auto-redirect if already logged in.
|
|
28
|
+
const session = await getCurrentSession();
|
|
29
|
+
if (session) location.href = redirect;
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"build": "tsc"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@axium/core": ">=0.
|
|
43
|
+
"@axium/core": ">=0.12.0",
|
|
44
44
|
"utilium": "^2.3.8",
|
|
45
45
|
"zod": "^4.0.5",
|
|
46
46
|
"svelte": "^5.36.0"
|