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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ so typically a hand-written user interface will be chosen over a generic machine
These panes are used in the Data Browser - see mashlib
[https://github.com/linkeddata/mashlib](https://github.com/linkeddata/mashlib)

When panes are hosted through mashlib and use solid-logic authentication, ensure the refresh worker is served same-origin. The worker export and runtime override contract are documented in solid-logic:
https://github.com/solidos/solid-logic#worker-asset-and-runtime-configuration

Currently the panes available include:

- A default pane which lists the properties of any object
Expand Down
19 changes: 10 additions & 9 deletions dev/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,22 @@ window.onload = async () => {
// registerPanes((cjsOrEsModule: any) => paneRegistry.register(cjsOrEsModule.default || cjsOrEsModule))
const contactsPane = await import('contacts-pane')
paneRegistry.register((contactsPane as any).default || contactsPane)
await authSession.handleIncomingRedirect({
restorePreviousSession: true
})
const session = await authSession
if (!session.info.isLoggedIn) {
await solidLogicSingleton.authn.checkUser()
const session = authSession
const isLoggedIn = session?.info?.isLoggedIn ?? session?.isActive ?? Boolean(session?.webId)
if (!isLoggedIn) {
console.log('The user is not logged in')
const loginBanner = document.getElementById('loginBanner');
if (loginBanner) {
loginBanner.innerHTML = '<button onclick="login()">Log in</button>';
}
} else {
console.log(`Logged in as ${session.info.webId}`)
const loggedWebId = session?.info?.webId || session?.webId
console.log(`Logged in as ${loggedWebId}`)

const loginBanner = document.getElementById('loginBanner');
if (loginBanner) {
loginBanner.innerHTML = `Logged in as ${session.info.webId} <button onclick="logout()">Log out</button>`;
loginBanner.innerHTML = `Logged in as ${loggedWebId} <button onclick="logout()">Log out</button>`;
}
}
addLayoutButtons()
Expand All @@ -156,8 +156,9 @@ window.logout = () => {
window.location.href = ''
}
window.login = async function () {
const session = await authSession
if (!session.info.isLoggedIn) {
const session = authSession
const isLoggedIn = session?.info?.isLoggedIn ?? session?.isActive ?? Boolean(session?.webId)
if (!isLoggedIn) {
const issuer = prompt('Please enter an issuer URI', 'https://solidcommunity.net')
if (issuer) {
await authSession.login({
Expand Down
15 changes: 14 additions & 1 deletion jest.config.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
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 localSolidLogicIndex = path.resolve(__dirname, '../solid-logic/src/index.ts')
const useLocalSolidLogic = existsSync(localSolidLogicIndex)

export default {
collectCoverage: true,
coverageDirectory: 'coverage',
Expand All @@ -19,7 +28,11 @@ export default {
'\\.svg\\?raw$': '<rootDir>/test/__mocks__/fileMock.js',
'\\.(svg)$': '<rootDir>/test/__mocks__/fileMock.js',
'\\.(png|jpe?g|gif|webp|avif)$': '<rootDir>/test/__mocks__/fileMock.js',
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js'
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
...(useLocalSolidLogic ? { '^solid-logic$': localSolidLogicIndex } : {}),
'^@uvdsl/solid-oidc-client-browser$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts',
'^@uvdsl/solid-oidc-client-browser/core$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts',
'^solid-oidc-client-browser$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts'
},
setupFilesAfterEnv: ['./test/helpers/setup.ts'],
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
Expand Down
28 changes: 28 additions & 0 deletions src/mainPage/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ export const HELP_MENU_LIST = [
]

const HEADER_MOBILE_STYLE_ID = 'solid-ui-header-mobile-style'
let authRefreshInFlight: Promise<void> | null = null

async function ensureAuthUserResolved (): Promise<void> {
if (authn.currentUser()) return
if (authRefreshInFlight) {
await authRefreshInFlight
return
}

authRefreshInFlight = (async () => {
try {
await authn.checkUser()
// Some auth stacks resolve session state asynchronously after first check.
if (!authn.currentUser()) {
await authn.checkUser()
}
} catch (_err) {
// Keep header rendering resilient when auth refresh fails.
}
})()

try {
await authRefreshInFlight
} finally {
authRefreshInFlight = null
}
}

type ManagedHeader = Header & {
__solidPanesListenersAttached?: boolean
Expand Down Expand Up @@ -139,6 +166,7 @@ function attachHeaderListeners (header: ManagedHeader) {

export async function refreshHeader (outliner: OutlineManager, headerElement?: Header) {
ensureMobileHeaderStyles()
await ensureAuthUserResolved()
const headerOptions = setHeaderOptions(outliner)
const header = headerElement || document.querySelector('solid-ui-header') as Header | null
if (!header) return null
Expand Down
6 changes: 5 additions & 1 deletion src/mainPage/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ const applyMenuCollapsedState = (navMenu: HTMLElement | null): void => {
updateCollapseButtonPosition(navMenu, collapseBtn)
}

const isLoggedIn = (): boolean => Boolean(authSession?.info?.isLoggedIn)
// Compatibility: solid-logic Session shape differs across stacks (info.isLoggedIn vs isActive/webId).
const isLoggedIn = (): boolean => {
const sessionAny = authSession as any
return Boolean(sessionAny?.info?.isLoggedIn ?? sessionAny?.isActive ?? sessionAny?.webId)
}

const setFooterVisibility = (loggedIn: boolean): void => {
const footer = document.querySelector('solid-ui-footer') as HTMLElement | null
Expand Down
59 changes: 59 additions & 0 deletions test/mocks/solid-oidc-client-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
type Listener = (...args: any[]) => void

class EventEmitterLike {
private listeners: Record<string, Listener[]> = {}

on (event: string, listener: Listener): void {
const list = this.listeners[event] || []
list.push(listener)
this.listeners[event] = list
}

emit (event: string, ...args: any[]): void {
const list = this.listeners[event] || []
list.forEach(listener => listener(...args))
}
}

export class Session {
info: { webId?: string, isLoggedIn: boolean } = { isLoggedIn: false }
webId?: string
isActive = false
events = new EventEmitterLike()

addEventListener (event: string, listener: Listener): void {
this.events.on(event, listener)
}

async handleIncomingRedirect (): Promise<void> {

}

async handleRedirectFromLogin (): Promise<void> {

}

async restore (): Promise<void> {

}

async login (): Promise<void> {

}

async logout (): Promise<void> {
this.info = { isLoggedIn: false }
this.webId = undefined
this.isActive = false
}

fetch (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return globalThis.fetch(input, init)
}

authFetch (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return globalThis.fetch(input, init)
}
}

export class SessionCore extends Session {}
Loading