Skip to content

Commit

Permalink
feat(oauth): support dual login method
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed Jun 19, 2024
1 parent 9323aac commit 1c79b49
Show file tree
Hide file tree
Showing 15 changed files with 901 additions and 370 deletions.
6 changes: 3 additions & 3 deletions app/configure/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ButtonGroup, ButtonPrimary, ButtonSecondary } from '@/common/buttons'
import { Checkbox, Textarea } from '@/common/forms'
import { isDarkModeEnabled } from '@/common/useColorScheme'
import { useSyncedState } from '@/lib/useSyncedState'
import { useAuthDid, usePdsAgent } from '@/shell/AuthContext'
import { useAuthContext, useAuthDid } from '@/shell/AuthContext'
import { useConfigurationContext } from '@/shell/ConfigurationContext'

const BrowserReactJsonView = dynamic(() => import('react-json-view'), {
Expand Down Expand Up @@ -90,7 +90,7 @@ function ConfigureDetails() {

function RecordInitStep({ repo }: { repo: string }) {
const [checked, setChecked] = useState(false)
const pdsAgent = usePdsAgent()
const { pdsAgent } = useAuthContext()
const { reconfigure } = useConfigurationContext()

const createInitialRecord = useMutation({
Expand Down Expand Up @@ -153,7 +153,7 @@ function RecordEditStep({
record: AppBskyLabelerService.Record
repo: string
}) {
const pdsAgent = usePdsAgent()
const { pdsAgent } = useAuthContext()
const { reconfigure } = useConfigurationContext()

const [editorMode, setEditorMode] = useState<'json' | 'plain'>('json')
Expand Down
6 changes: 5 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { QueryClientProvider } from '@tanstack/react-query'
import { ToastContainer } from 'react-toastify'

import { isDarkModeEnabled } from '@/common/useColorScheme'
import { PLC_DIRECTORY_URL, SOCIAL_APP_URL } from '@/lib/constants'
import { AuthProvider } from '@/shell/AuthContext'
import { CommandPaletteRoot } from '@/shell/CommandPalette/Root'
import { ConfigurationProvider } from '@/shell/ConfigurationContext'
Expand Down Expand Up @@ -47,7 +48,10 @@ export default function RootLayout({
closeOnClick
/>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<AuthProvider
plcDirectoryUrl={PLC_DIRECTORY_URL}
handleResolver={SOCIAL_APP_URL}
>
<ConfigurationProvider>
<CommandPaletteRoot>
<Shell>{children}</Shell>
Expand Down
4 changes: 2 additions & 2 deletions app/repositories/[id]/[...record]/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ReportPanel } from '@/reports/ReportPanel'
import { CollectionId } from '@/reports/helpers/subject'
import { RecordView } from '@/repositories/RecordView'
import { useCreateReport } from '@/repositories/createReport'
import { usePdsAgent } from '@/shell/AuthContext'
import { useAuthContext } from '@/shell/AuthContext'
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'

Expand Down Expand Up @@ -56,7 +56,7 @@ export default function RecordViewPageContent({
params: { id: string; record: string[] }
}) {
const labelerAgent = useLabelerAgent()
const pdsAgent = usePdsAgent()
const { pdsAgent } = useAuthContext()

const emitEvent = useEmitEvent()
const createReport = useCreateReport()
Expand Down
32 changes: 32 additions & 0 deletions components/common/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { classNames } from '@/lib/util'
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'

export type TabView<ViewName> = {
view: ViewName
Expand Down Expand Up @@ -61,3 +62,34 @@ function Tab<ViewName>({
</span>
)
}

export function TabsPanel<ViewName>({
views,
fallback,
...props
}: {
views: (TabView<ViewName> & { content: ReactNode })[]
fallback?: ReactNode
} & HTMLAttributes<HTMLDivElement>) {
const available = views.filter((v) => v.content)
const defaultView = available[0]?.view

const [currentView, setCurrentView] = useState(defaultView)

const current = available.find((v) => v.view === currentView)

useEffect(() => {
if (!current?.view) setCurrentView(defaultView)
}, [current?.view, defaultView])

return (
<div {...props}>
<Tabs
views={available}
currentView={currentView}
onSetCurrentView={setCurrentView}
/>
{current?.content ?? fallback}
</div>
)
}
4 changes: 2 additions & 2 deletions components/common/labels/useLabelerDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ComAtprotoLabelDefs } from '@atproto/api'
import { useQuery } from '@tanstack/react-query'

import { usePdsAgent } from '@/shell/AuthContext'
import { useAuthContext } from '@/shell/AuthContext'
import { ExtendedLabelerServiceDef } from './util'

export const useLabelerServiceDef = (did: string) => {
const pdsAgent = usePdsAgent()
const { pdsAgent } = useAuthContext()

const { data: labelerDef } = useQuery({
queryKey: ['labelerDef', { did, for: pdsAgent.getDid() }],
Expand Down
115 changes: 76 additions & 39 deletions components/shell/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,103 +1,140 @@
'use client'

import { AppBskyActorDefs, BskyAgent } from '@atproto/api'
import {
BrowserOAuthClientLoadOptions,
isLoopbackHost,
} from '@atproto/oauth-client-browser'
import { useQuery } from '@tanstack/react-query'
import { isLoopbackHost } from '@atproto/oauth-client-browser'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { usePathname, useRouter } from 'next/navigation'
import { createContext, useContext, useMemo, useState } from 'react'
import { createContext, ReactNode, useContext, useMemo } from 'react'

import { Loading } from '@/common/Loader'
import { SetupModal } from '@/common/SetupModal'
import { PLC_DIRECTORY_URL, SOCIAL_APP_URL } from '@/lib/constants'
import { useOAuth } from '@/lib/useOAuth'
import { queryClient } from 'components/QueryClient'
import { useAtpAuth } from './auth/atp/useAtpAuth'
import { useOAuth, UseOAuthOptions } from './auth/oauth/useOAuth'
import { AuthForm } from './AuthForm'

export type Profile = AppBskyActorDefs.ProfileViewDetailed

export type AuthContextData = {
export type AuthContext = {
pdsAgent: BskyAgent
signOut: () => Promise<void>
}

const AuthContext = createContext<AuthContextData | null>(null)
const AuthContext = createContext<AuthContext | null>(null)

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
export const AuthProvider = ({
children,
...options
}: {
children: ReactNode
} & UseOAuthOptions) => {
const pathname = usePathname()
const router = useRouter()
const queryClient = useQueryClient()

const {
isLoginPopup,
isInitializing,
client: oauthClient,
agent: oauthAgent,
signIn: oauthSignIn,
} = useOAuth({
...options,

const [oauthOptions] = useState<BrowserOAuthClientLoadOptions>(() => ({
clientId:
typeof window === 'undefined' || isLoopbackHost(window.location.hostname)
? 'http://localhost'
: new URL(`/oauth-client.json`, window.location.origin).href,
plcDirectoryUrl: PLC_DIRECTORY_URL,
handleResolver: SOCIAL_APP_URL,
}))

const { isInitialized, pdsAgent, signIn, signOut } = useOAuth(oauthOptions, {
getState: async () => {
// Save the current path before signing in
return pathname
},
onSignedIn: async (agent, state) => {
// Restore the previous path after signing in
if (state) router.push(state)
},
onSignedOut: async () => {
// Clear all cached queries when signing out
queryClient.removeQueries()
},

// use "https://ozone.example.com/oauth-client.json" in prod and a loopback URL in dev
clientId:
options['clientId'] ??
(options['clientMetadata'] == null
? typeof window === 'undefined' ||
isLoopbackHost(window.location.hostname)
? undefined
: new URL(`/oauth-client.json`, window.location.origin).href
: undefined),
})

const value = useMemo<AuthContextData | null>(
() => (pdsAgent ? { pdsAgent, signOut } : null),
[pdsAgent, signOut],
const {
session: atpSession,
signIn: atpSignIn,
signOut: atpSignOut,
} = useAtpAuth()

const value = useMemo<AuthContext | null>(
() =>
oauthAgent
? {
pdsAgent: new BskyAgent(oauthAgent),
signOut: () => oauthAgent.signOut(),
}
: atpSession
? {
pdsAgent: new BskyAgent(atpSession),
signOut: atpSignOut,
}
: null,
[atpSession, oauthAgent, atpSignOut],
)

if (!isInitialized) {
if (isLoginPopup) {
return (
<SetupModal>
<Loading message="Logging in..." />
<p className="text-center">This window can be closed</p>
</SetupModal>
)
}

if (isInitializing) {
return (
<SetupModal>
<Loading message="Initializing..." />
</SetupModal>
)
}

if (!value) {
return (
<SetupModal>
<AuthForm signIn={signIn} />
<AuthForm
atpSignIn={atpSignIn}
oauthSignIn={oauthClient ? oauthSignIn : undefined}
/>
</SetupModal>
)
}

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}

export const useAuthContext = () => {
export function useAuthContext(): AuthContext {
const context = useContext(AuthContext)
if (context) return context

throw new Error(`useAuthContext() must be used within a <AuthProvider />`)
}

export function usePdsAgent() {
const { pdsAgent } = useAuthContext()
return pdsAgent
throw new Error(`useAuthContext() must be used within an <AuthProvider />`)
}

export const useAuthDid = () => {
return usePdsAgent().getDid()
const { pdsAgent } = useAuthContext()
return pdsAgent.getDid()
}

export const useAuthProfileQuery = () => {
const pds = usePdsAgent()
const did = pds.getDid()
const { pdsAgent } = useAuthContext()
const did = pdsAgent.getDid()

return useQuery({
queryKey: ['profile', did],
queryFn: async () => pds.getProfile({ actor: did }),
queryFn: async () => pdsAgent.getProfile({ actor: did }),
})
}

Expand Down
Loading

0 comments on commit 1c79b49

Please sign in to comment.