@aerogel/plugin-solid 0.0.1-next.2fb667e321e0740b4db1a21bc948232a9bf219d6
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.hash.cjs.js +2 -0
- package/dist/InruptAuthenticator.lazy.hash.cjs.js.map +1 -0
- package/dist/InruptAuthenticator.lazy.hash.esm.js +2 -0
- package/dist/InruptAuthenticator.lazy.hash.esm.js.map +1 -0
- package/dist/aerogel-plugin-solid.cjs.js +2 -0
- package/dist/aerogel-plugin-solid.cjs.js.map +1 -0
- package/dist/aerogel-plugin-solid.d.ts +266 -0
- package/dist/aerogel-plugin-solid.esm.js +2 -0
- package/dist/aerogel-plugin-solid.esm.js.map +1 -0
- package/noeldemartin.config.js +2 -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 +83 -0
- package/src/auth/index.ts +38 -0
- package/src/components/AGSolidStatus.ts +5 -0
- package/src/components/AGSolidStatus.vue +60 -0
- package/src/components/index.ts +4 -0
- package/src/errors/AuthenticationCancelledError.ts +3 -0
- package/src/errors/ContainerAlreadyInUse.ts +11 -0
- package/src/errors/index.ts +4 -0
- package/src/main.ts +51 -0
- package/src/services/Solid.mock.ts +38 -0
- package/src/services/Solid.state.ts +54 -0
- package/src/services/Solid.ts +431 -0
- package/src/types/shims.d.ts +7 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
after,
|
|
3
|
+
arrayFilter,
|
|
4
|
+
arrayUnique,
|
|
5
|
+
arrayWithout,
|
|
6
|
+
asyncMemo,
|
|
7
|
+
facade,
|
|
8
|
+
fail,
|
|
9
|
+
getLocationQueryParameter,
|
|
10
|
+
hasLocationQueryParameter,
|
|
11
|
+
objectWithout,
|
|
12
|
+
objectWithoutEmpty,
|
|
13
|
+
parseBoolean,
|
|
14
|
+
requireUrlParse,
|
|
15
|
+
setAsyncMemo,
|
|
16
|
+
tap,
|
|
17
|
+
urlRoot,
|
|
18
|
+
} from '@noeldemartin/utils';
|
|
19
|
+
import { App, Errors, Events, UI, translateWithDefault } from '@aerogel/core';
|
|
20
|
+
import { fetchLoginUserProfile } from '@noeldemartin/solid-utils';
|
|
21
|
+
import { setEngine } from 'soukai';
|
|
22
|
+
import { SolidACLAuthorization, SolidContainer, SolidDocument, SolidTypeIndex } from 'soukai-solid';
|
|
23
|
+
import type { ErrorSource } from '@aerogel/core';
|
|
24
|
+
import type { Fetch, SolidModelConstructor } from 'soukai-solid';
|
|
25
|
+
import type { SolidUserProfile } from '@noeldemartin/solid-utils';
|
|
26
|
+
|
|
27
|
+
import AuthenticationCancelledError from '@/errors/AuthenticationCancelledError';
|
|
28
|
+
import { ContainerAlreadyInUse } from '@/errors';
|
|
29
|
+
import { getAuthenticator } from '@/auth';
|
|
30
|
+
import type Authenticator from '@/auth/Authenticator';
|
|
31
|
+
import type { AuthenticatorName } from '@/auth';
|
|
32
|
+
import type { AuthSession } from '@/auth/Authenticator';
|
|
33
|
+
|
|
34
|
+
import Service from './Solid.state';
|
|
35
|
+
|
|
36
|
+
export type LoginOptions = {
|
|
37
|
+
authenticator?: AuthenticatorName;
|
|
38
|
+
onError?(error: ErrorSource): unknown;
|
|
39
|
+
fallbackUrl?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ReconnectOptions = Omit<LoginOptions, 'authenticator'> & {
|
|
43
|
+
force?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class SolidService extends Service {
|
|
47
|
+
|
|
48
|
+
public isLoggedIn(): this is { session: AuthSession; user: SolidUserProfile; authenticator: Authenticator } {
|
|
49
|
+
return !!this.session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public wasLoggedIn(): this is { user: SolidUserProfile } {
|
|
53
|
+
return !!this.previousSession;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public hasLoggedIn(): this is { user: SolidUserProfile } {
|
|
57
|
+
return !!(this.session || this.previousSession);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public requireAuthenticator(): Authenticator {
|
|
61
|
+
return this.authenticator ?? fail('Could not get authenticator');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public requireUser(): SolidUserProfile {
|
|
65
|
+
return this.user ?? fail('Could not get user profile (are you logged in?)');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public requireUserProfile(url: string): Promise<SolidUserProfile> {
|
|
69
|
+
return this.getUserProfile(url, true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async getUserProfile(url: string, required: true): Promise<SolidUserProfile>;
|
|
73
|
+
public async getUserProfile(url: string, required?: false): Promise<SolidUserProfile | null>;
|
|
74
|
+
public async getUserProfile(url: string, required: boolean = false): Promise<SolidUserProfile | null> {
|
|
75
|
+
return (
|
|
76
|
+
this.profiles[url] ??
|
|
77
|
+
tap(
|
|
78
|
+
await fetchLoginUserProfile(url, {
|
|
79
|
+
fetch: this.fetch,
|
|
80
|
+
required,
|
|
81
|
+
}),
|
|
82
|
+
(profile) => profile && this.rememberProfile(profile),
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public async refreshUserProfile(): Promise<void> {
|
|
88
|
+
if (!this.isLoggedIn()) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.setState({ profiles: objectWithout(this.profiles, [this.user.webId]) });
|
|
93
|
+
|
|
94
|
+
const user = await this.requireUserProfile(this.user.webId);
|
|
95
|
+
|
|
96
|
+
this.setState({
|
|
97
|
+
session: {
|
|
98
|
+
...this.session,
|
|
99
|
+
user,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public async login(loginUrl: string, options: LoginOptions = {}): Promise<boolean> {
|
|
105
|
+
const authenticatorName = options.authenticator ?? this.preferredAuthenticator ?? 'default';
|
|
106
|
+
const handleLoginError =
|
|
107
|
+
options.onError ??
|
|
108
|
+
((error: ErrorSource) =>
|
|
109
|
+
App.isMounted() ? Errors.report(error) : this.setState({ loginStartupError: error }));
|
|
110
|
+
|
|
111
|
+
if (App.development && loginUrl === 'devserver') {
|
|
112
|
+
loginUrl = 'http://localhost:3000';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!/^https?:\/\//.test(loginUrl)) {
|
|
116
|
+
options.fallbackUrl ??= `http://${loginUrl}`;
|
|
117
|
+
loginUrl = `https://${loginUrl}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (this.isLoggedIn()) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.loginOngoing) {
|
|
125
|
+
throw new Error('Authentication already in progress');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const staleTimeout = setTimeout(() => (this.loginStale = true), 10000);
|
|
129
|
+
const initialState = {
|
|
130
|
+
dismissed: this.dismissed,
|
|
131
|
+
ignorePreviousSessionError: this.ignorePreviousSessionError,
|
|
132
|
+
previousSession: this.previousSession,
|
|
133
|
+
};
|
|
134
|
+
this.loginOngoing = true;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const profile = await this.getUserProfile(loginUrl);
|
|
138
|
+
const oidcIssuerUrl = profile?.oidcIssuerUrl ?? urlRoot(profile?.webId ?? loginUrl);
|
|
139
|
+
const authenticator = await this.bootAuthenticator(authenticatorName);
|
|
140
|
+
const { domain: loginDomain } = requireUrlParse(loginUrl);
|
|
141
|
+
|
|
142
|
+
this.setState({
|
|
143
|
+
dismissed: false,
|
|
144
|
+
ignorePreviousSessionError: true,
|
|
145
|
+
previousSession: objectWithoutEmpty({
|
|
146
|
+
profile,
|
|
147
|
+
loginUrl,
|
|
148
|
+
authenticator: authenticatorName,
|
|
149
|
+
error: translateWithDefault(
|
|
150
|
+
'auth.stuckConnecting',
|
|
151
|
+
`We didn't hear back from the identity provider at \`${loginDomain}\`, maybe try reconnecting?`,
|
|
152
|
+
),
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await Events.emit('auth:before-login');
|
|
157
|
+
|
|
158
|
+
// This should redirect away from the app, so in most cases
|
|
159
|
+
// the rest of the code won't be reached.
|
|
160
|
+
await authenticator.login(oidcIssuerUrl);
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.setState(initialState);
|
|
165
|
+
|
|
166
|
+
if (error instanceof AuthenticationCancelledError) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (options.fallbackUrl) {
|
|
171
|
+
return after().then(() => this.login(options.fallbackUrl ?? '', objectWithout(options, 'fallbackUrl')));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
handleLoginError(error);
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(staleTimeout);
|
|
179
|
+
|
|
180
|
+
this.setState({
|
|
181
|
+
loginOngoing: false,
|
|
182
|
+
loginStale: false,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async reconnect(options: ReconnectOptions = {}): Promise<void> {
|
|
188
|
+
const { force, ...loginOptions } = options;
|
|
189
|
+
if (this.isLoggedIn() || !this.previousSession || (this.previousSession.error !== null && !force)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await this.login(this.previousSession.loginUrl, {
|
|
194
|
+
authenticator: this.previousSession.authenticator,
|
|
195
|
+
...loginOptions,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public async logout(force: boolean = false): Promise<void> {
|
|
200
|
+
const confirmLogout =
|
|
201
|
+
force ||
|
|
202
|
+
(await UI.confirm(translateWithDefault('solid.logoutConfirm', 'Are you sure you want to log out?')));
|
|
203
|
+
|
|
204
|
+
if (!confirmLogout) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!this.previousSession) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.setState({ previousSession: null });
|
|
213
|
+
this.isLoggedIn() && (await this.authenticator.logout());
|
|
214
|
+
|
|
215
|
+
await Events.emit('auth:logout');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public async whenLoggedIn<T>(callback: (session: AuthSession) => T): Promise<T> {
|
|
219
|
+
if (this.isLoggedIn()) {
|
|
220
|
+
return callback(this.session);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
const onLogin = (session: AuthSession) => resolve(callback(session));
|
|
225
|
+
|
|
226
|
+
Events.once('auth:login', onLogin);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public dismiss(): void {
|
|
231
|
+
this.setState({ dismissed: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public async findOrCreatePrivateTypeIndex(): Promise<SolidTypeIndex> {
|
|
235
|
+
return (await this.findPrivateTypeIndex()) ?? (await this.createPrivateTypeIndex());
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public async findPrivateTypeIndex(): Promise<SolidTypeIndex | null> {
|
|
239
|
+
const user = this.requireUser();
|
|
240
|
+
const engine = this.requireAuthenticator().engine;
|
|
241
|
+
|
|
242
|
+
return asyncMemo(`${user.webId}-privateTypeIndex`, () =>
|
|
243
|
+
SolidTypeIndex.withEngine(engine).find(user.privateTypeIndexUrl));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public async findOrCreatePublicTypeIndex(): Promise<SolidTypeIndex> {
|
|
247
|
+
return (await this.findPublicTypeIndex()) ?? (await this.createPublicTypeIndex());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
protected async findPublicTypeIndex(): Promise<SolidTypeIndex | null> {
|
|
251
|
+
const user = this.requireUser();
|
|
252
|
+
const engine = this.requireAuthenticator().engine;
|
|
253
|
+
|
|
254
|
+
return asyncMemo(`${user.webId}-publicTypeIndex`, () =>
|
|
255
|
+
SolidTypeIndex.withEngine(engine).find(user.publicTypeIndexUrl));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public async createPrivateContainer(options: {
|
|
259
|
+
url: string;
|
|
260
|
+
name?: string;
|
|
261
|
+
registerFor?: SolidModelConstructor;
|
|
262
|
+
reuseExisting?: boolean;
|
|
263
|
+
}): Promise<SolidContainer> {
|
|
264
|
+
const engine = this.requireAuthenticator().engine;
|
|
265
|
+
|
|
266
|
+
return SolidContainer.withEngine(engine, async () => {
|
|
267
|
+
const existingContainer = await SolidContainer.find(options.url);
|
|
268
|
+
|
|
269
|
+
if (existingContainer && !options.reuseExisting) {
|
|
270
|
+
throw new ContainerAlreadyInUse(existingContainer);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const container = existingContainer ?? new SolidContainer({ url: options.url, name: options.name });
|
|
274
|
+
|
|
275
|
+
await container.save();
|
|
276
|
+
|
|
277
|
+
if (options.registerFor) {
|
|
278
|
+
const typeIndex = await this.findOrCreatePrivateTypeIndex();
|
|
279
|
+
|
|
280
|
+
await container.register(typeIndex, options.registerFor);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return container;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
protected async boot(): Promise<void> {
|
|
288
|
+
await Errors.booted;
|
|
289
|
+
|
|
290
|
+
if (hasLocationQueryParameter('authenticator')) {
|
|
291
|
+
this.setState({
|
|
292
|
+
preferredAuthenticator: getLocationQueryParameter('authenticator') as AuthenticatorName,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (hasLocationQueryParameter('refreshProfile')) {
|
|
297
|
+
this.setState({
|
|
298
|
+
staleProfiles: arrayUnique(
|
|
299
|
+
this.staleProfiles.concat(arrayFilter([this.session?.user.webId, this.previousSession?.loginUrl])),
|
|
300
|
+
),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await this.restorePreviousSession();
|
|
305
|
+
await this.startupReconnect();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
protected async createPrivateTypeIndex(): Promise<SolidTypeIndex> {
|
|
309
|
+
const user = this.requireUser();
|
|
310
|
+
const authenticator = this.requireAuthenticator();
|
|
311
|
+
const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPrivate(user);
|
|
312
|
+
|
|
313
|
+
return tap(typeIndex, () => {
|
|
314
|
+
user.privateTypeIndexUrl = typeIndex.url;
|
|
315
|
+
|
|
316
|
+
setAsyncMemo(`${user.webId}-privateTypeIndex`, typeIndex);
|
|
317
|
+
|
|
318
|
+
this.rememberProfile(user);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public async createPublicTypeIndex(): Promise<SolidTypeIndex> {
|
|
323
|
+
const user = this.requireUser();
|
|
324
|
+
const authenticator = this.requireAuthenticator();
|
|
325
|
+
const typeIndex = await SolidTypeIndex.withEngine(authenticator.engine).createPublic(user);
|
|
326
|
+
|
|
327
|
+
return tap(typeIndex, () => {
|
|
328
|
+
user.privateTypeIndexUrl = typeIndex.url;
|
|
329
|
+
|
|
330
|
+
setAsyncMemo(`${user.webId}-publicTypeIndex`, typeIndex);
|
|
331
|
+
|
|
332
|
+
this.rememberProfile(user);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async restorePreviousSession(): Promise<void> {
|
|
337
|
+
if (!this.previousSession) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await this.bootAuthenticator(this.previousSession.authenticator);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private async startupReconnect(): Promise<void> {
|
|
345
|
+
if (!this.reconnectOnStartup()) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await this.reconnect();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private reconnectOnStartup(): boolean {
|
|
353
|
+
if (hasLocationQueryParameter('autoReconnect')) {
|
|
354
|
+
return parseBoolean(getLocationQueryParameter('autoReconnect'));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return this.autoReconnect;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async bootAuthenticator(name: AuthenticatorName): Promise<Authenticator> {
|
|
361
|
+
const authenticator = getAuthenticator(name);
|
|
362
|
+
|
|
363
|
+
authenticator.addListener({
|
|
364
|
+
onSessionStarted: async (session) => {
|
|
365
|
+
this.setState({
|
|
366
|
+
session,
|
|
367
|
+
previousSession: {
|
|
368
|
+
authenticator: authenticator.name,
|
|
369
|
+
loginUrl: session.loginUrl,
|
|
370
|
+
profile: session.user,
|
|
371
|
+
error: null,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
session.user.cloaked ||
|
|
377
|
+
!session.user.writableProfileUrl ||
|
|
378
|
+
this.staleProfiles.includes(session.user.webId)
|
|
379
|
+
) {
|
|
380
|
+
await this.refreshUserProfile();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
App.plugin('@aerogel/offline-first') || setEngine(session.authenticator.engine);
|
|
384
|
+
|
|
385
|
+
SolidACLAuthorization.setEngine(session.authenticator.engine);
|
|
386
|
+
SolidTypeIndex.setEngine(session.authenticator.engine);
|
|
387
|
+
SolidDocument.setEngine(session.authenticator.engine);
|
|
388
|
+
|
|
389
|
+
await Events.emit('auth:login', session);
|
|
390
|
+
},
|
|
391
|
+
onSessionFailed: async (loginUrl, error) => {
|
|
392
|
+
this.setState({
|
|
393
|
+
session: null,
|
|
394
|
+
previousSession: {
|
|
395
|
+
authenticator: authenticator.name,
|
|
396
|
+
loginUrl,
|
|
397
|
+
error,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
onSessionEnded: () => this.setState({ session: null }),
|
|
402
|
+
onAuthenticatedFetchReady: (fetch) => Events.emit('auth:fetch-ready', fetch),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await authenticator.boot();
|
|
406
|
+
|
|
407
|
+
return authenticator;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private rememberProfile(profile: SolidUserProfile): void {
|
|
411
|
+
this.setState({
|
|
412
|
+
profiles: {
|
|
413
|
+
...this.profiles,
|
|
414
|
+
[profile.webId]: profile,
|
|
415
|
+
},
|
|
416
|
+
staleProfiles: arrayWithout(this.staleProfiles, profile.webId),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default facade(SolidService);
|
|
423
|
+
|
|
424
|
+
declare module '@aerogel/core' {
|
|
425
|
+
export interface EventsPayload {
|
|
426
|
+
'auth:fetch-ready': Fetch;
|
|
427
|
+
'auth:before-login': void;
|
|
428
|
+
'auth:login': AuthSession;
|
|
429
|
+
'auth:logout': void;
|
|
430
|
+
}
|
|
431
|
+
}
|