@reflagged/shell 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { loadAppConfig } from '../config'
|
|
|
6
6
|
import { baseSearchUrl, isPaletteShortcut, matchApps, type PaletteApp } from '../lib/palette/palette'
|
|
7
7
|
import { ImpersonationCapsule, type ImpersonationInfo } from './ImpersonationCapsule'
|
|
8
8
|
import { ServiceIcon } from './ServiceIcon'
|
|
9
|
-
import { useTenantTheme } from './TenantTheme'
|
|
9
|
+
import { faviconHref, useTenantFavicon, useTenantTheme } from './TenantTheme'
|
|
10
10
|
|
|
11
11
|
type Booking = {
|
|
12
12
|
id: string
|
|
@@ -226,6 +226,7 @@ export function BrandSwitcher() {
|
|
|
226
226
|
}, [open])
|
|
227
227
|
|
|
228
228
|
useTenantTheme(shell?.org?.themeCssUrl)
|
|
229
|
+
useTenantFavicon(faviconHref(shell?.org))
|
|
229
230
|
|
|
230
231
|
const orgName = shell?.org?.name ?? null
|
|
231
232
|
const baseUrl = shell?.baseUrl ?? 'https://app.rfl.gd'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
|
|
5
5
|
const THEME_LINK_ID = 'rflgd-tenant-theme'
|
|
6
|
+
const FAVICON_LINK_ID = 'rflgd-tenant-favicon'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Loads the tenant palette stylesheet from the platform once per page:
|
|
@@ -26,24 +27,79 @@ export function useTenantTheme(href: string | null | undefined): void {
|
|
|
26
27
|
}, [href])
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Which stored image becomes the favicon.
|
|
32
|
+
*
|
|
33
|
+
* The mark is the square asset made for small sizes; the wide logo squeezed
|
|
34
|
+
* into 16×16 looks poor but still tells you which tab you are on. A tenant may
|
|
35
|
+
* have neither — then the tab stays blank rather than showing somebody else's
|
|
36
|
+
* logo, which is worse than no icon at all.
|
|
37
|
+
*
|
|
38
|
+
* Pure on purpose: the choice is the part worth testing, the DOM work below
|
|
39
|
+
* only repeats what `useTenantTheme` has been doing since 1.x.
|
|
40
|
+
*/
|
|
41
|
+
export function faviconHref(
|
|
42
|
+
org: { logoMarkUrl?: string | null; logoUrl?: string | null } | null | undefined,
|
|
43
|
+
): string | null {
|
|
44
|
+
return org?.logoMarkUrl?.trim() || org?.logoUrl?.trim() || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Points the browser tab at the tenant's logo.
|
|
49
|
+
*
|
|
50
|
+
* Set from the client, because the modules share this package rather than a
|
|
51
|
+
* server layout: a per-app `icon.tsx` would mean the same file in every
|
|
52
|
+
* service. The cost is a blank tab for the moment it takes `/api/shell-info`
|
|
53
|
+
* to answer.
|
|
54
|
+
*/
|
|
55
|
+
export function useTenantFavicon(href: string | null | undefined): void {
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!href) return
|
|
58
|
+
const existing = document.getElementById(FAVICON_LINK_ID) as HTMLLinkElement | null
|
|
59
|
+
if (existing) {
|
|
60
|
+
if (existing.href !== href) existing.href = href
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const link = document.createElement('link')
|
|
64
|
+
link.id = FAVICON_LINK_ID
|
|
65
|
+
link.rel = 'icon'
|
|
66
|
+
link.href = href
|
|
67
|
+
document.head.appendChild(link)
|
|
68
|
+
}, [href])
|
|
69
|
+
}
|
|
70
|
+
|
|
29
71
|
/**
|
|
30
72
|
* For modules that do not mount `BrandSwitcher` (which loads the palette
|
|
31
73
|
* itself): render once in the root layout.
|
|
32
74
|
*/
|
|
33
75
|
export function TenantTheme(): null {
|
|
34
76
|
const [href, setHref] = useState<string | null>(null)
|
|
77
|
+
const [icon, setIcon] = useState<string | null>(null)
|
|
35
78
|
useEffect(() => {
|
|
36
79
|
let cancel = false
|
|
37
80
|
fetch('/api/shell-info', { credentials: 'include' })
|
|
38
81
|
.then((r) => (r.ok ? r.json() : null))
|
|
39
|
-
.then(
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
.then(
|
|
83
|
+
(
|
|
84
|
+
d: {
|
|
85
|
+
org?: {
|
|
86
|
+
themeCssUrl?: string | null
|
|
87
|
+
logoMarkUrl?: string | null
|
|
88
|
+
logoUrl?: string | null
|
|
89
|
+
} | null
|
|
90
|
+
} | null,
|
|
91
|
+
) => {
|
|
92
|
+
if (cancel) return
|
|
93
|
+
setHref(d?.org?.themeCssUrl ?? null)
|
|
94
|
+
setIcon(faviconHref(d?.org))
|
|
95
|
+
},
|
|
96
|
+
)
|
|
42
97
|
.catch(() => undefined)
|
|
43
98
|
return () => {
|
|
44
99
|
cancel = true
|
|
45
100
|
}
|
|
46
101
|
}, [])
|
|
47
102
|
useTenantTheme(href)
|
|
103
|
+
useTenantFavicon(icon)
|
|
48
104
|
return null
|
|
49
105
|
}
|
|
@@ -55,13 +55,26 @@ export async function GET(req: Request): Promise<NextResponse> {
|
|
|
55
55
|
|
|
56
56
|
const params = oauth.validateAuthResponse(as, client, url, pkce.state)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
// The provider issued a code and may still refuse to exchange it. Seen live:
|
|
59
|
+
// after an "Einloggen als" session ended, the browser's provider session
|
|
60
|
+
// still belonged to the impersonated account, every code was minted for it,
|
|
61
|
+
// and every exchange came back invalid_grant — an unhandled throw, so a bare
|
|
62
|
+
// 500 on each reload.
|
|
63
|
+
let result: Awaited<ReturnType<typeof oauth.processAuthorizationCodeResponse>>
|
|
64
|
+
try {
|
|
65
|
+
const tokenResponse = await oauth.authorizationCodeGrantRequest(
|
|
66
|
+
as, client, clientAuth, params, env.redirectUri, pkce.codeVerifier,
|
|
67
|
+
)
|
|
68
|
+
result = await oauth.processAuthorizationCodeResponse(as, client, tokenResponse, {
|
|
69
|
+
expectedNonce: pkce.nonce,
|
|
70
|
+
requireIdToken: true,
|
|
71
|
+
})
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!(error instanceof oauth.ResponseBodyError)) throw error
|
|
74
|
+
const res = deniedResponse(error.error, error.error_description ?? null)
|
|
75
|
+
res.cookies.delete(OIDC_STATE_COOKIE)
|
|
76
|
+
return res
|
|
77
|
+
}
|
|
65
78
|
|
|
66
79
|
const claims = oauth.getValidatedIdTokenClaims(result)
|
|
67
80
|
if (!claims) return NextResponse.json({ error: 'no-id-token' }, { status: 400 })
|
|
@@ -123,13 +136,25 @@ const escapeHtml = (v: string): string =>
|
|
|
123
136
|
*/
|
|
124
137
|
export function deniedResponse(error: string, description: string | null): NextResponse {
|
|
125
138
|
const status = error === 'access_denied' ? 403 : 400
|
|
139
|
+
// invalid_grant: the code belongs to a provider session that is no longer
|
|
140
|
+
// valid. Trying again lands in that same session, so the way out is to end
|
|
141
|
+
// it — signing out at the platform — not to ask an administrator.
|
|
142
|
+
const expired = error === 'invalid_grant'
|
|
126
143
|
const headline =
|
|
127
|
-
error === 'access_denied'
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
error === 'access_denied'
|
|
145
|
+
? 'Kein Zugriff auf diese Anwendung'
|
|
146
|
+
: expired
|
|
147
|
+
? 'Anmeldung abgelaufen'
|
|
148
|
+
: 'Anmeldung fehlgeschlagen'
|
|
149
|
+
const detail = expired
|
|
150
|
+
? 'Die Anmeldung gehört zu einer Sitzung, die nicht mehr gilt — etwa nach dem Ende von „Einloggen als“. Melden Sie sich bei der Plattform ab und danach neu an.'
|
|
151
|
+
: description?.trim() ||
|
|
152
|
+
(error === 'access_denied'
|
|
153
|
+
? 'Ihr Account ist dieser Anwendung nicht zugeordnet.'
|
|
154
|
+
: `Der Anmeldedienst hat die Anmeldung abgelehnt (${error}).`)
|
|
155
|
+
const next = expired
|
|
156
|
+
? '<p><a href="/api/oidc/signout">Bei der Plattform abmelden</a></p>'
|
|
157
|
+
: '<p>Bitten Sie eine Administratorin oder einen Administrator Ihrer Organisation, Ihren Account für diese Anwendung freizuschalten.</p>\n <p><a href="/">Zur Startseite</a></p>'
|
|
133
158
|
const html = `<!doctype html>
|
|
134
159
|
<html lang="de">
|
|
135
160
|
<head>
|
|
@@ -150,8 +175,7 @@ export function deniedResponse(error: string, description: string | null): NextR
|
|
|
150
175
|
<main>
|
|
151
176
|
<h1>${escapeHtml(headline)}</h1>
|
|
152
177
|
<p>${escapeHtml(detail)}</p>
|
|
153
|
-
|
|
154
|
-
<p><a href="/">Zur Startseite</a></p>
|
|
178
|
+
${next}
|
|
155
179
|
<p><small>Fehlercode: ${escapeHtml(error)}</small></p>
|
|
156
180
|
</main>
|
|
157
181
|
</body>
|