Skip to content
Open
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 @@ -515,10 +515,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)
} catch (err) {
alert(err.message)
}
Expand Down Expand Up @@ -671,9 +668,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
}
Expand Down Expand Up @@ -718,6 +715,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