Skip to content
Open
Show file tree
Hide file tree
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
15 changes: 14 additions & 1 deletion jest.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import { existsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const localSolidLogicSrc = path.resolve(__dirname, '../solid-logic/src')
const solidLogicMapper = existsSync(localSolidLogicSrc)
? localSolidLogicSrc
: '<rootDir>/node_modules/solid-logic/src'

export default {
// verbose: true, // Uncomment for detailed test output
collectCoverage: true,
Expand All @@ -15,7 +26,9 @@ export default {
],
setupFilesAfterEnv: ['./test/helpers/setup.ts'],
moduleNameMapper: {
'^.+\\.css$': '<rootDir>/__mocks__/styleMock.js'
'^.+\\.css$': '<rootDir>/__mocks__/styleMock.js',
'^solid-logic$': solidLogicMapper,
'^@uvdsl/solid-oidc-client-browser$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts'
},
Comment on lines 28 to 32
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
roots: ['<rootDir>/src', '<rootDir>/test', '<rootDir>/__mocks__'],
Expand Down
17 changes: 10 additions & 7 deletions src/login/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,7 @@ export function renderSignInPopup (dom: HTMLDocument) {
// Login
const locationUrl = new URL(window.location.href)
locationUrl.hash = '' // remove hash part
await authSession.login({
redirectUrl: locationUrl.href,
oidcIssuer: issuerUri
})
await authSession.login(issuerUri, locationUrl.href)
Comment thread
bourgeoa marked this conversation as resolved.
} catch (err) {
alert(err.message)
}
Expand Down Expand Up @@ -669,9 +666,9 @@ export function loginStatusBox (
}

box.refresh = function () {
const sessionInfo = authSession.info
if (sessionInfo && sessionInfo.webId && sessionInfo.isLoggedIn) {
me = solidLogicSingleton.store.sym(sessionInfo.webId)
const webId = authSession.webId
if (webId) {
me = solidLogicSingleton.store.sym(webId)
} else {
me = null
}
Comment thread
bourgeoa marked this conversation as resolved.
Expand Down Expand Up @@ -716,6 +713,12 @@ authSession.events.on('logout', async () => {
await fetch(openidConfiguration.end_session_endpoint, { credentials: 'include' })
}
}

try {
await fetch('/.well-known/solid/logout', { credentials: 'include' })
} catch (_err) {
// Not all deployments expose NSS-compatible well-known logout endpoint.
}
} catch (_err) {
// Do nothing
}
Expand Down
5 changes: 1 addition & 4 deletions src/v2/components/auth/loginButton/LoginButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,7 @@ export class LoginButton extends LitElement {

const locationUrl = new URL(window.location.href)
locationUrl.hash = ''
await authSession.login({
redirectUrl: locationUrl.href,
oidcIssuer: issuerUri
})
await authSession.login(issuerUri, locationUrl.href)
Comment thread
bourgeoa marked this conversation as resolved.
} catch (err: any) {
this._errorMsg = err.message || String(err)
this.requestUpdate()
Expand Down
3 changes: 0 additions & 3 deletions src/v2/components/layout/footer/Footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@ export class Footer extends LitElement {
if (typeof authSession.events.off === 'function') {
authSession.events.off('login', this._updateFooter)
authSession.events.off('logout', this._updateFooter)
} else if (typeof authSession.events.removeListener === 'function') {
authSession.events.removeListener('login', this._updateFooter)
authSession.events.removeListener('logout', this._updateFooter)
}
super.disconnectedCallback()
}
Expand Down
105 changes: 100 additions & 5 deletions src/v2/components/layout/header/Header.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LitElement, html, css } from 'lit'
import { icons } from '../../../../iconBase'
import { authSession } from 'solid-logic'
import { authSession, authn, performServerSideLogout } from 'solid-logic'
import '../../auth/loginButton/index'
import '../../auth/signupButton/index'
import { ifDefined } from 'lit/directives/if-defined.js'
Expand All @@ -10,6 +10,36 @@ const DEFAULT_SOLID_ICON_URL = 'https://solidproject.org/assets/img/solid-emblem
const DEFAULT_SIGNUP_URL = 'https://solidproject.org/get_a_pod'
const DEFAULT_LOGGEDIN_MENU_BUTTON_AVATAR = icons.iconBase + 'emptyProfileAvatar.png'

async function clearPersistedAuthState (): Promise<void> {
if (typeof window === 'undefined') {
return
}

const explicitKeys = ['loginIssuer', 'preLoginRedirectHash']
for (const key of explicitKeys) {
window.localStorage.removeItem(key)
window.sessionStorage.removeItem(key)
}

if (typeof indexedDB === 'undefined') {
return
}

const databases = ['soidc', 'solid-client-authn-store', 'solid-client-authn']
for (const dbName of databases) {
await new Promise<void>((resolve) => {
try {
const request = indexedDB.deleteDatabase(dbName)
request.onsuccess = () => resolve()
request.onerror = () => resolve()
request.onblocked = () => resolve()
} catch (_err) {
resolve()
}
})
}
}

export type HeaderAuthState = 'logged-out' | 'logged-in'

export type HeaderMenuItem = {
Expand Down Expand Up @@ -47,7 +77,8 @@ export class Header extends LitElement {
accountMenuOpen: { state: true },
helpMenuOpen: { state: true },
hasSlottedAccountMenu: { state: true },
hasSlottedHelpMenu: { state: true }
hasSlottedHelpMenu: { state: true },
authResolved: { state: true }
}

static styles = css`
Expand Down Expand Up @@ -510,6 +541,14 @@ export class Header extends LitElement {
declare helpMenuOpen: boolean
declare hasSlottedAccountMenu: boolean
declare hasSlottedHelpMenu: boolean
declare authResolved: boolean
private _refreshPromise: Promise<void> | null = null

private readonly handleAuthSessionChange = () => {
this.refreshAuthStateFromSession().catch(() => {
// Keep auth event handling resilient on transient refresh failures.
})
}

constructor () {
super()
Expand All @@ -534,20 +573,59 @@ export class Header extends LitElement {
this.helpMenuOpen = false
this.hasSlottedAccountMenu = false
this.hasSlottedHelpMenu = false
this.authResolved = false
}

connectedCallback () {
super.connectedCallback()
document.addEventListener('click', this.handleDocumentClick)
window.addEventListener('keydown', this.handleWindowKeydown)
if (typeof authSession.events?.on === 'function') {
authSession.events.on('login', this.handleAuthSessionChange)
authSession.events.on('logout', this.handleAuthSessionChange)
authSession.events.on('sessionRestore', this.handleAuthSessionChange)
}
this.refreshAuthStateFromSession().catch(() => {
// Keep initial header render resilient on transient refresh failures.
})
}

disconnectedCallback () {
document.removeEventListener('click', this.handleDocumentClick)
window.removeEventListener('keydown', this.handleWindowKeydown)
if (typeof authSession.events?.off === 'function') {
authSession.events.off('login', this.handleAuthSessionChange)
authSession.events.off('logout', this.handleAuthSessionChange)
authSession.events.off('sessionRestore', this.handleAuthSessionChange)
}
super.disconnectedCallback()
}

private async refreshAuthStateFromSession () {
if (!this._refreshPromise) {
this._refreshPromise = (async () => {
try {
await authn.checkUser()
// Some auth stacks resolve session state asynchronously after first check.
if (!authn.currentUser()) {
await authn.checkUser()
}
} catch (_err) {
// Keep rendering even if session refresh cannot complete.
}
})()
}

try {
await this._refreshPromise
} finally {
this._refreshPromise = null
}

this.authState = authn.currentUser() ? 'logged-in' : 'logged-out'
this.authResolved = true
}

private handleHelpMenuClick (item: HeaderMenuItem, event: MouseEvent) {
event.preventDefault()
this.helpMenuOpen = false
Expand Down Expand Up @@ -665,8 +743,8 @@ export class Header extends LitElement {
`
}

private handleLoginSuccess () {
this.authState = 'logged-in'
private async handleLoginSuccess () {
await this.refreshAuthStateFromSession()
this.dispatchEvent(new CustomEvent('auth-action-select', {
detail: { role: 'login' },
bubbles: true,
Expand All @@ -676,12 +754,25 @@ export class Header extends LitElement {

private async handleLogout () {
this.accountMenuOpen = false
const issuer = window.localStorage.getItem('loginIssuer') || ''

try {
await authSession.logout()
} catch (_err) {
// logout errors are non-fatal — proceed to clear state
}
this.authState = 'logged-out'

await clearPersistedAuthState()

const redirectedToServerLogout = await performServerSideLogout({
issuer,
postLogoutRedirectPath: '/'
})
if (redirectedToServerLogout) {
return
}

await this.refreshAuthStateFromSession()
this.dispatchEvent(new CustomEvent('logout-select', {
detail: { role: 'logout' },
bubbles: true,
Expand Down Expand Up @@ -790,6 +881,10 @@ export class Header extends LitElement {
}

private renderUserArea () {
if (!this.authResolved) {
return html`<div class="auth-actions" part="auth-actions"></div>`
}

if (this.authState === 'logged-out') {
return this.renderLoggedOutActions()
}
Expand Down
Loading
Loading