diff --git a/src/login/login.ts b/src/login/login.ts index b93e07f1..1ba342b1 100644 --- a/src/login/login.ts +++ b/src/login/login.ts @@ -65,6 +65,125 @@ const { deleteTypeIndexRegistration } = solidLogicSingleton.typeIndex +function formatDynamicError (error: unknown, contextLabel: string): string { + const e = error as any + const name = e?.name || 'Error' + const status = typeof e?.status === 'number' ? ` (HTTP ${e.status})` : '' + const message = e?.message + ? String(e.message) + : (typeof error === 'string' ? error : 'No additional details provided') + return `${contextLabel}: ${name}${status} - ${message}` +} + +function guessPreferencesFileUri (me: NamedNode): string { + return me.uri + .replace('/profile/', '/') + .replace('/public/', '/') + .split('/') + .slice(0, -1) + .join('/') + '/settings/prefs.ttl' +} + +function getMissingPreferencesFileUri (context: AuthenticationContext, err: unknown): string | null { + const responseUrl = (err as any)?.response?.url + if (typeof responseUrl === 'string' && responseUrl) { + return responseUrl + } + if (context.me) { + const pointer = store.any( + context.me, + ns.space('preferencesFile'), + undefined, + context.me.doc ? context.me.doc() : undefined + ) as NamedNode | null + if (pointer?.uri) { + return pointer.uri + } + return guessPreferencesFileUri(context.me as NamedNode) + } + return null +} + +async function ensurePreferencesFileRowInProfile ( + me: NamedNode, + prefsNode: NamedNode, + profileDoc: NamedNode +): Promise { + const existingPreferencesFile = store.any(me, ns.space('preferencesFile'), undefined, profileDoc) + + if (existingPreferencesFile && existingPreferencesFile.equals(prefsNode)) { + return + } + + if (!store.updater) { + throw new Error('Cannot create preferences file: store has no updater') + } + + const deletions = existingPreferencesFile + ? [st(me, ns.space('preferencesFile'), existingPreferencesFile, profileDoc)] + : [] + const insertions = [st(me, ns.space('preferencesFile'), prefsNode, profileDoc)] + + await new Promise((resolve, reject) => { + store.updater!.update(deletions as any, insertions as any, (_uri, ok, body) => { + if (ok) { + resolve() + } else { + reject(new Error(body || 'Unable to update preferencesFile in WebID profile')) + } + }) + }) +} + +function buildPreferencesFileTurtle (me: NamedNode): string { + const mbox = store.any(me, ns.foaf('mbox')) + const mboxLine = mbox ? `\n<${me.uri}> foaf:mbox <${mbox.value}> .\n` : '\n' + + return ( + '@prefix dct: .\n' + + '@prefix pim: .\n' + + '@prefix foaf: .\n' + + '@prefix solid: .\n\n' + + '<>\n' + + ' a pim:ConfigurationFile;\n\n' + + ' dct:title "Preferences file" .\n' + + mboxLine + + `<${me.uri}>\n` + + ' solid:publicTypeIndex ;\n' + + ' solid:privateTypeIndex .\n' + ) +} + +async function writePreferencesFileDocument (uri: string, data: string): Promise { + if (!store.fetcher) { + throw new Error('Cannot create preferences file: store has no fetcher') + } + + await store.fetcher.webOperation('PUT', uri, { + data, + contentType: 'text/turtle' + }) +} + +async function createPreferencesFile (context: AuthenticationContext, uri: string): Promise { + const me = (context.me as NamedNode) || authn.currentUser() + if (!me) { + throw new Error('Cannot create preferences file: no logged-in WebID') + } + context.me = me + + const profileDoc = me.doc() + const prefsNode = store.sym(uri) + + await ensurePreferencesFileRowInProfile(me, prefsNode, profileDoc) + + const prefsTurtle = buildPreferencesFileTurtle(me) + await writePreferencesFileDocument(uri, prefsTurtle) + + context.preferencesFile = await loadPreferences(me) + context.preferencesFileError = undefined +} + /** * Resolves with the logged in user's WebID * @@ -125,6 +244,11 @@ export async function ensureLoadedPreferences ( } */ try { context = await ensureLoadedProfile(context) + if (!context.me) { + context.preferencesFileError = 'Not logged in, so preferences were not loaded.' + debug.log('not logged in, no preferences loaded') + return context + } // console.log('back in Solid UI after logInLoadProfile', context) const preferencesFile = await loadPreferences(context.me as NamedNode) @@ -137,30 +261,63 @@ export async function ensureLoadedPreferences ( if (err instanceof UnauthorizedError) { m2 = 'Oops — you are not authenticated (properly logged in), so SolidOS cannot read your preferences file. Try logging out and then logging back in.' + context.preferencesFileError = m2 alert(m2) + return context } else if (err instanceof CrossOriginForbiddenError) { - m2 = `Unauthorized: Assuming preference file blocked for origin ${window.location.origin}` + m2 = `Blocked by origin: preference file request from ${window.location.origin} was forbidden. Ensure this app origin is trusted/allowed for your preferences file.` context.preferencesFileError = m2 return context } else if (err instanceof SameOriginForbiddenError) { m2 = - 'You are not authorized to read your preference file. This may be because you are using an untrusted web app.' + 'You are not authorized to read your preference file from this app context.' + context.preferencesFileError = m2 debug.warn(m2) return context } else if (err instanceof NotEditableError) { m2 = - 'You are not authorized to edit your preference file. This may be because you are using an untrusted web app.' + 'You are not authorized to edit your preference file from this app context.' + context.preferencesFileError = m2 debug.warn(m2) return context } else if (err instanceof WebOperationError) { - m2 = - 'You are not authorized to edit your preference file. This may be because you are using an untrusted web app.' + m2 = formatDynamicError(err, 'Preference file web operation failed') + context.preferencesFileError = m2 debug.warn(m2) + return context } else if (err instanceof FetchError) { - m2 = `Strange: Error ${err.status} trying to read your preference file.${err.message}` + if (err.status === 404) { + m2 = 'Your preferences file was not found (404). It may not exist yet.' + debug.warn(m2) + const missingPreferencesFileUri = getMissingPreferencesFileUri(context, err) + if (missingPreferencesFileUri) { + try { + await createPreferencesFile(context, missingPreferencesFileUri) + debug.log(`Preferences file created automatically: ${missingPreferencesFileUri}`) + return context + } catch (createError) { + const message = formatDynamicError(createError, 'Failed to create preferences file automatically') + context.preferencesFileError = message + debug.error(message) + if (context.div && context.dom) { + context.div.appendChild(widgets.errorMessageBlock(context.dom, message)) + } + return context + } + } + context.preferencesFileError = m2 + return context + } + m2 = formatDynamicError(err, 'Error reading your preferences file') + context.preferencesFileError = m2 + debug.warn(m2) alert(m2) + return context } else { - throw new Error(`(via loadPrefs) ${err}`) + m2 = formatDynamicError(err, 'Unexpected error while loading preferences') + context.preferencesFileError = m2 + debug.error(m2) + return context } } return context @@ -183,14 +340,17 @@ export async function ensureLoadedProfile ( try { const logInContext = await ensureLoggedIn(context) if (!logInContext.me) { - throw new Error('Could not log in') + const message = 'Could not log in; skipping profile load.' + debug.log(message) + throw new Error(message) } context.publicProfile = await loadProfile(logInContext.me) } catch (err) { + const message = formatDynamicError(err, 'Unable to load your profile') if (context.div && context.dom) { - context.div.appendChild(widgets.errorMessageBlock(context.dom, err.message)) + context.div.appendChild(widgets.errorMessageBlock(context.dom, message)) } - throw new Error(`Can't log in: ${err}`) + throw new Error(message) } return context } @@ -1049,9 +1209,13 @@ export function newAppInstance ( export async function getUserRoles (): Promise> { try { const { me, preferencesFile, preferencesFileError } = await ensureLoadedPreferences({}) - if (!preferencesFile || preferencesFileError) { + if (preferencesFileError) { throw new Error(preferencesFileError) } + if (!preferencesFile) { + const authState = me ? `logged in as ${me.value || me.uri}` : 'not logged in' + throw new Error(`Preferences file unavailable (${authState})`) + } return solidLogicSingleton.store.each( me, ns.rdf('type'), @@ -1059,7 +1223,7 @@ export async function getUserRoles (): Promise> { preferencesFile.doc() ) as NamedNode[] } catch (error) { - debug.warn('Unable to fetch your preferences - this was the error: ', error) + debug.warn(formatDynamicError(error, 'Unable to fetch your preferences')) } return [] }