Skip to content

Commit

Permalink
feat: prepare for branding page
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed Mar 10, 2024
1 parent 393e365 commit 87c2286
Show file tree
Hide file tree
Showing 18 changed files with 454 additions and 434 deletions.
303 changes: 197 additions & 106 deletions packages/oauth-provider/src/assets/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,137 +1,228 @@
import { useMemo, useState } from 'react'

import type { AuthorizeData } from './backend-data'
import type { AuthorizeData, BrandingData } from './backend-data'
import { cookies } from './cookies'
import { Account, Session } from './types'

import { AcceptPage } from './pages/accept-page'
import { AcceptForm } from './components/accept-form'
import { AccountPicker } from './components/account-picker'
import { PageLayout } from './components/page-layout'
import { SessionSelectionPage } from './pages/session-selector-page'

export function App({
requestUri,
clientId,
clientMetadata,
csrfCookie,
loginHint,
sessions: initialSessions,
}: AuthorizeData) {
import { SignInForm, SignInFormOutput } from './components/sign-in-form'
import { Api, SignInResponse } from './lib/api'

type AppProps = {
authorizeData: AuthorizeData
brandingData?: BrandingData
}

// TODO: show brandingData when "flow" is null

export function App({ authorizeData }: AppProps) {
const {
requestUri,
clientId,
clientMetadata,
csrfCookie,
loginHint,
sessions: initialSessions,
} = authorizeData

const csrfToken = useMemo(() => cookies[csrfCookie], [csrfCookie])
const [isDone, setIsDone] = useState(false)
// const [flow, setFlow] = useState<null | 'sign-in' | 'sign-up'>(
// loginHint != null ? 'sign-in' : null,
// )
const [sessions, setSessions] = useState(initialSessions)
const [selectedSession, onSession] = useState<Session | null>(null)

const updateSession = useMemo(() => {
return (account: Account, consentRequired: boolean): Session => {
const sessionIdx = sessions.findIndex(
(s) => s.account.sub === account.sub,
)
if (sessionIdx === -1) {
const newSession: Session = {
initiallySelected: false,
account,
loginRequired: false,
consentRequired,
}
setSessions([...sessions, newSession])
return newSession
} else {
const curSession = sessions[sessionIdx]
const newSession: Session = {
...curSession,
initiallySelected: false,
account,
consentRequired,
loginRequired: false,
}
setSessions([
...sessions.slice(0, sessionIdx),
newSession,
...sessions.slice(sessionIdx + 1),
])
return newSession
}
}
}, [sessions, setSessions])

const onLogin = async (credentials: {
username: string
password: string
remember: boolean
}) => {
const r = await fetch('/oauth/authorize/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
mode: 'same-origin',
body: JSON.stringify({
csrf_token: csrfToken,
request_uri: requestUri,
client_id: clientId,
credentials,
}),
})
const json = await r.json()
if (!r.ok) throw new Error(json.error || 'Error', { cause: json })

const { account, info } = json
const consentRequired = !info.authorizedClients.includes(clientId)
const session = updateSession(account, consentRequired)
onSession(session)
}
const accounts = useMemo(() => sessions.map((s) => s.account), [sessions])
const [showSignInForm, setShowSignInForm] = useState(sessions.length === 0)
const [sub, setSub] = useState(
sessions.find((s) => s.initiallySelected)?.account.sub || null,
)
const clearSub = () => setSub(null)

const authorizeAccept = async (account: Account) => {
setIsDone(true)
const session = useMemo(() => {
return sub ? sessions.find((s) => s.account.sub == sub) : undefined
}, [sub, sessions])

const url = new URL('/oauth/authorize/accept', window.origin)
url.searchParams.set('request_uri', requestUri)
url.searchParams.set('account_sub', account.sub)
url.searchParams.set('client_id', clientId)
url.searchParams.set('csrf_token', csrfToken)
const api = useMemo(
() => new Api(requestUri, clientId, csrfToken),
[requestUri, clientId, csrfToken],
)

const handleSignIn = async (credentials: SignInFormOutput) => {
const { account, info } = await api.signIn(credentials)

const newSessions = updateSessions(sessions, { account, info }, clientId)

window.location.href = url.href
setSessions(newSessions)
setSub(account.sub)
}

const authorizeReject = () => {
const handleAccept = async (account: Account) => {
setIsDone(true)
api.accept(account)
}

const url = new URL('/oauth/authorize/reject', window.origin)
url.searchParams.set('request_uri', requestUri)
url.searchParams.set('client_id', clientId)
url.searchParams.set('csrf_token', csrfToken)

window.location.href = url.href
const handleReject = () => {
setIsDone(true)
api.reject()
}

if (isDone) {
// TODO
return (
<PageLayout title="Login complete">You are being redirected</PageLayout>
)
}

if (selectedSession) {
// if (!flow) {
// return (
// <PageLayout title="Sign in as...">
// <button onClick={() => setFlow('sign-in')}>Sign in</button>
// <button onClick={() => setFlow('sign-up')}>Sign up</button>
// <button onClick={handleReject}>Abort</button>
// </PageLayout>
// )
// }

if (session && !session.loginRequired) {
const { account } = session
return (
<AcceptPage
onBack={() => onSession(null)}
onAccept={() => authorizeAccept(selectedSession.account)}
onReject={() => authorizeReject()}
account={selectedSession.account}
clientId={clientId}
clientMetadata={clientMetadata}
/>
<PageLayout
title="Authorize"
subtitle={
<>
Grant access to your{' '}
<b>{account.preferred_username || account.email || account.sub}</b>{' '}
account.
</>
}
>
<AcceptForm
className="max-w-lg w-full"
clientId={clientId}
clientMetadata={clientMetadata}
account={account}
onBack={clearSub}
onAccept={() => handleAccept(account)}
onReject={handleReject}
/>
</PageLayout>
)
}

if (session && session.loginRequired) {
return (
<PageLayout title="Sign in" subtitle="Confirm your password to continue">
<SignInForm
className="max-w-lg w-full"
remember={true}
username={session.account.preferred_username}
usernameReadonly={true}
onSubmit={handleSignIn}
onCancel={clearSub}
cancelLabel={'Back' /* to account picker */}
/>
</PageLayout>
)
}

if (loginHint) {
return (
<PageLayout title="Sign in" subtitle="Enter your password">
<SignInForm
className="max-w-lg w-full"
username={loginHint}
usernameReadonly={true}
onSubmit={handleSignIn}
onCancel={handleReject}
cancelLabel="Back"
/>
</PageLayout>
)
}

if (sessions.length === 0) {
return (
<PageLayout title="Sign in" subtitle="Enter your username and password">
<SignInForm
className="max-w-lg w-full"
onSubmit={handleSignIn}
onCancel={handleReject}
cancelLabel="Back"
/>
</PageLayout>
)
}

if (showSignInForm) {
return (
<PageLayout title="Sign in" subtitle="Enter your username and password">
<SignInForm
className="max-w-lg w-full"
onSubmit={handleSignIn}
onCancel={() => setShowSignInForm(false)}
cancelLabel={'Back' /* to account picker */}
/>
</PageLayout>
)
}

return (
<SessionSelectionPage
clientId={clientId}
clientMetadata={clientMetadata}
loginHint={loginHint}
sessions={sessions}
onLogin={onLogin}
onSession={onSession}
onBack={() => authorizeReject()}
backLabel="Deny access"
/>
<PageLayout
title="Sign in as..."
subtitle={
<>
Select an account to access to{' '}
<b>
{clientMetadata.client_name ||
clientMetadata.client_uri ||
clientId}
</b>
.
</>
}
>
<AccountPicker
className="max-w-lg w-full"
accounts={accounts}
onAccount={(a) => setSub(a.sub)}
onOther={() => setShowSignInForm(true)}
onBack={handleReject}
backLabel="Back"
/>
</PageLayout>
)
}

function updateSessions(
sessions: readonly Session[],
{ account, info }: SignInResponse,
clientId: string,
): Session[] {
const consentRequired = !info.authorizedClients.includes(clientId)

const sessionIdx = sessions.findIndex((s) => s.account.sub === account.sub)
if (sessionIdx === -1) {
const newSession: Session = {
initiallySelected: false,
account,
loginRequired: false,
consentRequired,
}
return [...sessions, newSession]
} else {
const curSession = sessions[sessionIdx]
const newSession: Session = {
...curSession,
initiallySelected: false,
account,
consentRequired,
loginRequired: false,
}
return [
...sessions.slice(0, sessionIdx),
newSession,
...sessions.slice(sessionIdx + 1),
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export function AcceptForm({
onClick={() => onBack()}
className="bg-transparent font-light text-primary rounded-md py-2"
>
Select another account
Back
</button>
)}

<div className="flex-auto"></div>

<button
Expand Down
Loading

0 comments on commit 87c2286

Please sign in to comment.