@@ -77,7 +77,7 @@ export function LoginForm({
id="remember"
name="remember"
type="checkbox"
- className="text-blue-600"
+ className="text-primary"
/>
@@ -93,7 +93,7 @@ export function LoginForm({
@@ -102,13 +102,13 @@ export function LoginForm({
)}
-
+
)
}
diff --git a/packages/oauth-provider/src/assets/app/components/layout.tsx b/packages/oauth-provider/src/assets/app/components/page-layout.tsx
similarity index 88%
rename from packages/oauth-provider/src/assets/app/components/layout.tsx
rename to packages/oauth-provider/src/assets/app/components/page-layout.tsx
index e37dd9406ca..94c7cbe00c6 100644
--- a/packages/oauth-provider/src/assets/app/components/layout.tsx
+++ b/packages/oauth-provider/src/assets/app/components/page-layout.tsx
@@ -1,6 +1,6 @@
import { HTMLAttributes } from 'react'
-export function Layout({
+export function PageLayout({
title,
subTitle,
children,
@@ -16,7 +16,7 @@ export function Layout({
{...props}
>
-
+
{title}
{subTitle}
diff --git a/packages/oauth-provider/src/assets/app/components/session-selector.tsx b/packages/oauth-provider/src/assets/app/components/session-selector-page.tsx
similarity index 83%
rename from packages/oauth-provider/src/assets/app/components/session-selector.tsx
rename to packages/oauth-provider/src/assets/app/components/session-selector-page.tsx
index 02db236c75e..43ba9609d44 100644
--- a/packages/oauth-provider/src/assets/app/components/session-selector.tsx
+++ b/packages/oauth-provider/src/assets/app/components/session-selector-page.tsx
@@ -1,10 +1,10 @@
import { useState } from 'react'
import { Session } from '../types'
-import { AccountList } from './account-list'
-import { LoginForm } from './login-form'
+import { AccountSelectorPage } from './account-selector-page'
+import { LoginPage } from './login-page'
-export function SessionSelector({
+export function SessionSelectorPage({
sessions,
onSession,
onLogin,
@@ -22,7 +22,7 @@ export function SessionSelector({
const [showLogin, setShowLogin] = useState(sessions.length === 0)
return showLogin ? (
-
0
@@ -33,7 +33,7 @@ export function SessionSelector({
}
/>
) : (
- s.account)}
onAccount={(a) => {
const session = sessions.find((s) => s.account.sub === a.sub)
diff --git a/packages/oauth-provider/src/assets/app/main.tsx b/packages/oauth-provider/src/assets/app/main.tsx
index 248f3b864ed..6e7ad37c304 100644
--- a/packages/oauth-provider/src/assets/app/main.tsx
+++ b/packages/oauth-provider/src/assets/app/main.tsx
@@ -2,29 +2,29 @@ import './main.css'
import { createRoot } from 'react-dom/client'
-import { App } from './app'
-import { backendData } from './backend-data'
-
-const url = new URL(window.location.href)
+import { authorizeData, errorData } from './backend-data'
+import { AuthorizePage } from './components/authorize-page'
+import { ErrorPage } from './components/error-page'
// When the user is logging in, make sure the page URL contains the
// "request_uri" in case the user refreshes the page.
-if (
- url.pathname === '/oauth/authorize' &&
- 'clientId' in backendData &&
- 'requestUri' in backendData
-) {
- if (
- !url.searchParams.has('client_id') &&
- !url.searchParams.has('request_uri')
- ) {
- url.search = ''
- url.searchParams.set('client_id', backendData.clientId)
- url.searchParams.set('request_uri', backendData.requestUri)
- window.history.replaceState(history.state, '', url.pathname + url.search)
- }
+const url = new URL(window.location.href)
+if (authorizeData && url.pathname === '/oauth/authorize') {
+ url.search = ''
+ url.searchParams.set('client_id', authorizeData.clientId)
+ url.searchParams.set('request_uri', authorizeData.requestUri)
+ window.history.replaceState(history.state, '', url.pathname + url.search)
}
+// TODO: inject brandingData (from backend-data.ts) into the page (logo & co)
+
const container = document.getElementById('root')!
const root = createRoot(container)
-root.render()
+
+if (authorizeData) {
+ root.render()
+} else if (errorData) {
+ root.render()
+} else {
+ throw new Error('No data found')
+}
diff --git a/packages/oauth-provider/src/oauth-provider.ts b/packages/oauth-provider/src/oauth-provider.ts
index eab7c10856e..d94ac71490d 100644
--- a/packages/oauth-provider/src/oauth-provider.ts
+++ b/packages/oauth-provider/src/oauth-provider.ts
@@ -115,6 +115,7 @@ import {
} from './token/types.js'
import { VerifyTokenClaimsOptions } from './token/verify-token-claims.js'
import { dateToEpoch, dateToRelativeSeconds } from './util/date.js'
+import { Branding } from './output/branding.js'
export type OAuthProviderStore = Partial<
ClientStore &
@@ -854,10 +855,12 @@ export class OAuthProvider extends OAuthVerifier {
Req extends IncomingMessage = IncomingMessage,
Res extends ServerResponse = ServerResponse,
>({
+ branding,
onError = process.env['NODE_ENV'] === 'development'
? (req, res, err): void => console.error('OAuthProvider error:', err)
: undefined,
}: {
+ branding?: Branding
onError?: (req: Req, res: Res, err: unknown) => void
}): Handler {
const sessionManager = new SessionManager(this.sessionStore)
@@ -1128,7 +1131,7 @@ export class OAuthProvider extends OAuthVerifier {
}
case 'authorize' in data: {
await setupCsrfToken(req, res, csrfCookie(data.authorize.uri))
- return await sendAuthorizePage(req, res, data)
+ return await sendAuthorizePage(req, res, data, branding)
}
default: {
// Should never happen
@@ -1139,7 +1142,7 @@ export class OAuthProvider extends OAuthVerifier {
await onError?.(req, res, err)
if (!res.headersSent) {
- await sendErrorPage(req, res, err)
+ await sendErrorPage(req, res, err, branding)
}
}
})
@@ -1235,7 +1238,7 @@ export class OAuthProvider extends OAuthVerifier {
await onError?.(req, res, err)
if (!res.headersSent) {
- await sendErrorPage(req, res, err)
+ await sendErrorPage(req, res, err, branding)
}
}
})
@@ -1287,7 +1290,7 @@ export class OAuthProvider extends OAuthVerifier {
await onError?.(req, res, err)
if (!res.headersSent) {
- await sendErrorPage(req, res, err)
+ await sendErrorPage(req, res, err, branding)
}
}
})
diff --git a/packages/oauth-provider/src/output/branding.ts b/packages/oauth-provider/src/output/branding.ts
new file mode 100644
index 00000000000..1c38bc7d978
--- /dev/null
+++ b/packages/oauth-provider/src/output/branding.ts
@@ -0,0 +1,70 @@
+const DEFAULT_COLORS = {
+ primary: parseColor('#6231af')!,
+}
+export type BrandingColors = Record
+
+export type Branding = {
+ logo?: string
+ colors?: Partial
+}
+
+export function buildBrandingData({ logo }: Branding = {}) {
+ return {
+ logo,
+ }
+}
+
+const DEFAULT_COLOR_ENTRIES = Object.entries(DEFAULT_COLORS)
+export function buildBrandingCss({ colors = {} }: Branding = {}) {
+ const vars = DEFAULT_COLOR_ENTRIES.map(([name, value]) => {
+ const color = Object.hasOwn(colors, name) ? colors[name] : undefined
+ const { r, g, b } = (color && parseColor(color)) || value
+ // alpha not supported by tailwind (it does not work that way)
+ return `--color-${name}: ${r} ${g} ${b};`
+ })
+ return `:root { ${vars.join(' ')} }`
+}
+
+function parseColor(
+ color: string,
+): undefined | { r: number; g: number; b: number; a?: number } {
+ if (color.startsWith('#')) {
+ if (color.length === 4 || color.length === 5) {
+ const [r, g, b, a] = color
+ .slice(1)
+ .split('')
+ .map((c) => parseInt(`${c}${c}`, 16))
+ return { r, g, b, a }
+ }
+
+ if (color.length === 7 || color.length === 9) {
+ const r = parseInt(color.substr(1, 2), 16)
+ const g = parseInt(color.substr(3, 2), 16)
+ const b = parseInt(color.substr(5, 2), 16)
+ const a =
+ color.length === 9 ? parseInt(color.substr(7, 2), 16) : undefined
+ return { r, g, b, a }
+ }
+
+ return undefined
+ }
+
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
+ if (rgbMatch) {
+ const [, r, g, b] = rgbMatch
+ return { r: parseInt(r, 10), g: parseInt(g, 10), b: parseInt(b, 10) }
+ }
+
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+)\)/)
+ if (rgbaMatch) {
+ const [, r, g, b, a] = rgbaMatch
+ return {
+ r: parseInt(r, 10),
+ g: parseInt(g, 10),
+ b: parseInt(b, 10),
+ a: parseInt(a, 10),
+ }
+ }
+
+ return undefined
+}
diff --git a/packages/oauth-provider/src/output/send-authorize-page.ts b/packages/oauth-provider/src/output/send-authorize-page.ts
index 676f96c5c41..7b89161382a 100644
--- a/packages/oauth-provider/src/output/send-authorize-page.ts
+++ b/packages/oauth-provider/src/output/send-authorize-page.ts
@@ -1,12 +1,13 @@
import { IncomingMessage, ServerResponse } from 'node:http'
-import { html } from '@atproto/html'
+import { Html, html } from '@atproto/html'
import { Account } from '../account/account.js'
import { getAsset } from '../assets/index.js'
import { Client } from '../client/client.js'
import { AuthorizationParameters } from '../parameters/authorization-parameters.js'
import { RequestUri } from '../request/request-uri.js'
+import { Branding, buildBrandingCss, buildBrandingData } from './branding.js'
import { declareBrowserGlobalVar, sendWebApp } from './send-web-app.js'
export type AuthorizationResultAuthorize = {
@@ -24,7 +25,7 @@ export type AuthorizationResultAuthorize = {
}
}
-function buildBackendData(data: AuthorizationResultAuthorize) {
+function buildAuthorizeData(data: AuthorizationResultAuthorize) {
return {
csrfCookie: `csrf-${data.authorize.uri}`,
requestUri: data.authorize.uri,
@@ -40,13 +41,18 @@ export async function sendAuthorizePage(
req: IncomingMessage,
res: ServerResponse,
data: AuthorizationResultAuthorize,
+ branding?: Branding,
): Promise {
return sendWebApp(req, res, {
scripts: [
- declareBrowserGlobalVar('__backendData', buildBackendData(data)),
+ declareBrowserGlobalVar('__brandingData', buildBrandingData(branding)),
+ declareBrowserGlobalVar('__authorizeData', buildAuthorizeData(data)),
await getAsset('main.js'),
],
- styles: [await getAsset('main.css')],
+ styles: [
+ await getAsset('main.css'),
+ Html.dangerouslyCreate([buildBrandingCss(branding)]),
+ ],
title: 'Authorize',
body: html``,
})
diff --git a/packages/oauth-provider/src/output/send-error-page.ts b/packages/oauth-provider/src/output/send-error-page.ts
index 6390938c18d..2ba023176e8 100644
--- a/packages/oauth-provider/src/output/send-error-page.ts
+++ b/packages/oauth-provider/src/output/send-error-page.ts
@@ -1,23 +1,29 @@
import { IncomingMessage, ServerResponse } from 'node:http'
-import { html } from '@atproto/html'
+import { Html, html } from '@atproto/html'
-import { getAsset } from '../assets'
-import { buildErrorPayload, buildErrorStatus } from './build-error-payload'
-import { declareBrowserGlobalVar, sendWebApp } from './send-web-app'
+import { getAsset } from '../assets/index.js'
+import { Branding, buildBrandingCss, buildBrandingData } from './branding.js'
+import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
+import { declareBrowserGlobalVar, sendWebApp } from './send-web-app.js'
export async function sendErrorPage(
req: IncomingMessage,
res: ServerResponse,
err: unknown,
+ branding?: Branding,
): Promise {
return sendWebApp(req, res, {
status: buildErrorStatus(err),
scripts: [
- declareBrowserGlobalVar('__backendData', buildErrorPayload(err)),
+ declareBrowserGlobalVar('__brandingData', buildBrandingData(branding)),
+ declareBrowserGlobalVar('__errorData', buildErrorPayload(err)),
await getAsset('main.js'),
],
- styles: [await getAsset('main.css')],
+ styles: [
+ await getAsset('main.css'),
+ Html.dangerouslyCreate([buildBrandingCss(branding)]),
+ ],
title: 'Error',
body: html``,
})
diff --git a/packages/oauth-provider/tailwind.config.js b/packages/oauth-provider/tailwind.config.js
index 89eabe180e4..6a011d407b2 100644
--- a/packages/oauth-provider/tailwind.config.js
+++ b/packages/oauth-provider/tailwind.config.js
@@ -2,6 +2,9 @@
export default {
content: ['src/assets/app/**/*.{js,ts,jsx,tsx}'],
theme: {
+ colors: {
+ primary: 'rgb(var(--color-primary) / )',
+ },
extend: {},
},
plugins: [],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f5777f1700d..c0c8f3abb86 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -765,9 +765,6 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
- react-error-boundary:
- specifier: ^4.0.13
- version: 4.0.13(react@18.2.0)
rollup:
specifier: ^4.10.0
version: 4.12.0
@@ -12176,15 +12173,6 @@ packages:
scheduler: 0.23.0
dev: true
- /react-error-boundary@4.0.13(react@18.2.0):
- resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==}
- peerDependencies:
- react: '>=16.13.1'
- dependencies:
- '@babel/runtime': 7.22.10
- react: 18.2.0
- dev: true
-
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true