@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
package/src/services/Solid.ts
CHANGED
|
@@ -1,39 +1,69 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
after,
|
|
3
3
|
arrayFilter,
|
|
4
4
|
arrayUnique,
|
|
5
5
|
arrayWithout,
|
|
6
|
+
asyncMemo,
|
|
6
7
|
facade,
|
|
7
8
|
fail,
|
|
8
9
|
getLocationQueryParameter,
|
|
9
10
|
hasLocationQueryParameter,
|
|
11
|
+
isDevelopment,
|
|
10
12
|
objectWithout,
|
|
13
|
+
objectWithoutEmpty,
|
|
11
14
|
parseBoolean,
|
|
15
|
+
requireUrlParse,
|
|
16
|
+
resetAsyncMemo,
|
|
17
|
+
setAsyncMemo,
|
|
12
18
|
tap,
|
|
13
19
|
urlRoot,
|
|
14
20
|
} from '@noeldemartin/utils';
|
|
21
|
+
import { App, Errors, Events, Storage, UI, translateWithDefault } from '@aerogel/core';
|
|
15
22
|
import { fetchLoginUserProfile } from '@noeldemartin/solid-utils';
|
|
16
|
-
import {
|
|
23
|
+
import { setEngine } from 'soukai';
|
|
24
|
+
import { SolidACLAuthorization, SolidContainer, SolidDocument, SolidTypeIndex } from 'soukai-solid';
|
|
25
|
+
import type { ErrorSource } from '@aerogel/core';
|
|
17
26
|
import type { Fetch, SolidModelConstructor } from 'soukai-solid';
|
|
18
|
-
import {
|
|
19
|
-
import type { SolidUserProfile } from '@noeldemartin/solid-utils';
|
|
20
|
-
|
|
21
|
-
import AuthenticationCancelledError from '
|
|
22
|
-
import { ContainerAlreadyInUse } from '
|
|
23
|
-
import { getAuthenticator } from '
|
|
24
|
-
import type
|
|
25
|
-
import type
|
|
26
|
-
import type { AuthSession } from '
|
|
27
|
-
|
|
28
|
-
import Service from './Solid.state';
|
|
27
|
+
import type { NullablePartial } from '@noeldemartin/utils';
|
|
28
|
+
import type { SolidStore, SolidUserProfile } from '@noeldemartin/solid-utils';
|
|
29
|
+
|
|
30
|
+
import AuthenticationCancelledError from '@aerogel/plugin-solid/errors/AuthenticationCancelledError';
|
|
31
|
+
import { ContainerAlreadyInUse } from '@aerogel/plugin-solid/errors';
|
|
32
|
+
import { getAuthenticator } from '@aerogel/plugin-solid/auth';
|
|
33
|
+
import type Authenticator from '@aerogel/plugin-solid/auth/Authenticator';
|
|
34
|
+
import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
|
|
35
|
+
import type { AuthSession } from '@aerogel/plugin-solid/auth/Authenticator';
|
|
36
|
+
|
|
37
|
+
import Service, { DEFAULT_STATE } from './Solid.state';
|
|
38
|
+
|
|
39
|
+
export type LoginOptions = NullablePartial<{
|
|
40
|
+
authenticator: AuthenticatorName;
|
|
41
|
+
onError(error: ErrorSource): unknown;
|
|
42
|
+
fallbackUrl: string;
|
|
43
|
+
loading: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
export type UserProfileOptions = NullablePartial<{
|
|
47
|
+
required: boolean;
|
|
48
|
+
markStale: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export type ReconnectOptions = Omit<LoginOptions, 'authenticator'> & {
|
|
52
|
+
force?: boolean;
|
|
53
|
+
};
|
|
29
54
|
|
|
30
55
|
export class SolidService extends Service {
|
|
31
56
|
|
|
32
|
-
protected publicTypeIndexes: Record<string, PromisedValue<SolidTypeIndex>> = {};
|
|
33
|
-
protected privateTypeIndexes: Record<string, PromisedValue<SolidTypeIndex>> = {};
|
|
34
|
-
|
|
35
57
|
public isLoggedIn(): this is { session: AuthSession; user: SolidUserProfile; authenticator: Authenticator } {
|
|
36
|
-
return this.
|
|
58
|
+
return !!this.session;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public wasLoggedIn(): this is { user: SolidUserProfile } {
|
|
62
|
+
return !!this.previousSession;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public hasLoggedIn(): this is { user: SolidUserProfile } {
|
|
66
|
+
return !!(this.session || this.previousSession);
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
public requireAuthenticator(): Authenticator {
|
|
@@ -41,26 +71,49 @@ export class SolidService extends Service {
|
|
|
41
71
|
}
|
|
42
72
|
|
|
43
73
|
public requireUser(): SolidUserProfile {
|
|
44
|
-
return this.user ?? fail('Could not get user profile');
|
|
74
|
+
return this.user ?? fail('Could not get user profile (are you logged in?)');
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
public requireUserProfile(url: string): Promise<SolidUserProfile> {
|
|
48
|
-
return this.getUserProfile(url, true);
|
|
78
|
+
return this.getUserProfile(url, { required: true });
|
|
49
79
|
}
|
|
50
80
|
|
|
51
|
-
|
|
52
|
-
public async getUserProfile(url: string, required
|
|
53
|
-
public async getUserProfile(url: string,
|
|
54
|
-
|
|
81
|
+
/* eslint-disable max-len */
|
|
82
|
+
public async getUserProfile(url: string, options: Omit<UserProfileOptions, 'required'> & { required: true }): Promise<SolidUserProfile>; // prettier-ignore
|
|
83
|
+
public async getUserProfile(url: string, options?: UserProfileOptions): Promise<SolidUserProfile | null>;
|
|
84
|
+
/* eslint-enable max-len */
|
|
85
|
+
|
|
86
|
+
public async getUserProfile(url: string, options: UserProfileOptions = {}): Promise<SolidUserProfile | null> {
|
|
87
|
+
let profileStore: SolidStore | null = null;
|
|
88
|
+
const markStale = options.markStale ?? !this.authenticator?.getAuthenticatedFetch();
|
|
89
|
+
const userProfile =
|
|
55
90
|
this.profiles[url] ??
|
|
56
|
-
tap(
|
|
91
|
+
(await tap(
|
|
57
92
|
await fetchLoginUserProfile(url, {
|
|
58
93
|
fetch: this.fetch,
|
|
59
|
-
required,
|
|
94
|
+
required: options.required ?? false,
|
|
95
|
+
onLoaded(store) {
|
|
96
|
+
profileStore = store;
|
|
97
|
+
},
|
|
60
98
|
}),
|
|
61
|
-
(profile) =>
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
async (profile) => {
|
|
100
|
+
if (!profile) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (profileStore) {
|
|
105
|
+
await Events.emit('solid:user-profile-loaded', [profile, profileStore]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.rememberProfile(profile);
|
|
109
|
+
},
|
|
110
|
+
));
|
|
111
|
+
|
|
112
|
+
if (userProfile && markStale) {
|
|
113
|
+
this.setState({ staleProfiles: arrayUnique(this.staleProfiles.concat([userProfile.webId])) });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return userProfile;
|
|
64
117
|
}
|
|
65
118
|
|
|
66
119
|
public async refreshUserProfile(): Promise<void> {
|
|
@@ -80,14 +133,17 @@ export class SolidService extends Service {
|
|
|
80
133
|
});
|
|
81
134
|
}
|
|
82
135
|
|
|
83
|
-
public async login(loginUrl: string,
|
|
84
|
-
|
|
136
|
+
public async login(loginUrl: string, options: LoginOptions = {}): Promise<boolean> {
|
|
137
|
+
if (isDevelopment() && loginUrl === 'dev') {
|
|
138
|
+
loginUrl = 'http://localhost:3000';
|
|
139
|
+
}
|
|
85
140
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
141
|
+
if (!/^https?:\/\//.test(loginUrl)) {
|
|
142
|
+
options.fallbackUrl ??= `http://${loginUrl}`;
|
|
143
|
+
loginUrl = `https://${loginUrl}`;
|
|
88
144
|
}
|
|
89
145
|
|
|
90
|
-
if (this.
|
|
146
|
+
if (this.isLoggedIn()) {
|
|
91
147
|
return true;
|
|
92
148
|
}
|
|
93
149
|
|
|
@@ -95,59 +151,84 @@ export class SolidService extends Service {
|
|
|
95
151
|
throw new Error('Authentication already in progress');
|
|
96
152
|
}
|
|
97
153
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const profile = await this.getUserProfile(loginUrl);
|
|
103
|
-
const oidcIssuerUrl = profile?.oidcIssuerUrl ?? urlRoot(profile?.webId ?? loginUrl);
|
|
104
|
-
const authenticator = await this.bootAuthenticator(authenticatorName);
|
|
105
|
-
|
|
106
|
-
this.setState({
|
|
107
|
-
dismissed: false,
|
|
108
|
-
ignorePreviousSessionError: true,
|
|
109
|
-
previousSession: {
|
|
110
|
-
loginUrl,
|
|
111
|
-
avatarUrl: profile?.avatarUrl,
|
|
112
|
-
authenticator: authenticatorName,
|
|
113
|
-
error: translateWithDefault(
|
|
114
|
-
'auth.stuckConnecting',
|
|
115
|
-
'We didn\'t hear back from the identity provider, maybe try reconnecting?',
|
|
116
|
-
),
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
await authenticator.login(oidcIssuerUrl);
|
|
121
|
-
|
|
122
|
-
return true;
|
|
123
|
-
} catch (error) {
|
|
124
|
-
if (error instanceof AuthenticationCancelledError) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
App.isMounted ? Errors.report(error) : this.setState({ loginError: error });
|
|
129
|
-
|
|
130
|
-
return false;
|
|
131
|
-
} finally {
|
|
132
|
-
clearTimeout(staleTimeout);
|
|
133
|
-
|
|
134
|
-
this.loginOngoing = false;
|
|
135
|
-
this.loginStale = false;
|
|
154
|
+
if (!options.loading) {
|
|
155
|
+
return this.attemptLogin(loginUrl, options);
|
|
136
156
|
}
|
|
157
|
+
|
|
158
|
+
return UI.loading(translateWithDefault('solid.loginOngoing', 'Logging in...'), () => {
|
|
159
|
+
return this.attemptLogin(loginUrl, options);
|
|
160
|
+
});
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
public async reconnect(
|
|
140
|
-
|
|
163
|
+
public async reconnect(options: ReconnectOptions = {}): Promise<void> {
|
|
164
|
+
const { force, ...loginOptions } = options;
|
|
165
|
+
if (this.isLoggedIn() || !this.previousSession || (this.previousSession.error !== null && !force)) {
|
|
141
166
|
return;
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
await this.login(this.previousSession.loginUrl,
|
|
169
|
+
await this.login(this.previousSession.loginUrl, {
|
|
170
|
+
authenticator: this.previousSession.authenticator,
|
|
171
|
+
...loginOptions,
|
|
172
|
+
loading: loginOptions.loading ?? false,
|
|
173
|
+
});
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
public async logout(force: boolean = false): Promise<void> {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
const isCloudReady = !App.plugin('@aerogel/local-first') || !!App.service('$cloud')?.ready;
|
|
178
|
+
const hasLocalChanges = !App.plugin('@aerogel/local-first') || !!App.service('$cloud')?.dirty;
|
|
179
|
+
const [confirmLogout, options] = force
|
|
180
|
+
? [true, { wipeLocalData: isCloudReady }]
|
|
181
|
+
: await UI.confirm(
|
|
182
|
+
isCloudReady
|
|
183
|
+
? translateWithDefault('solid.logoutConfirmTitle', 'Log out from this device?')
|
|
184
|
+
: translateWithDefault('solid.disconnectConfirmTitle', 'Disconnect account?'),
|
|
185
|
+
isCloudReady
|
|
186
|
+
? hasLocalChanges
|
|
187
|
+
? translateWithDefault(
|
|
188
|
+
'solid.logoutConfirmMessageWithLocalChanges',
|
|
189
|
+
'There are some changes that haven\'t been synchronized and will be lost, ' +
|
|
190
|
+
'but the rest of the data will remain in your Solid POD.',
|
|
191
|
+
)
|
|
192
|
+
: translateWithDefault(
|
|
193
|
+
'solid.logoutConfirmMessage',
|
|
194
|
+
'Logging out will remove all the data from this device, ' +
|
|
195
|
+
'but you\'ll still have it in your Solid POD.',
|
|
196
|
+
)
|
|
197
|
+
: translateWithDefault(
|
|
198
|
+
'solid.disconnectConfirmMessage',
|
|
199
|
+
'You\'ll need to introduce your credentials again to connect to your Solid POD.',
|
|
200
|
+
),
|
|
201
|
+
{
|
|
202
|
+
acceptText: translateWithDefault('solid.logoutConfirmAccept', 'Log out'),
|
|
203
|
+
acceptVariant: 'danger',
|
|
204
|
+
cancelText: translateWithDefault(
|
|
205
|
+
'solid.logoutConfirmCancel',
|
|
206
|
+
translateWithDefault('ui.cancel', 'Cancel'),
|
|
207
|
+
),
|
|
208
|
+
checkboxes: isCloudReady
|
|
209
|
+
? hasLocalChanges
|
|
210
|
+
? {
|
|
211
|
+
wipeLocalData: {
|
|
212
|
+
label: translateWithDefault(
|
|
213
|
+
'solid.logoutConfirmWipeNotice',
|
|
214
|
+
'I understand that changes that haven\'t been synchronized will be lost.',
|
|
215
|
+
),
|
|
216
|
+
default: false,
|
|
217
|
+
required: true,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
: undefined
|
|
221
|
+
: {
|
|
222
|
+
wipeLocalData: {
|
|
223
|
+
label: translateWithDefault(
|
|
224
|
+
'solid.logoutConfirmWipe',
|
|
225
|
+
'Also remove all existing data',
|
|
226
|
+
),
|
|
227
|
+
default: false,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
);
|
|
151
232
|
|
|
152
233
|
if (!confirmLogout) {
|
|
153
234
|
return;
|
|
@@ -157,10 +238,36 @@ export class SolidService extends Service {
|
|
|
157
238
|
return;
|
|
158
239
|
}
|
|
159
240
|
|
|
160
|
-
|
|
161
|
-
|
|
241
|
+
await UI.loading(translateWithDefault('solid.logoutOngoing', 'Logging out...'), async () => {
|
|
242
|
+
this.setState({
|
|
243
|
+
dismissed: false,
|
|
244
|
+
ignorePreviousSessionError: false,
|
|
245
|
+
loginStartupError: null,
|
|
246
|
+
previousSession: null,
|
|
247
|
+
...DEFAULT_STATE,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this.isLoggedIn() && (await this.authenticator.logout());
|
|
162
251
|
|
|
163
|
-
|
|
252
|
+
if (options.wipeLocalData ?? true) {
|
|
253
|
+
await Storage.purge();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await Events.emit('auth:logout');
|
|
257
|
+
await Events.emit('auth:after-logout');
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async whenLoggedIn<T>(callback: (session: AuthSession) => T): Promise<T> {
|
|
262
|
+
if (this.isLoggedIn()) {
|
|
263
|
+
return callback(this.session);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
const onLogin = (session: AuthSession) => resolve(callback(session));
|
|
268
|
+
|
|
269
|
+
Events.once('auth:login', onLogin);
|
|
270
|
+
});
|
|
164
271
|
}
|
|
165
272
|
|
|
166
273
|
public dismiss(): void {
|
|
@@ -168,40 +275,48 @@ export class SolidService extends Service {
|
|
|
168
275
|
}
|
|
169
276
|
|
|
170
277
|
public async findOrCreatePrivateTypeIndex(): Promise<SolidTypeIndex> {
|
|
171
|
-
|
|
172
|
-
|
|
278
|
+
return (await this.findPrivateTypeIndex()) ?? (await this.createPrivateTypeIndex());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public async findPrivateTypeIndex(options: { fresh?: boolean } = {}): Promise<SolidTypeIndex | null> {
|
|
282
|
+
const { webId, privateTypeIndexUrl } = this.requireUser();
|
|
283
|
+
const engine = this.requireAuthenticator().engine;
|
|
284
|
+
|
|
285
|
+
if (!privateTypeIndexUrl) {
|
|
286
|
+
return null;
|
|
173
287
|
}
|
|
174
288
|
|
|
175
|
-
|
|
289
|
+
if (options.fresh) {
|
|
290
|
+
resetAsyncMemo(`${webId}-privateTypeIndex`);
|
|
291
|
+
}
|
|
176
292
|
|
|
177
|
-
return (
|
|
178
|
-
|
|
179
|
-
PromisedValue.from(
|
|
180
|
-
(async () => (await this.findPrivateTypeIndex()) ?? (await this.createPrivateTypeIndex()))(),
|
|
181
|
-
));
|
|
293
|
+
return asyncMemo(`${webId}-privateTypeIndex`, () =>
|
|
294
|
+
SolidTypeIndex.withEngine(engine).find(privateTypeIndexUrl));
|
|
182
295
|
}
|
|
183
296
|
|
|
184
297
|
public async findOrCreatePublicTypeIndex(): Promise<SolidTypeIndex> {
|
|
185
|
-
|
|
186
|
-
|
|
298
|
+
return (await this.findPublicTypeIndex()) ?? (await this.createPublicTypeIndex());
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
public async findPublicTypeIndex(options: { fresh?: boolean } = {}): Promise<SolidTypeIndex | null> {
|
|
302
|
+
const { webId, publicTypeIndexUrl } = this.requireUser();
|
|
303
|
+
const engine = this.requireAuthenticator().engine;
|
|
304
|
+
|
|
305
|
+
if (!publicTypeIndexUrl) {
|
|
306
|
+
return null;
|
|
187
307
|
}
|
|
188
308
|
|
|
189
|
-
|
|
309
|
+
if (options.fresh) {
|
|
310
|
+
resetAsyncMemo(`${webId}-publicTypeIndex`);
|
|
311
|
+
}
|
|
190
312
|
|
|
191
|
-
return (
|
|
192
|
-
this.publicTypeIndexes[webId] ??
|
|
193
|
-
PromisedValue.from(
|
|
194
|
-
(async () => (await this.findPublicTypeIndex()) ?? (await this.createPublicTypeIndex()))(),
|
|
195
|
-
));
|
|
313
|
+
return asyncMemo(`${webId}-publicTypeIndex`, () => SolidTypeIndex.withEngine(engine).find(publicTypeIndexUrl));
|
|
196
314
|
}
|
|
197
315
|
|
|
198
316
|
public async createPrivateContainer(options: {
|
|
199
317
|
url: string;
|
|
200
|
-
name
|
|
201
|
-
|
|
202
|
-
typeIndex: SolidTypeIndex;
|
|
203
|
-
modelClass: SolidModelConstructor;
|
|
204
|
-
};
|
|
318
|
+
name?: string;
|
|
319
|
+
registerFor?: SolidModelConstructor;
|
|
205
320
|
reuseExisting?: boolean;
|
|
206
321
|
}): Promise<SolidContainer> {
|
|
207
322
|
const engine = this.requireAuthenticator().engine;
|
|
@@ -213,18 +328,21 @@ export class SolidService extends Service {
|
|
|
213
328
|
throw new ContainerAlreadyInUse(existingContainer);
|
|
214
329
|
}
|
|
215
330
|
|
|
216
|
-
const container = existingContainer ?? new SolidContainer({ url: options.url, name: options.
|
|
331
|
+
const container = existingContainer ?? new SolidContainer({ url: options.url, name: options.name });
|
|
217
332
|
|
|
218
333
|
await container.save();
|
|
219
334
|
|
|
220
|
-
|
|
335
|
+
if (options.registerFor) {
|
|
336
|
+
const typeIndex = await this.findOrCreatePrivateTypeIndex();
|
|
337
|
+
|
|
338
|
+
await container.register(typeIndex, options.registerFor);
|
|
339
|
+
}
|
|
221
340
|
|
|
222
341
|
return container;
|
|
223
342
|
});
|
|
224
343
|
}
|
|
225
344
|
|
|
226
|
-
protected async boot(): Promise<void> {
|
|
227
|
-
await super.boot();
|
|
345
|
+
protected override async boot(): Promise<void> {
|
|
228
346
|
await Errors.booted;
|
|
229
347
|
|
|
230
348
|
if (hasLocationQueryParameter('authenticator')) {
|
|
@@ -245,47 +363,99 @@ export class SolidService extends Service {
|
|
|
245
363
|
await this.startupReconnect();
|
|
246
364
|
}
|
|
247
365
|
|
|
248
|
-
protected async
|
|
366
|
+
protected async createPrivateTypeIndex(): Promise<SolidTypeIndex> {
|
|
249
367
|
const user = this.requireUser();
|
|
250
368
|
const authenticator = this.requireAuthenticator();
|
|
369
|
+
const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPrivate(user);
|
|
370
|
+
|
|
371
|
+
return tap(typeIndex, () => {
|
|
372
|
+
user.privateTypeIndexUrl = typeIndex.url;
|
|
251
373
|
|
|
252
|
-
|
|
374
|
+
setAsyncMemo(`${user.webId}-privateTypeIndex`, typeIndex);
|
|
375
|
+
|
|
376
|
+
this.rememberProfile(user);
|
|
377
|
+
});
|
|
253
378
|
}
|
|
254
379
|
|
|
255
|
-
|
|
380
|
+
public async createPublicTypeIndex(): Promise<SolidTypeIndex> {
|
|
256
381
|
const user = this.requireUser();
|
|
257
382
|
const authenticator = this.requireAuthenticator();
|
|
258
|
-
const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).
|
|
383
|
+
const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPublic(user);
|
|
259
384
|
|
|
260
385
|
return tap(typeIndex, () => {
|
|
261
386
|
user.privateTypeIndexUrl = typeIndex.url;
|
|
262
387
|
|
|
388
|
+
setAsyncMemo(`${user.webId}-publicTypeIndex`, typeIndex);
|
|
389
|
+
|
|
263
390
|
this.rememberProfile(user);
|
|
264
391
|
});
|
|
265
392
|
}
|
|
266
393
|
|
|
267
|
-
protected async
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
394
|
+
protected async attemptLogin(loginUrl: string, options: LoginOptions): Promise<boolean> {
|
|
395
|
+
const authenticatorName = options.authenticator ?? this.preferredAuthenticator ?? 'default';
|
|
396
|
+
const handleLoginError =
|
|
397
|
+
options.onError ??
|
|
398
|
+
((error: ErrorSource) =>
|
|
399
|
+
App.isMounted() ? Errors.report(error) : this.setState({ loginStartupError: error }));
|
|
400
|
+
const staleTimeout = setTimeout(() => (this.loginStale = true), 10000);
|
|
401
|
+
const initialState = {
|
|
402
|
+
dismissed: this.dismissed,
|
|
403
|
+
ignorePreviousSessionError: this.ignorePreviousSessionError,
|
|
404
|
+
previousSession: this.previousSession,
|
|
405
|
+
};
|
|
271
406
|
|
|
272
|
-
|
|
273
|
-
}
|
|
407
|
+
this.loginOngoing = true;
|
|
274
408
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
409
|
+
try {
|
|
410
|
+
const profile = await this.getUserProfile(loginUrl, { markStale: true });
|
|
411
|
+
const oidcIssuerUrl = profile?.oidcIssuerUrl ?? urlRoot(profile?.webId ?? loginUrl);
|
|
412
|
+
const authenticator = await this.bootAuthenticator(authenticatorName);
|
|
413
|
+
const { domain: loginDomain } = requireUrlParse(loginUrl);
|
|
414
|
+
|
|
415
|
+
this.setState({
|
|
416
|
+
dismissed: false,
|
|
417
|
+
ignorePreviousSessionError: true,
|
|
418
|
+
previousSession: objectWithoutEmpty({
|
|
419
|
+
profile,
|
|
420
|
+
loginUrl,
|
|
421
|
+
authenticator: authenticatorName,
|
|
422
|
+
error: translateWithDefault(
|
|
423
|
+
'solid.stuckConnecting',
|
|
424
|
+
'We didn\'t hear back from the identity provider at `{domain}`, maybe try reconnecting?',
|
|
425
|
+
{ domain: loginDomain },
|
|
426
|
+
),
|
|
427
|
+
}),
|
|
428
|
+
});
|
|
279
429
|
|
|
280
|
-
|
|
281
|
-
const typeIndex = await SolidTypeIndex.withEngine(this.authenticator.engine, () =>
|
|
282
|
-
SolidTypeIndex.createPublic(user));
|
|
430
|
+
await Events.emit('auth:before-login');
|
|
283
431
|
|
|
284
|
-
|
|
285
|
-
|
|
432
|
+
// This should redirect away from the app, so in most cases
|
|
433
|
+
// the rest of the code won't be reached.
|
|
434
|
+
await authenticator.login(oidcIssuerUrl, profile);
|
|
286
435
|
|
|
287
|
-
|
|
288
|
-
})
|
|
436
|
+
return true;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
this.setState(initialState);
|
|
439
|
+
|
|
440
|
+
if (error instanceof AuthenticationCancelledError) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (options.fallbackUrl) {
|
|
445
|
+
return after().then(() => this.login(options.fallbackUrl ?? '', objectWithout(options, 'fallbackUrl')));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
handleLoginError(error);
|
|
449
|
+
|
|
450
|
+
return false;
|
|
451
|
+
} finally {
|
|
452
|
+
clearTimeout(staleTimeout);
|
|
453
|
+
|
|
454
|
+
this.setState({
|
|
455
|
+
loginOngoing: false,
|
|
456
|
+
loginStale: false,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
289
459
|
}
|
|
290
460
|
|
|
291
461
|
private async restorePreviousSession(): Promise<void> {
|
|
@@ -301,7 +471,7 @@ export class SolidService extends Service {
|
|
|
301
471
|
return;
|
|
302
472
|
}
|
|
303
473
|
|
|
304
|
-
await this.reconnect();
|
|
474
|
+
await this.reconnect({ force: this.forcingReconnect() });
|
|
305
475
|
}
|
|
306
476
|
|
|
307
477
|
private reconnectOnStartup(): boolean {
|
|
@@ -312,6 +482,10 @@ export class SolidService extends Service {
|
|
|
312
482
|
return this.autoReconnect;
|
|
313
483
|
}
|
|
314
484
|
|
|
485
|
+
private forcingReconnect(): boolean {
|
|
486
|
+
return hasLocationQueryParameter('autoReconnect') && parseBoolean(getLocationQueryParameter('autoReconnect'));
|
|
487
|
+
}
|
|
488
|
+
|
|
315
489
|
private async bootAuthenticator(name: AuthenticatorName): Promise<Authenticator> {
|
|
316
490
|
const authenticator = getAuthenticator(name);
|
|
317
491
|
|
|
@@ -322,27 +496,28 @@ export class SolidService extends Service {
|
|
|
322
496
|
previousSession: {
|
|
323
497
|
authenticator: authenticator.name,
|
|
324
498
|
loginUrl: session.loginUrl,
|
|
325
|
-
|
|
499
|
+
profile: session.user,
|
|
326
500
|
error: null,
|
|
327
501
|
},
|
|
328
502
|
});
|
|
329
503
|
|
|
330
|
-
if (
|
|
331
|
-
session.user.cloaked ||
|
|
332
|
-
!session.user.writableProfileUrl ||
|
|
333
|
-
this.staleProfiles.includes(session.user.webId)
|
|
334
|
-
) {
|
|
504
|
+
if (this.staleProfiles.includes(session.user.webId)) {
|
|
335
505
|
await this.refreshUserProfile();
|
|
336
506
|
}
|
|
337
507
|
|
|
508
|
+
App.plugin('@aerogel/local-first') || setEngine(session.authenticator.engine);
|
|
509
|
+
|
|
338
510
|
SolidACLAuthorization.setEngine(session.authenticator.engine);
|
|
511
|
+
SolidTypeIndex.setEngine(session.authenticator.engine);
|
|
512
|
+
SolidDocument.setEngine(session.authenticator.engine);
|
|
339
513
|
|
|
340
|
-
await Events.emit('login', session);
|
|
514
|
+
await Events.emit('auth:login', session);
|
|
341
515
|
},
|
|
342
516
|
onSessionFailed: async (loginUrl, error) => {
|
|
343
517
|
this.setState({
|
|
344
518
|
session: null,
|
|
345
519
|
previousSession: {
|
|
520
|
+
profile: this.previousSession?.profile,
|
|
346
521
|
authenticator: authenticator.name,
|
|
347
522
|
loginUrl,
|
|
348
523
|
error,
|
|
@@ -350,7 +525,7 @@ export class SolidService extends Service {
|
|
|
350
525
|
});
|
|
351
526
|
},
|
|
352
527
|
onSessionEnded: () => this.setState({ session: null }),
|
|
353
|
-
onAuthenticatedFetchReady: (fetch) => Events.emit('
|
|
528
|
+
onAuthenticatedFetchReady: (fetch) => Events.emit('auth:fetch-ready', fetch),
|
|
354
529
|
});
|
|
355
530
|
|
|
356
531
|
await authenticator.boot();
|
|
@@ -370,12 +545,15 @@ export class SolidService extends Service {
|
|
|
370
545
|
|
|
371
546
|
}
|
|
372
547
|
|
|
373
|
-
export default facade(
|
|
548
|
+
export default facade(SolidService);
|
|
374
549
|
|
|
375
550
|
declare module '@aerogel/core' {
|
|
376
551
|
export interface EventsPayload {
|
|
377
|
-
'
|
|
378
|
-
login:
|
|
379
|
-
|
|
552
|
+
'auth:fetch-ready': Fetch;
|
|
553
|
+
'auth:before-login': void;
|
|
554
|
+
'auth:login': AuthSession;
|
|
555
|
+
'auth:logout': void;
|
|
556
|
+
'auth:after-logout': void;
|
|
557
|
+
'solid:user-profile-loaded': [SolidUserProfile, SolidStore];
|
|
380
558
|
}
|
|
381
559
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FakeLocalStorage } from '@noeldemartin/testing';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
vi.mock('@aerogel/core', async () => {
|
|
5
|
+
const original = (await vi.importActual('@aerogel/core')) as object;
|
|
6
|
+
|
|
7
|
+
FakeLocalStorage.reset();
|
|
8
|
+
FakeLocalStorage.patchGlobal();
|
|
9
|
+
|
|
10
|
+
return original;
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@aerogel/plugin-local-first" />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module 'solid-auth-client' {
|
|
2
|
+
export type Session = {
|
|
3
|
+
webId: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export class SolidAuthClient {
|
|
7
|
+
|
|
8
|
+
public fetch(...args: any[]): Promise<Response>;
|
|
9
|
+
|
|
10
|
+
public login(loginUrl: string): Promise<Session | void>;
|
|
11
|
+
|
|
12
|
+
public currentSession(): Promise<Session | void>;
|
|
13
|
+
|
|
14
|
+
public logout(): Promise<void>;
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const auth: SolidAuthClient;
|
|
19
|
+
|
|
20
|
+
export default auth;
|
|
21
|
+
}
|