Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 176 additions & 12 deletions src/login/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void>((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: <http://purl.org/dc/terms/>.\n' +
'@prefix pim: <http://www.w3.org/ns/pim/space#>.\n' +
'@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n' +
'@prefix solid: <http://www.w3.org/ns/solid/terms#>.\n\n' +
'<>\n' +
' a pim:ConfigurationFile;\n\n' +
' dct:title "Preferences file" .\n' +
mboxLine +
`<${me.uri}>\n` +
' solid:publicTypeIndex <publicTypeIndex.ttl> ;\n' +
' solid:privateTypeIndex <privateTypeIndex.ttl> .\n'
)
}

async function writePreferencesFileDocument (uri: string, data: string): Promise<void> {
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<void> {
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
*
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -1049,17 +1209,21 @@ export function newAppInstance (
export async function getUserRoles (): Promise<Array<NamedNode>> {
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'),
null,
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 []
}
Expand Down