kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: support ?next= urls for login/signup pages
rodzic
4de5519c4e
commit
28479f72a1
|
@ -18,11 +18,15 @@ export function registerV1GitHubOAuthCallback(
|
|||
const redirectUri = entry.redirectUri
|
||||
assert(entry.redirectUri, 400, 'Redirect URI not found')
|
||||
|
||||
const url = new URL(
|
||||
`${redirectUri}?${new URLSearchParams(query).toString()}`
|
||||
).toString()
|
||||
logger.info('GitHub auth callback', query, '=>', url)
|
||||
const url = new URL(redirectUri)
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
url.searchParams.set(key, value)
|
||||
}
|
||||
|
||||
return c.redirect(url)
|
||||
logger.info('GitHub auth callback', query, '=>', url.toString(), {
|
||||
rawUrl: redirectUri,
|
||||
query
|
||||
})
|
||||
return c.redirect(url.toString())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ const relevantStripeEvents = new Set<Stripe.Event.Type>([
|
|||
|
||||
export function registerV1StripeWebhook(app: HonoApp) {
|
||||
return app.post('webhooks/stripe', async (ctx) => {
|
||||
const logger = ctx.get('logger')
|
||||
const body = await ctx.req.text()
|
||||
const signature = ctx.req.header('Stripe-Signature')
|
||||
assert(signature, 400, 'missing signature')
|
||||
|
@ -46,6 +47,8 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
return ctx.json({ status: 'ok' })
|
||||
}
|
||||
|
||||
logger.info('stripe webhook', event.type, event.data?.object)
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case 'customer.subscription.updated': {
|
||||
|
@ -55,11 +58,11 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
assert(userId, 400, 'missing metadata userId')
|
||||
assert(projectId, 400, 'missing metadata projectId')
|
||||
|
||||
// logger.info(event.type, {
|
||||
// userId,
|
||||
// projectId,
|
||||
// status: subscription.status
|
||||
// })
|
||||
logger.info('stripe webhook', event.type, {
|
||||
userId,
|
||||
projectId,
|
||||
status: subscription.status
|
||||
})
|
||||
|
||||
const consumer = await db.query.consumers.findFirst({
|
||||
where: and(
|
||||
|
@ -77,6 +80,7 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
consumer.stripeStatus = subscription.status
|
||||
setConsumerStripeSubscriptionStatus(consumer)
|
||||
|
||||
// TODO: update plan
|
||||
await db
|
||||
.update(schema.consumers)
|
||||
.set({
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
'use client'
|
||||
|
||||
import { sanitizeSearchParams } from '@agentic/platform-core'
|
||||
import { redirect, RedirectType, useSearchParams } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useUnauthenticatedAgentic } from '@/components/agentic-provider'
|
||||
import {
|
||||
useNextUrl,
|
||||
useUnauthenticatedAgentic
|
||||
} from '@/components/agentic-provider'
|
||||
import { LoadingIndicator } from '@/components/loading-indicator'
|
||||
import { toastError } from '@/lib/notifications'
|
||||
|
||||
export function SuccessPage({
|
||||
export function OAuthSuccessCallback({
|
||||
provider:
|
||||
// TODO
|
||||
// TODO: make generic using this provider instead of hard-coding github
|
||||
_provider
|
||||
}: {
|
||||
provider: string
|
||||
|
@ -17,30 +21,34 @@ export function SuccessPage({
|
|||
const searchParams = useSearchParams()
|
||||
const code = searchParams.get('code')
|
||||
const ctx = useUnauthenticatedAgentic()
|
||||
const nextUrl = useNextUrl()
|
||||
|
||||
useEffect(() => {
|
||||
;(async function () {
|
||||
if (!ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
// TODO
|
||||
throw new Error('Missing code or challenge')
|
||||
}
|
||||
|
||||
if (!ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make generic using `provider`
|
||||
try {
|
||||
await ctx.api.exchangeOAuthCodeWithGitHub({ code })
|
||||
} catch (err) {
|
||||
await toastError(err, { label: 'Auth error' })
|
||||
|
||||
return redirect('/login', RedirectType.replace)
|
||||
return redirect(
|
||||
`/login?${sanitizeSearchParams({ next: nextUrl }).toString()}`,
|
||||
RedirectType.replace
|
||||
)
|
||||
}
|
||||
|
||||
return redirect('/app', RedirectType.replace)
|
||||
return redirect(nextUrl || '/app', RedirectType.replace)
|
||||
})()
|
||||
}, [code, ctx])
|
||||
}, [code, ctx, nextUrl])
|
||||
|
||||
return <LoadingIndicator />
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
import { SuccessPage } from './success-page'
|
||||
import { OAuthSuccessCallback } from './oauth-success-callback'
|
||||
|
||||
export default async function Page({
|
||||
params
|
||||
|
@ -10,5 +10,5 @@ export default async function Page({
|
|||
const { provider } = await params
|
||||
assert(provider, 'Missing provider')
|
||||
|
||||
return <SuccessPage provider={provider} />
|
||||
return <OAuthSuccessCallback provider={provider} />
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { sanitizeSearchParams } from '@agentic/platform-core'
|
||||
import { isValidEmail, isValidPassword } from '@agentic/platform-validators'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
|
@ -7,7 +8,10 @@ import { redirect, RedirectType } from 'next/navigation'
|
|||
import { useCallback } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { useUnauthenticatedAgentic } from '@/components/agentic-provider'
|
||||
import {
|
||||
useNextUrl,
|
||||
useUnauthenticatedAgentic
|
||||
} from '@/components/agentic-provider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
@ -17,6 +21,15 @@ import { cn } from '@/lib/utils'
|
|||
|
||||
export default function LoginPage() {
|
||||
const ctx = useUnauthenticatedAgentic()
|
||||
const nextUrl = useNextUrl()
|
||||
|
||||
const onAuthWithGitHub = useCallback(async () => {
|
||||
const redirectUri = `${globalThis.location.origin}/auth/github/success?${sanitizeSearchParams({ next: nextUrl }).toString()}`
|
||||
|
||||
const url = await ctx!.api.initAuthFlowWithGitHub({ redirectUri })
|
||||
|
||||
redirect(url, RedirectType.push)
|
||||
}, [ctx, nextUrl])
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
|
@ -45,18 +58,10 @@ export default function LoginPage() {
|
|||
return
|
||||
}
|
||||
|
||||
return redirect('/app', RedirectType.push)
|
||||
return redirect(nextUrl || '/app', RedirectType.push)
|
||||
}
|
||||
})
|
||||
|
||||
const onAuthWithGitHub = useCallback(async () => {
|
||||
const url = await ctx!.api.initAuthFlowWithGitHub({
|
||||
redirectUri: `${globalThis.location.origin}/auth/github/success`
|
||||
})
|
||||
|
||||
redirect(url, RedirectType.push)
|
||||
}, [ctx])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
|
@ -168,7 +173,10 @@ export default function LoginPage() {
|
|||
|
||||
<div className='text-center text-xs'>
|
||||
Don't have an account?{' '}
|
||||
<a href='/signup' className='underline underline-offset-4'>
|
||||
<a
|
||||
href={`/signup?${sanitizeSearchParams({ next: nextUrl }).toString()}`}
|
||||
className='underline underline-offset-4'
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { sanitizeSearchParams } from '@agentic/platform-core'
|
||||
import {
|
||||
isValidEmail,
|
||||
isValidPassword,
|
||||
|
@ -11,7 +12,10 @@ import { redirect, RedirectType } from 'next/navigation'
|
|||
import { useCallback } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { useUnauthenticatedAgentic } from '@/components/agentic-provider'
|
||||
import {
|
||||
useNextUrl,
|
||||
useUnauthenticatedAgentic
|
||||
} from '@/components/agentic-provider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
@ -22,6 +26,14 @@ import { cn } from '@/lib/utils'
|
|||
export default function SignupPage() {
|
||||
// const [error] = useState<PasswordLoginError | undefined>(undefined)
|
||||
const ctx = useUnauthenticatedAgentic()
|
||||
const nextUrl = useNextUrl()
|
||||
|
||||
const onAuthWithGitHub = useCallback(async () => {
|
||||
const redirectUri = `${globalThis.location.origin}/auth/github/success?${sanitizeSearchParams({ next: nextUrl }).toString()}`
|
||||
const url = await ctx!.api.initAuthFlowWithGitHub({ redirectUri })
|
||||
|
||||
redirect(url, RedirectType.push)
|
||||
}, [ctx, nextUrl])
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
|
@ -60,18 +72,10 @@ export default function SignupPage() {
|
|||
return
|
||||
}
|
||||
|
||||
return redirect('/app', RedirectType.push)
|
||||
return redirect(nextUrl || '/app', RedirectType.push)
|
||||
}
|
||||
})
|
||||
|
||||
const onAuthWithGitHub = useCallback(async () => {
|
||||
const url = await ctx!.api.initAuthFlowWithGitHub({
|
||||
redirectUri: `${globalThis.location.origin}/auth/github/success`
|
||||
})
|
||||
|
||||
redirect(url, RedirectType.push)
|
||||
}, [ctx])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
|
@ -214,7 +218,10 @@ export default function SignupPage() {
|
|||
|
||||
<div className='text-center text-xs'>
|
||||
Already have an account?{' '}
|
||||
<a href='/login' className='underline underline-offset-4'>
|
||||
<a
|
||||
href={`/login?${sanitizeSearchParams({ next: nextUrl }).toString()}`}
|
||||
className='underline underline-offset-4'
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,13 @@ import {
|
|||
AgenticApiClient,
|
||||
type AuthSession
|
||||
} from '@agentic/platform-api-client'
|
||||
import { redirect, RedirectType } from 'next/navigation'
|
||||
import { sanitizeSearchParams } from '@agentic/platform-core'
|
||||
import {
|
||||
redirect,
|
||||
RedirectType,
|
||||
usePathname,
|
||||
useSearchParams
|
||||
} from 'next/navigation'
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
|
@ -124,10 +130,11 @@ export function useAgentic(): AgenticContextType | undefined {
|
|||
|
||||
export function useUnauthenticatedAgentic(): AgenticContextType | undefined {
|
||||
const ctx = useAgentic()
|
||||
const nextUrl = useNextUrl() || '/app'
|
||||
|
||||
if (ctx && ctx.isAuthenticated) {
|
||||
// console.log('REQUIRES UNAUTHENTICATED: redirecting to /app')
|
||||
redirect('/app', RedirectType.replace)
|
||||
console.log('REQUIRES NO AUTHENTICATION: redirecting to', nextUrl)
|
||||
return redirect(nextUrl, RedirectType.replace)
|
||||
}
|
||||
|
||||
return ctx
|
||||
|
@ -135,11 +142,37 @@ export function useUnauthenticatedAgentic(): AgenticContextType | undefined {
|
|||
|
||||
export function useAuthenticatedAgentic(): AgenticContextType | undefined {
|
||||
const ctx = useAgentic()
|
||||
const pathname = usePathname()
|
||||
|
||||
if (ctx && !ctx.isAuthenticated) {
|
||||
// console.log('REQUIRES AUTHENTICATED: redirecting to /login')
|
||||
redirect('/login', RedirectType.replace)
|
||||
if (pathname === '/logout') {
|
||||
console.log('LOGOUT SUCCESS: redirecting to /')
|
||||
return redirect('/', RedirectType.replace)
|
||||
}
|
||||
|
||||
console.log('REQUIRES AUTHENTICATION: redirecting to /login', {
|
||||
next: pathname
|
||||
})
|
||||
|
||||
return redirect(
|
||||
`/login?${sanitizeSearchParams({ next: pathname }).toString()}`,
|
||||
RedirectType.replace
|
||||
)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function useNextUrl(): string | undefined {
|
||||
const searchParams = useSearchParams()
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) {
|
||||
setIsMounted(true)
|
||||
return
|
||||
}
|
||||
}, [isMounted, setIsMounted])
|
||||
|
||||
return isMounted ? (searchParams.get('next') ?? undefined) : undefined
|
||||
}
|
||||
|
|
|
@ -15,6 +15,14 @@
|
|||
- **website**
|
||||
- marketing landing page
|
||||
- webapp
|
||||
- stripe
|
||||
- if user is subscribed to a plan, show that plan as selected
|
||||
- handle unauthenticated checkout flow => auth and then redirect to create a checkout session
|
||||
- will need a `redirect` url for `/login` and `/signup`
|
||||
- `/marketplace/projects/@{projectIdentifier}/checkout?plan={plan}`
|
||||
- data loading / react-query
|
||||
- don't retry queries on 401/403
|
||||
- mimic logic from `ky` for automatic retries
|
||||
- stripe
|
||||
- stripe checkout
|
||||
- stripe billing portal
|
||||
|
|
Ładowanie…
Reference in New Issue