@aerogel/plugin-solid 0.0.1 → 0.1.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/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 +120 -92
- package/dist/aerogel-plugin-solid.js +724 -0
- package/dist/aerogel-plugin-solid.js.map +1 -0
- package/package.json +35 -28
- package/src/auth/Authenticator.ts +10 -8
- package/src/auth/authenticators/InruptAuthenticator.lazy.ts +1 -0
- package/src/auth/authenticators/InruptAuthenticator.ts +41 -12
- package/src/auth/authenticators/LegacyAuthenticator.lazy.ts +1 -0
- package/src/auth/authenticators/LegacyAuthenticator.ts +80 -0
- package/src/auth/index.ts +3 -1
- package/src/components/SolidAvatar.vue +35 -0
- package/src/components/SolidImage.vue +33 -0
- package/src/components/SolidLogin.vue +141 -0
- package/src/components/index.ts +3 -4
- package/src/errors/AuthenticationFailedError.ts +22 -0
- package/src/errors/index.ts +3 -4
- package/src/forms/validation.ts +11 -0
- package/src/index.css +1 -0
- package/src/index.test.ts +24 -0
- package/src/index.ts +65 -0
- package/src/services/Solid.state.ts +12 -16
- package/src/services/Solid.ts +317 -139
- package/src/testing/setup.ts +11 -0
- package/src/types/env.d.ts +1 -0
- package/src/types/solid-auth-client.d.ts +21 -0
- package/.eslintrc.js +0 -7
- package/dist/aerogel-plugin-solid.cjs.js +0 -2
- package/dist/aerogel-plugin-solid.cjs.js.map +0 -1
- package/dist/aerogel-plugin-solid.esm.js +0 -2
- package/dist/aerogel-plugin-solid.esm.js.map +0 -1
- package/noeldemartin.config.js +0 -2
- package/src/components/AGSolidStatus.ts +0 -5
- package/src/components/AGSolidStatus.vue +0 -60
- package/src/main.ts +0 -43
- package/tsconfig.json +0 -10
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Form :form :class="renderedClasses" @submit="submit">
|
|
3
|
+
<div
|
|
4
|
+
class="flex w-full flex-col items-center gap-2"
|
|
5
|
+
:class="{ 'md:flex-row': layout === 'horizontal' && !form.authenticator }"
|
|
6
|
+
>
|
|
7
|
+
<Input
|
|
8
|
+
v-if="showInput"
|
|
9
|
+
name="url"
|
|
10
|
+
:aria-label="$td('solid.logIn.label', 'Login url')"
|
|
11
|
+
:placeholder="$td('solid.logIn.placeholder', 'https://me.solidcommunity.net')"
|
|
12
|
+
class="w-full md:min-w-96"
|
|
13
|
+
/>
|
|
14
|
+
<Select
|
|
15
|
+
v-if="form.authenticator"
|
|
16
|
+
name="authenticator"
|
|
17
|
+
class="w-full max-w-full md:min-w-96"
|
|
18
|
+
label-class="sr-only"
|
|
19
|
+
options-class="md:w-96 w-full max-w-[calc(100vw-4rem)]"
|
|
20
|
+
:label="$td('solid.logIn.switchAuthenticatorLabel', 'Authentication method')"
|
|
21
|
+
:options="authenticatorOptions"
|
|
22
|
+
:render-option="renderAuthenticator"
|
|
23
|
+
/>
|
|
24
|
+
<div v-if="showDevLogin && !usingManualUrl" class="flex w-full justify-center gap-2">
|
|
25
|
+
<Button
|
|
26
|
+
variant="secondary"
|
|
27
|
+
class="group gap-0"
|
|
28
|
+
:title="$td('solid.logIn.manualUrl', 'Enter url')"
|
|
29
|
+
@click="usingManualUrl = true"
|
|
30
|
+
>
|
|
31
|
+
<IconLogin class="size-4" />
|
|
32
|
+
<span
|
|
33
|
+
v-measure.css="() => (measured = true)"
|
|
34
|
+
class="overflow-hidden pl-1 whitespace-nowrap"
|
|
35
|
+
:class="{
|
|
36
|
+
'opacity-0': !measured,
|
|
37
|
+
'w-0 transition-[width] md:group-hover:w-(--width)': measured,
|
|
38
|
+
}"
|
|
39
|
+
>{{ $td('solid.logIn.manualUrl', 'Enter url') }}</span>
|
|
40
|
+
</Button>
|
|
41
|
+
<Button
|
|
42
|
+
:disabled="noLoading && $solid.loginOngoing"
|
|
43
|
+
submit
|
|
44
|
+
class="whitespace-nowrap"
|
|
45
|
+
@click="form.url = 'dev'"
|
|
46
|
+
>
|
|
47
|
+
{{ $td('solid.logIn.dev', 'Log in to dev server') }}
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
<Button
|
|
51
|
+
v-else
|
|
52
|
+
:disabled="noLoading && $solid.loginOngoing"
|
|
53
|
+
submit
|
|
54
|
+
:class="renderedButtonClasses"
|
|
55
|
+
>
|
|
56
|
+
{{ $td('solid.logIn.submit', 'Log in') }}
|
|
57
|
+
</Button>
|
|
58
|
+
</div>
|
|
59
|
+
<Link
|
|
60
|
+
v-if="allowLegacyAuthenticator && showInput && !form.authenticator"
|
|
61
|
+
class="text-sm font-normal text-gray-700"
|
|
62
|
+
@click="form.authenticator = 'inrupt'"
|
|
63
|
+
>
|
|
64
|
+
{{ $td('solid.logIn.switchAuthenticator', "Can't log in? Try using a different authentication method") }}
|
|
65
|
+
</Link>
|
|
66
|
+
</Form>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
import IconLogin from '~icons/ic/baseline-log-in';
|
|
71
|
+
|
|
72
|
+
import {
|
|
73
|
+
App,
|
|
74
|
+
Button,
|
|
75
|
+
Form,
|
|
76
|
+
Input,
|
|
77
|
+
Link,
|
|
78
|
+
Select,
|
|
79
|
+
UI,
|
|
80
|
+
classes,
|
|
81
|
+
requiredStringInput,
|
|
82
|
+
stringInput,
|
|
83
|
+
translateWithDefault,
|
|
84
|
+
useForm,
|
|
85
|
+
} from '@aerogel/core';
|
|
86
|
+
import { computed, ref } from 'vue';
|
|
87
|
+
import type { HTMLAttributes } from 'vue';
|
|
88
|
+
|
|
89
|
+
import Solid from '@aerogel/plugin-solid/services/Solid';
|
|
90
|
+
import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
|
|
91
|
+
|
|
92
|
+
const AUTHENTICATOR_LABELS = {
|
|
93
|
+
inrupt: 'Log in using Inrupt\'s authentication library',
|
|
94
|
+
legacy: 'Log in using the legacy authentication library',
|
|
95
|
+
} satisfies Partial<Record<AuthenticatorName, string>>;
|
|
96
|
+
|
|
97
|
+
const {
|
|
98
|
+
class: rootClasses = '',
|
|
99
|
+
layout = 'horizontal',
|
|
100
|
+
buttonClass,
|
|
101
|
+
noLoading,
|
|
102
|
+
allowLegacyAuthenticator,
|
|
103
|
+
} = defineProps<{
|
|
104
|
+
class?: HTMLAttributes['class'];
|
|
105
|
+
buttonClass?: HTMLAttributes['class'];
|
|
106
|
+
layout?: 'vertical' | 'horizontal';
|
|
107
|
+
noLoading?: boolean;
|
|
108
|
+
allowLegacyAuthenticator?: boolean;
|
|
109
|
+
}>();
|
|
110
|
+
const form = useForm({
|
|
111
|
+
url: requiredStringInput(),
|
|
112
|
+
authenticator: stringInput(),
|
|
113
|
+
});
|
|
114
|
+
const authenticatorOptions = Object.keys(AUTHENTICATOR_LABELS) as (keyof typeof AUTHENTICATOR_LABELS)[];
|
|
115
|
+
const measured = ref(false);
|
|
116
|
+
const usingManualUrl = ref(false);
|
|
117
|
+
const showInput = computed(() => !showDevLogin.value || usingManualUrl.value);
|
|
118
|
+
const showDevLogin = computed(
|
|
119
|
+
() => App.development && (!form.url || form.url === 'dev' || form.url.trim().length === 0),
|
|
120
|
+
);
|
|
121
|
+
const renderedClasses = computed(() => classes('flex flex-col items-center gap-2 w-full md:w-auto', rootClasses));
|
|
122
|
+
const renderedButtonClasses = computed(() =>
|
|
123
|
+
classes('w-full whitespace-nowrap', { 'md:w-auto': layout === 'horizontal' && !form.authenticator }, buttonClass));
|
|
124
|
+
|
|
125
|
+
function renderAuthenticator(option: keyof typeof AUTHENTICATOR_LABELS) {
|
|
126
|
+
return translateWithDefault(
|
|
127
|
+
`solid.logIn.switchAuthenticator${option.charAt(0).toUpperCase() + option.slice(1)}`,
|
|
128
|
+
AUTHENTICATOR_LABELS[option],
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function submit() {
|
|
133
|
+
if (noLoading) {
|
|
134
|
+
await Solid.login(form.url, { authenticator: form.authenticator as AuthenticatorName });
|
|
135
|
+
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await UI.loading(translateWithDefault('solid.loginOngoing', 'Logging in...'), Solid.login(form.url));
|
|
140
|
+
}
|
|
141
|
+
</script>
|
package/src/components/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export { AGSolidStatus, IAGSolidStatusLoggedInSlotProps };
|
|
1
|
+
export { default as SolidAvatar } from './SolidAvatar.vue';
|
|
2
|
+
export { default as SolidImage } from './SolidImage.vue';
|
|
3
|
+
export { default as SolidLogin } from './SolidLogin.vue';
|
|
@@ -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
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export { AuthenticationCancelledError, ContainerAlreadyInUse };
|
|
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
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@source './';
|
|
@@ -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
|
+
}
|
|
@@ -2,23 +2,25 @@ import { defineServiceState } from '@aerogel/core';
|
|
|
2
2
|
import type { ErrorSource } from '@aerogel/core';
|
|
3
3
|
import type { Fetch, SolidUserProfile } from '@noeldemartin/solid-utils';
|
|
4
4
|
|
|
5
|
-
import type { AuthSession } from '
|
|
6
|
-
import type { AuthenticatorName } from '
|
|
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 };
|
|
7
9
|
|
|
8
10
|
export default defineServiceState({
|
|
9
11
|
name: 'solid',
|
|
10
12
|
persist: ['autoReconnect', 'dismissed', 'previousSession', 'profiles', 'staleProfiles'],
|
|
11
13
|
initialState: {
|
|
12
|
-
autoReconnect:
|
|
14
|
+
autoReconnect: DEFAULT_STATE.autoReconnect,
|
|
13
15
|
dismissed: false,
|
|
14
16
|
ignorePreviousSessionError: false,
|
|
15
|
-
|
|
17
|
+
loginStartupError: null as ErrorSource | null,
|
|
16
18
|
loginOngoing: false,
|
|
17
19
|
preferredAuthenticator: null as AuthenticatorName | null,
|
|
18
20
|
previousSession: null as {
|
|
21
|
+
profile?: SolidUserProfile;
|
|
19
22
|
authenticator: AuthenticatorName;
|
|
20
23
|
loginUrl: string;
|
|
21
|
-
avatarUrl?: string;
|
|
22
24
|
error: ErrorSource | null;
|
|
23
25
|
} | null,
|
|
24
26
|
profiles: {} as Record<string, SolidUserProfile>,
|
|
@@ -34,20 +36,14 @@ export default defineServiceState({
|
|
|
34
36
|
return state;
|
|
35
37
|
},
|
|
36
38
|
computed: {
|
|
37
|
-
|
|
38
|
-
user: (state) => state.session?.user ?? null,
|
|
39
|
-
userAvatarUrl: (state) => state.session?.user?.avatarUrl ?? state.previousSession?.avatarUrl ?? null,
|
|
40
|
-
wasLoggedIn: (state) => !!state.previousSession?.loginUrl,
|
|
39
|
+
user: (state) => state.session?.user ?? state.previousSession?.profile ?? null,
|
|
41
40
|
authenticator: (state) => state.session?.authenticator ?? null,
|
|
42
|
-
hasLoggedIn(): boolean {
|
|
43
|
-
return this.loggedIn || this.wasLoggedIn;
|
|
44
|
-
},
|
|
45
41
|
fetch(): Fetch {
|
|
46
|
-
return this.authenticator?.getAuthenticatedFetch() ??
|
|
42
|
+
return this.authenticator?.getAuthenticatedFetch() ?? globalThis.fetch.bind(globalThis);
|
|
47
43
|
},
|
|
48
|
-
error(state) {
|
|
49
|
-
if (state.
|
|
50
|
-
return state.
|
|
44
|
+
error(state): ErrorSource | null {
|
|
45
|
+
if (state.loginStartupError) {
|
|
46
|
+
return state.loginStartupError ?? null;
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
if (state.ignorePreviousSessionError) {
|