@aerogel/plugin-solid 0.0.1-next.0d1cf82f9dfbe1d3e7ec5644f85f97a43c8c2334
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/InruptAuthenticator.lazy-BvpQLGoj.js +9 -0
- package/dist/InruptAuthenticator.lazy-BvpQLGoj.js.map +1 -0
- package/dist/LegacyAuthenticator.lazy-ChFFqP14.js +5 -0
- package/dist/LegacyAuthenticator.lazy-ChFFqP14.js.map +1 -0
- package/dist/aerogel-plugin-solid.d.ts +309 -0
- package/dist/aerogel-plugin-solid.js +683 -0
- package/dist/aerogel-plugin-solid.js.map +1 -0
- package/package.json +49 -0
- package/src/auth/Authenticator.ts +123 -0
- package/src/auth/authenticators/InruptAuthenticator.lazy.ts +1 -0
- package/src/auth/authenticators/InruptAuthenticator.ts +103 -0
- package/src/auth/authenticators/LegacyAuthenticator.lazy.ts +1 -0
- package/src/auth/authenticators/LegacyAuthenticator.ts +80 -0
- package/src/auth/index.ts +40 -0
- package/src/components/AGSolidImage.vue +33 -0
- package/src/components/AGSolidStatus.ts +5 -0
- package/src/components/AGSolidStatus.vue +68 -0
- package/src/components/AGSolidUserAvatar.vue +35 -0
- package/src/components/index.ts +4 -0
- package/src/errors/AuthenticationCancelledError.ts +3 -0
- package/src/errors/AuthenticationFailedError.ts +22 -0
- package/src/errors/ContainerAlreadyInUse.ts +11 -0
- package/src/errors/index.ts +3 -0
- package/src/forms/validation.ts +11 -0
- package/src/index.test.ts +24 -0
- package/src/index.ts +65 -0
- package/src/services/Solid.state.ts +56 -0
- package/src/services/Solid.ts +549 -0
- package/src/testing/setup.ts +11 -0
- package/src/types/env.d.ts +1 -0
- package/src/types/shims.d.ts +7 -0
- package/src/types/solid-auth-client.d.ts +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<slot v-if="$solid.loginStale" name="login-stale">
|
|
4
|
+
<AGMarkdown :text="$td('solid.loginStale', 'This is taking too long...')" />
|
|
5
|
+
</slot>
|
|
6
|
+
|
|
7
|
+
<slot v-if="$solid.loginOngoing" name="login-ongoing">
|
|
8
|
+
<AGMarkdown :text="$td('ui.loading', 'Loading...')" />
|
|
9
|
+
</slot>
|
|
10
|
+
|
|
11
|
+
<slot v-else-if="$solid.isLoggedIn()" name="logged-in" :session="$solid.session">
|
|
12
|
+
<div class="flex flex-col gap-3">
|
|
13
|
+
<AGMarkdown
|
|
14
|
+
:text="
|
|
15
|
+
$td('solid.loggedIn', 'You are logged in as [{userName}]({userWebId})', {
|
|
16
|
+
userName: $solid.user.name ?? $solid.user.webId,
|
|
17
|
+
userWebId: $solid.user.webId,
|
|
18
|
+
})
|
|
19
|
+
"
|
|
20
|
+
/>
|
|
21
|
+
<AGButton @click="$solid.logout()">
|
|
22
|
+
{{ $td('solid.logout', 'Logout') }}
|
|
23
|
+
</AGButton>
|
|
24
|
+
</div>
|
|
25
|
+
</slot>
|
|
26
|
+
|
|
27
|
+
<slot v-else name="logged-out">
|
|
28
|
+
<div class="flex flex-col">
|
|
29
|
+
<AGForm :form="form" class="flex" @submit="$solid.login(form.url)">
|
|
30
|
+
<AGInput
|
|
31
|
+
name="url"
|
|
32
|
+
:aria-label="$td('solid.url', 'Login url')"
|
|
33
|
+
:placeholder="$td('solid.placeholder', 'https://...')"
|
|
34
|
+
/>
|
|
35
|
+
<AGButton v-if="showDevLogin" submit @click="form.url = 'dev'">
|
|
36
|
+
{{ $td('solid.devLogin', 'Dev login') }}
|
|
37
|
+
</AGButton>
|
|
38
|
+
<AGButton v-else submit>
|
|
39
|
+
{{ $td('solid.login', 'Login') }}
|
|
40
|
+
</AGButton>
|
|
41
|
+
</AGForm>
|
|
42
|
+
<AGButton v-if="$solid.wasLoggedIn()" class="mt-3" @click="$solid.reconnect({ force: true })">
|
|
43
|
+
{{ $td('solid.reconnect', 'Reconnect') }}
|
|
44
|
+
</AGButton>
|
|
45
|
+
<p
|
|
46
|
+
v-if="$solid.previousSession?.error || $solid.loginStartupError"
|
|
47
|
+
class="mt-1 self-center text-sm text-red-800"
|
|
48
|
+
>
|
|
49
|
+
<AGMarkdown :text="$td('solid.previousLoginError', 'Previous login attempt failed')" inline />
|
|
50
|
+
<AGLink @click="$errors.inspect($solid.previousSession?.error || $solid.loginStartupError)">
|
|
51
|
+
({{ $td('errors.viewDetails', 'View details') }})
|
|
52
|
+
</AGLink>
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</slot>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup lang="ts">
|
|
60
|
+
import { AGButton, AGForm, AGInput, AGLink, AGMarkdown, requiredStringInput, useForm } from '@aerogel/core';
|
|
61
|
+
import { isDevelopment } from '@noeldemartin/utils';
|
|
62
|
+
import { computed } from 'vue';
|
|
63
|
+
|
|
64
|
+
const form = useForm({ url: requiredStringInput() });
|
|
65
|
+
const showDevLogin = computed(
|
|
66
|
+
() => isDevelopment() && (!form.url || form.url === 'dev' || form.url.trim().length === 0),
|
|
67
|
+
);
|
|
68
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative flex items-center justify-center rounded-full bg-gray-200">
|
|
3
|
+
<AGSolidImage
|
|
4
|
+
v-if="$solid.user?.avatarUrl"
|
|
5
|
+
:src="$solid.user?.avatarUrl"
|
|
6
|
+
alt=""
|
|
7
|
+
class="absolute inset-0 h-full w-full rounded-full"
|
|
8
|
+
/>
|
|
9
|
+
<span v-else-if="userInitials" class="text-sm font-semibold tracking-wider text-gray-500" aria-hidden="true">
|
|
10
|
+
{{ userInitials }}
|
|
11
|
+
</span>
|
|
12
|
+
<IconAnonymous v-else class="h-6 w-6 text-gray-500" />
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import IconAnonymous from '~icons/ooui/user-anonymous';
|
|
18
|
+
|
|
19
|
+
import { computed } from 'vue';
|
|
20
|
+
|
|
21
|
+
import Solid from '@aerogel/plugin-solid/services/Solid';
|
|
22
|
+
|
|
23
|
+
import AGSolidImage from './AGSolidImage.vue';
|
|
24
|
+
|
|
25
|
+
const userInitials = computed(() => {
|
|
26
|
+
const name = Solid.user?.name ?? '';
|
|
27
|
+
|
|
28
|
+
return name
|
|
29
|
+
.trim()
|
|
30
|
+
.split(' ')
|
|
31
|
+
.map((part) => part[0]?.toUpperCase())
|
|
32
|
+
.join('')
|
|
33
|
+
.slice(0, 3);
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as AGSolidImage } from './AGSolidImage.vue';
|
|
2
|
+
export { default as AGSolidStatus } from './AGSolidStatus.vue';
|
|
3
|
+
export { default as AGSolidUserAvatar } from './AGSolidUserAvatar.vue';
|
|
4
|
+
export type { IAGSolidStatusLoggedInSlotProps } from './AGSolidStatus';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
|
+
import type { JSErrorOptions } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
function getErrorMessage(message: string, description?: string | null): string {
|
|
5
|
+
if (!description) {
|
|
6
|
+
return message;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return `${message} (${description})`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default class AuthenticationFailedError extends JSError {
|
|
13
|
+
|
|
14
|
+
public readonly description?: string | null;
|
|
15
|
+
|
|
16
|
+
constructor(message: string, description?: string | null, options?: JSErrorOptions) {
|
|
17
|
+
super(getErrorMessage(message), options);
|
|
18
|
+
|
|
19
|
+
this.description = description;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { JSError } from '@noeldemartin/utils';
|
|
2
|
+
import type { JSErrorOptions } from '@noeldemartin/utils';
|
|
3
|
+
import type { SolidContainer } from 'soukai-solid';
|
|
4
|
+
|
|
5
|
+
export default class ContainerAlreadyInUse extends JSError {
|
|
6
|
+
|
|
7
|
+
constructor(container: SolidContainer, options?: JSErrorOptions) {
|
|
8
|
+
super(`Container at ${container.url} is already in use`, options);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { default as AuthenticationCancelledError } from '@aerogel/plugin-solid/errors/AuthenticationCancelledError';
|
|
2
|
+
export { default as AuthenticationFailedError } from '@aerogel/plugin-solid/errors/AuthenticationFailedError';
|
|
3
|
+
export { default as ContainerAlreadyInUse } from '@aerogel/plugin-solid/errors/ContainerAlreadyInUse';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineFormValidationRule } from '@aerogel/core';
|
|
2
|
+
|
|
3
|
+
export function defineFormValidationRules(): void {
|
|
4
|
+
defineFormValidationRule<string>('container_url', (value) => {
|
|
5
|
+
if (value.endsWith('/')) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return 'containerEndingSlashMissing';
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { Errors, bootServices } from '@aerogel/core';
|
|
4
|
+
import { Metadata } from 'soukai-solid';
|
|
5
|
+
import { requireBootedModel } from 'soukai';
|
|
6
|
+
|
|
7
|
+
import solid from './index';
|
|
8
|
+
|
|
9
|
+
describe('Solid plugin', () => {
|
|
10
|
+
|
|
11
|
+
it('Initializes models', async () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const app = createApp({});
|
|
14
|
+
|
|
15
|
+
await bootServices(app, { $errors: Errors });
|
|
16
|
+
|
|
17
|
+
// Act
|
|
18
|
+
await solid().install(app, {});
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(requireBootedModel('Metadata')).toEqual(Metadata);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Events, bootServices, registerErrorHandler } from '@aerogel/core';
|
|
2
|
+
import { bootSolidModels } from 'soukai-solid';
|
|
3
|
+
import type { Plugin } from '@aerogel/core';
|
|
4
|
+
import type { SolidStore, SolidUserProfile } from '@noeldemartin/solid-utils';
|
|
5
|
+
|
|
6
|
+
import Solid from '@aerogel/plugin-solid/services/Solid';
|
|
7
|
+
import { DEFAULT_STATE } from '@aerogel/plugin-solid/services/Solid.state';
|
|
8
|
+
import { defineFormValidationRules } from '@aerogel/plugin-solid/forms/validation';
|
|
9
|
+
import {
|
|
10
|
+
authenticators as baseAuthenticators,
|
|
11
|
+
getAuthenticator,
|
|
12
|
+
registerAuthenticators,
|
|
13
|
+
setDefaultAuthenticator,
|
|
14
|
+
} from '@aerogel/plugin-solid/auth';
|
|
15
|
+
import { AuthenticationFailedError } from '@aerogel/plugin-solid/errors';
|
|
16
|
+
import type Authenticator from '@aerogel/plugin-solid/auth/Authenticator';
|
|
17
|
+
import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
|
|
18
|
+
|
|
19
|
+
const services = { $solid: Solid };
|
|
20
|
+
|
|
21
|
+
export * from './auth';
|
|
22
|
+
export * from './components';
|
|
23
|
+
export * from './errors';
|
|
24
|
+
export * from './services/Solid';
|
|
25
|
+
export { Solid };
|
|
26
|
+
|
|
27
|
+
export interface Options {
|
|
28
|
+
autoReconnect?: boolean;
|
|
29
|
+
authenticators?: Record<string, Authenticator>;
|
|
30
|
+
defaultAuthenticator?: AuthenticatorName;
|
|
31
|
+
onUserProfileLoaded?(user: SolidUserProfile, store: SolidStore): Promise<unknown> | unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SolidServices = typeof services;
|
|
35
|
+
|
|
36
|
+
export default function solid(options: Options = {}): Plugin {
|
|
37
|
+
return {
|
|
38
|
+
async install(app) {
|
|
39
|
+
bootSolidModels();
|
|
40
|
+
registerAuthenticators({ ...baseAuthenticators, ...(options.authenticators ?? {}) });
|
|
41
|
+
defineFormValidationRules();
|
|
42
|
+
setDefaultAuthenticator(getAuthenticator(options.defaultAuthenticator ?? 'inrupt'));
|
|
43
|
+
registerErrorHandler((error) => {
|
|
44
|
+
if (!(error instanceof AuthenticationFailedError)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return error.description ? `${error.message} (${error.description})` : error.message;
|
|
48
|
+
});
|
|
49
|
+
if (typeof options.autoReconnect === 'boolean') {
|
|
50
|
+
DEFAULT_STATE.autoReconnect = options.autoReconnect;
|
|
51
|
+
Solid.hasPersistedState() || (Solid.autoReconnect = options.autoReconnect);
|
|
52
|
+
}
|
|
53
|
+
if (options.onUserProfileLoaded) {
|
|
54
|
+
Events.on('solid:user-profile-loaded', ([user, store]) => {
|
|
55
|
+
options.onUserProfileLoaded?.(user, store);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
await bootServices(app, services);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare module '@aerogel/core' {
|
|
64
|
+
interface Services extends SolidServices {}
|
|
65
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineServiceState } from '@aerogel/core';
|
|
2
|
+
import type { ErrorSource } from '@aerogel/core';
|
|
3
|
+
import type { Fetch, SolidUserProfile } from '@noeldemartin/solid-utils';
|
|
4
|
+
|
|
5
|
+
import type { AuthSession } from '@aerogel/plugin-solid/auth/Authenticator';
|
|
6
|
+
import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_STATE = { autoReconnect: true };
|
|
9
|
+
|
|
10
|
+
export default defineServiceState({
|
|
11
|
+
name: 'solid',
|
|
12
|
+
persist: ['autoReconnect', 'dismissed', 'previousSession', 'profiles', 'staleProfiles'],
|
|
13
|
+
initialState: {
|
|
14
|
+
autoReconnect: DEFAULT_STATE.autoReconnect,
|
|
15
|
+
dismissed: false,
|
|
16
|
+
ignorePreviousSessionError: false,
|
|
17
|
+
loginStartupError: null as ErrorSource | null,
|
|
18
|
+
loginOngoing: false,
|
|
19
|
+
preferredAuthenticator: null as AuthenticatorName | null,
|
|
20
|
+
previousSession: null as {
|
|
21
|
+
profile?: SolidUserProfile;
|
|
22
|
+
authenticator: AuthenticatorName;
|
|
23
|
+
loginUrl: string;
|
|
24
|
+
error: ErrorSource | null;
|
|
25
|
+
} | null,
|
|
26
|
+
profiles: {} as Record<string, SolidUserProfile>,
|
|
27
|
+
session: null as AuthSession | null,
|
|
28
|
+
loginStale: false,
|
|
29
|
+
staleProfiles: [] as string[],
|
|
30
|
+
},
|
|
31
|
+
serialize(state) {
|
|
32
|
+
if (state.previousSession?.error instanceof Error) {
|
|
33
|
+
state.previousSession.error = state.previousSession.error.message;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return state;
|
|
37
|
+
},
|
|
38
|
+
computed: {
|
|
39
|
+
user: (state) => state.session?.user ?? state.previousSession?.profile ?? null,
|
|
40
|
+
authenticator: (state) => state.session?.authenticator ?? null,
|
|
41
|
+
fetch(): Fetch {
|
|
42
|
+
return this.authenticator?.getAuthenticatedFetch() ?? globalThis.fetch.bind(globalThis);
|
|
43
|
+
},
|
|
44
|
+
error(state): ErrorSource | null {
|
|
45
|
+
if (state.loginStartupError) {
|
|
46
|
+
return state.loginStartupError ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (state.ignorePreviousSessionError) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return state.previousSession?.error ?? null;
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|