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