kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: web auth routes done for mvp
rodzic
eaa0e3aa33
commit
222b1d43f6
|
@ -0,0 +1,4 @@
|
|||
## TODO
|
||||
|
||||
- better auth error handling
|
||||
- display validation errors in auth forms
|
|
@ -35,14 +35,24 @@ function SuccessPage({
|
|||
throw new Error('Missing code or challenge')
|
||||
}
|
||||
|
||||
if (!ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make generic using `provider`
|
||||
const authSession = await ctx!.api.exchangeOAuthCodeWithGitHub({
|
||||
code
|
||||
})
|
||||
try {
|
||||
const authSession = await ctx.api.exchangeOAuthCodeWithGitHub({
|
||||
code
|
||||
})
|
||||
|
||||
console.log('AUTH SUCCESS', { authSession })
|
||||
console.log('AUTH SUCCESS', { authSession })
|
||||
} catch (err) {
|
||||
console.error('AUTH ERROR', err)
|
||||
|
||||
redirect('/app', RedirectType.replace)
|
||||
return redirect('/login', RedirectType.replace)
|
||||
}
|
||||
|
||||
return redirect('/app', RedirectType.replace)
|
||||
})()
|
||||
}, [code, ctx])
|
||||
|
||||
|
|
|
@ -177,43 +177,6 @@ a:hover .link {
|
|||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
@apply mb-8;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
@apply mb-7;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 450;
|
||||
margin-bottom: 1em;
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 400;
|
||||
@apply mb-5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 350;
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: 350;
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
main section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import type { PasswordLoginError } from '@agentic/openauth/provider/password'
|
||||
import { isValidEmail, isValidPassword } from '@agentic/platform-validators'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
import { redirect, RedirectType } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
@ -35,7 +36,7 @@ export default function LoginPage() {
|
|||
password: ''
|
||||
},
|
||||
validators: {
|
||||
onBlurAsync: z.object({
|
||||
onChange: z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
|
@ -50,13 +51,14 @@ export default function LoginPage() {
|
|||
password: value.password
|
||||
})
|
||||
|
||||
// TODO
|
||||
console.log('login success', res)
|
||||
redirect('/app', RedirectType.push)
|
||||
} catch (err) {
|
||||
// TODO
|
||||
console.error('login error', err)
|
||||
throw err
|
||||
}
|
||||
|
||||
return redirect('/app', RedirectType.push)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -68,129 +70,127 @@ export default function LoginPage() {
|
|||
redirect(url, RedirectType.push)
|
||||
}, [ctx])
|
||||
|
||||
// TODO:
|
||||
if (!ctx) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<h1 className='my-0! text-center text-balance leading-snug md:leading-none'>
|
||||
Log In
|
||||
</h1>
|
||||
<div className='flex-col flex-1 items-center justify-center w-full max-w-xs'>
|
||||
<form
|
||||
className={cn('flex flex-col gap-6 w-full')}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
void form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col items-center gap-2 text-center'>
|
||||
<h1 className='text-2xl font-bold'>Login to your account</h1>
|
||||
<p className='text-muted-foreground text-sm text-balance'>
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 items-center justify-center'>
|
||||
<div className='w-full max-w-xs'>
|
||||
<form
|
||||
className={cn('flex flex-col gap-6')}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
void form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col items-center gap-2 text-center'>
|
||||
<h1 className='text-2xl font-bold'>Login to your account</h1>
|
||||
<p className='text-muted-foreground text-sm text-balance'>
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-6'>
|
||||
<form.Field
|
||||
name='email'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<Label htmlFor={field.name}>Email</Label>
|
||||
|
||||
<div className='grid gap-6'>
|
||||
<form.Field
|
||||
name='email'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<Label htmlFor={field.name}>Email</Label>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='email'
|
||||
required
|
||||
placeholder='Email'
|
||||
autoFocus={!error}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e: any) =>
|
||||
field.handleChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<form.Field
|
||||
name='password'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<div className='flex items-center'>
|
||||
<Label htmlFor={field.name}>Password</Label>
|
||||
|
||||
<a
|
||||
href='/forgot-password'
|
||||
className='ml-auto text-sm underline-offset-4 hover:underline'
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
required
|
||||
placeholder='Password'
|
||||
autoFocus={error?.type === 'invalid_password'}
|
||||
autoComplete='current-password'
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<form.Subscribe
|
||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||
children={([canSubmit, isSubmitting]) => (
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!canSubmit || !ctx}
|
||||
className='w-full'
|
||||
>
|
||||
{isSubmitting ? '...' : 'Login'}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
onClick={onAuthWithGitHub}
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'
|
||||
fill='currentColor'
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='email'
|
||||
required
|
||||
placeholder='Email'
|
||||
autoComplete='email'
|
||||
autoFocus={!error}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e: any) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</svg>
|
||||
Login with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<form.Field
|
||||
name='password'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<div className='flex items-center'>
|
||||
<Label htmlFor={field.name}>Password</Label>
|
||||
|
||||
<a
|
||||
href='/forgot-password'
|
||||
className='ml-auto text-xs underline-offset-4 hover:underline'
|
||||
tabIndex={-1}
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
required
|
||||
placeholder='Password'
|
||||
autoFocus={error?.type === 'invalid_password'}
|
||||
autoComplete='current-password'
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<form.Subscribe
|
||||
selector={(state) => [
|
||||
state.canSubmit,
|
||||
state.isSubmitting,
|
||||
state.isTouched
|
||||
]}
|
||||
children={([canSubmit, isSubmitting, isTouched]) => (
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!(isTouched && canSubmit && ctx)}
|
||||
className='w-full'
|
||||
>
|
||||
{isSubmitting && <Loader2Icon className='animate-spin' />}
|
||||
|
||||
<span>Login</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='text-center text-sm'>
|
||||
Don't have an account?{' '}
|
||||
<a href='/signup' className='underline underline-offset-4'>
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
onClick={onAuthWithGitHub}
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span>Login with GitHub</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='text-center text-xs'>
|
||||
Don't have an account?{' '}
|
||||
<a href='/signup' className='underline underline-offset-4'>
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function LogoutPage() {
|
|||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (ctx) {
|
||||
await ctx.api.logout()
|
||||
ctx.logout()
|
||||
redirect('/', RedirectType.replace)
|
||||
}
|
||||
})()
|
||||
|
|
|
@ -1,120 +1,227 @@
|
|||
'use client'
|
||||
|
||||
import type {
|
||||
PasswordRegisterError,
|
||||
PasswordRegisterState
|
||||
} from '@agentic/openauth/provider/password'
|
||||
import { useState } from 'react'
|
||||
import type { PasswordLoginError } from '@agentic/openauth/provider/password'
|
||||
import {
|
||||
isValidEmail,
|
||||
isValidPassword,
|
||||
isValidUsername
|
||||
} from '@agentic/platform-validators'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
import { redirect, RedirectType } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { authCopy } from '@/lib/auth-copy'
|
||||
import { useUnauthenticatedAgentic } from '@/components/agentic-provider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { GitHubIcon } from '@/icons/github'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function SignupPage() {
|
||||
// TODO
|
||||
const [error, setError] = useState<PasswordRegisterError | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [state, setState] = useState<PasswordRegisterState>({ type: 'start' })
|
||||
const [form, setForm] = useState<FormData | undefined>(undefined)
|
||||
export default function LoginPage() {
|
||||
const [error] = useState<PasswordLoginError | undefined>(undefined)
|
||||
const ctx = useUnauthenticatedAgentic()
|
||||
|
||||
const emailError = ['invalid_email', 'email_taken'].includes(
|
||||
error?.type || ''
|
||||
)
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
repeat: ''
|
||||
},
|
||||
validators: {
|
||||
onChange: z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.refine((email) => isValidEmail(email)),
|
||||
username: z.string().refine((username) => isValidUsername(username)),
|
||||
password: z.string().refine((password) => isValidPassword(password)),
|
||||
repeat: z.string().refine((password) => isValidPassword(password))
|
||||
})
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
if (value.password !== value.repeat) {
|
||||
toast.error('Passwords do not match')
|
||||
return
|
||||
}
|
||||
|
||||
const passwordError = [
|
||||
'invalid_password',
|
||||
'password_mismatch',
|
||||
'validation_error'
|
||||
].includes(error?.type || '')
|
||||
const res = await ctx!.api.signUpWithPassword({
|
||||
email: value.email,
|
||||
username: value.username,
|
||||
password: value.password
|
||||
})
|
||||
|
||||
console.log('signup success', res)
|
||||
} catch (err: any) {
|
||||
console.error('Signup error', err.message)
|
||||
toast.error('Signup error', err.message)
|
||||
return
|
||||
}
|
||||
|
||||
return redirect('/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>
|
||||
<h1 className='my-0! text-center text-balance leading-snug md:leading-none'>
|
||||
{authCopy.register}
|
||||
</h1>
|
||||
<div className='flex-col flex-1 items-center justify-center w-full max-w-xs'>
|
||||
<form
|
||||
className={cn('flex flex-col gap-6 w-full')}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
void form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col items-center gap-2 text-center'>
|
||||
<h1 className='text-2xl font-bold'>Create an account</h1>
|
||||
<p className='text-muted-foreground text-sm text-balance'>
|
||||
Enter your info below to create an account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form data-component='form' method='post'>
|
||||
{/* <FormAlert
|
||||
message={
|
||||
error?.type
|
||||
? error.type === 'validation_error'
|
||||
? (error.message ?? authCopy?.[`error_${error.type}`])
|
||||
: authCopy?.[`error_${error.type}`]
|
||||
: undefined
|
||||
}
|
||||
/> */}
|
||||
|
||||
{state.type === 'start' && (
|
||||
<>
|
||||
<input type='hidden' name='action' value='register' />
|
||||
|
||||
<input
|
||||
data-component='input'
|
||||
autoFocus={!error || emailError}
|
||||
type='email'
|
||||
<div className='grid gap-6'>
|
||||
<form.Field
|
||||
name='email'
|
||||
value={!emailError ? form?.get('email')?.toString() : ''}
|
||||
required
|
||||
placeholder={authCopy.input_email}
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<Label htmlFor={field.name}>Email</Label>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='email'
|
||||
required
|
||||
placeholder='Email'
|
||||
autoComplete='email'
|
||||
autoFocus={!error}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e: any) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<input
|
||||
data-component='input'
|
||||
autoFocus={passwordError}
|
||||
type='password'
|
||||
<form.Field
|
||||
name='username'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<Label htmlFor={field.name}>Username</Label>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='text'
|
||||
required
|
||||
placeholder='Username'
|
||||
autoComplete='username'
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e: any) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<form.Field
|
||||
name='password'
|
||||
placeholder={authCopy.input_password}
|
||||
required
|
||||
value={!passwordError ? form?.get('password')?.toString() : ''}
|
||||
autoComplete='new-password'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<Label htmlFor={field.name}>Password</Label>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
required
|
||||
placeholder='Password'
|
||||
autoFocus={error?.type === 'invalid_password'}
|
||||
autoComplete='new-password'
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<input
|
||||
data-component='input'
|
||||
type='password'
|
||||
<form.Field
|
||||
name='repeat'
|
||||
required
|
||||
autoFocus={passwordError}
|
||||
placeholder={authCopy.input_repeat}
|
||||
autoComplete='new-password'
|
||||
children={(field) => (
|
||||
<div className='grid gap-3'>
|
||||
<Label htmlFor={field.name}>Repeat password</Label>
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
required
|
||||
placeholder='Password'
|
||||
autoComplete='new-password'
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button data-component='button'>
|
||||
{authCopy.button_continue}
|
||||
</button>
|
||||
<form.Subscribe
|
||||
selector={(state) => [
|
||||
state.canSubmit,
|
||||
state.isSubmitting,
|
||||
state.isTouched
|
||||
]}
|
||||
children={([canSubmit, isSubmitting, isTouched]) => (
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!(isTouched && canSubmit && ctx)}
|
||||
className='w-full'
|
||||
>
|
||||
{isSubmitting && <Loader2Icon className='animate-spin' />}
|
||||
<span>Sign up</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div data-component='form-footer'>
|
||||
<span>
|
||||
{authCopy.login_prompt}{' '}
|
||||
<a data-component='link' href='/login'>
|
||||
{authCopy.login}
|
||||
</a>
|
||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.type === 'code' && (
|
||||
<>
|
||||
<input type='hidden' name='action' value='verify' />
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
onClick={onAuthWithGitHub}
|
||||
>
|
||||
<GitHubIcon />
|
||||
|
||||
<input
|
||||
data-component='input'
|
||||
autoFocus
|
||||
name='code'
|
||||
minLength={6}
|
||||
maxLength={6}
|
||||
required
|
||||
placeholder={authCopy.input_code}
|
||||
autoComplete='one-time-code'
|
||||
/>
|
||||
<span>Sign up with GitHub</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button data-component='button'>
|
||||
{authCopy.button_continue}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
<div className='text-center text-xs'>
|
||||
Already have an account?{' '}
|
||||
<a href='/login' className='underline underline-offset-4'>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@ import { redirect, RedirectType } from 'next/navigation'
|
|||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
|
@ -17,17 +18,22 @@ import { useLocalStorage } from 'react-use'
|
|||
|
||||
import * as config from '@/lib/config'
|
||||
|
||||
type AgenticContext = {
|
||||
type AgenticContextType = {
|
||||
api: AgenticApiClient
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
const AgenticContext = createContext<AgenticContext | undefined>(undefined)
|
||||
const AgenticContext = createContext<AgenticContextType | undefined>(undefined)
|
||||
|
||||
export function AgenticProvider({ children }: { children: ReactNode }) {
|
||||
const [authSession, setAuthSession] = useLocalStorage<AuthSession>(
|
||||
const [authSession, setAuthSession] = useLocalStorage<AuthSession | null>(
|
||||
'agentic-auth-session'
|
||||
)
|
||||
const agenticContext = useRef<AgenticContext>({
|
||||
const logout = useCallback(() => {
|
||||
setAuthSession(null)
|
||||
}, [setAuthSession])
|
||||
|
||||
const agenticContext = useRef<AgenticContextType>({
|
||||
api: new AgenticApiClient({
|
||||
apiBaseUrl: config.apiBaseUrl,
|
||||
onUpdateAuth: (updatedAuthSession) => {
|
||||
|
@ -43,12 +49,19 @@ export function AgenticProvider({ children }: { children: ReactNode }) {
|
|||
console.log('auth session not updated')
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
logout
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
console.log('setting session from localStorage', authSession?.token)
|
||||
agenticContext.current.api.authSession = authSession
|
||||
console.log('updating session from localStorage', authSession?.token)
|
||||
if (agenticContext.current) {
|
||||
if (authSession) {
|
||||
agenticContext.current.api.authSession = authSession
|
||||
} else {
|
||||
agenticContext.current.api.authSession = undefined
|
||||
}
|
||||
}
|
||||
}, [agenticContext, authSession])
|
||||
|
||||
return (
|
||||
|
@ -58,7 +71,7 @@ export function AgenticProvider({ children }: { children: ReactNode }) {
|
|||
)
|
||||
}
|
||||
|
||||
export function useAgentic(): AgenticContext {
|
||||
export function useAgentic(): AgenticContextType {
|
||||
const ctx = useContext(AgenticContext)
|
||||
|
||||
if (!ctx) {
|
||||
|
@ -68,7 +81,7 @@ export function useAgentic(): AgenticContext {
|
|||
return ctx
|
||||
}
|
||||
|
||||
export function useUnauthenticatedAgentic(): AgenticContext | undefined {
|
||||
export function useUnauthenticatedAgentic(): AgenticContextType | undefined {
|
||||
const ctx = useAgentic()
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
|
@ -86,7 +99,7 @@ export function useUnauthenticatedAgentic(): AgenticContext | undefined {
|
|||
return isMounted ? ctx : undefined
|
||||
}
|
||||
|
||||
export function useAuthenticatedAgentic(): AgenticContext | undefined {
|
||||
export function useAuthenticatedAgentic(): AgenticContextType | undefined {
|
||||
const ctx = useAgentic()
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
|
@ -97,7 +110,7 @@ export function useAuthenticatedAgentic(): AgenticContext | undefined {
|
|||
}
|
||||
|
||||
if (!ctx.api.isAuthenticated) {
|
||||
redirect('/', RedirectType.replace)
|
||||
redirect('/login', RedirectType.replace)
|
||||
}
|
||||
}, [isMounted, setIsMounted, ctx])
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
import { ActiveLink } from '@/components/active-link'
|
||||
import { GitHub } from '@/icons/github'
|
||||
import { Twitter } from '@/icons/twitter'
|
||||
import { GitHubIcon } from '@/icons/github'
|
||||
import { TwitterIcon } from '@/icons/twitter'
|
||||
import { copyright, githubUrl, twitterUrl } from '@/lib/config'
|
||||
|
||||
export function Footer() {
|
||||
|
@ -54,7 +54,8 @@ export function Footer() {
|
|||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Twitter className='h-4 w-4' />
|
||||
<TwitterIcon className='h-4 w-4' />
|
||||
|
||||
<span>Twitter</span>
|
||||
</Link>
|
||||
|
||||
|
@ -64,7 +65,8 @@ export function Footer() {
|
|||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<GitHub className='h-4 w-4' />
|
||||
<GitHubIcon className='h-4 w-4' />
|
||||
|
||||
<span>GitHub</span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react'
|
|||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot='input'
|
||||
className={cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
|
@ -0,0 +1,24 @@
|
|||
'use client'
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot='label'
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
|
@ -1,7 +1,12 @@
|
|||
export function GitHub({ className }: { className?: string }) {
|
||||
export function GitHubIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox='0 0 24 24' fill='currentcolor' className={className}>
|
||||
<path d='M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22'></path>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
fill='currentColor'
|
||||
className={className}
|
||||
>
|
||||
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function Twitter({ className }: { className?: string }) {
|
||||
export function TwitterIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
role='img'
|
||||
|
|
Ładowanie…
Reference in New Issue