mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
feat: use OTP instead of email verification on signup
This commit is contained in:
@@ -43,7 +43,28 @@ export default function LoginPage() {
|
||||
|
||||
if (err.message?.includes('not verified')) {
|
||||
errorMessage =
|
||||
'Please verify your email before signing in. Check your inbox for the verification link.'
|
||||
'Please verify your email before signing in. Would you like to resend the verification code?'
|
||||
|
||||
// Offer to send a verification code and redirect to verification page
|
||||
const resendVerification = window.confirm(
|
||||
'Your email is not verified. Would you like to resend the verification code and go to the verification page?'
|
||||
)
|
||||
|
||||
if (resendVerification) {
|
||||
try {
|
||||
// Send a new verification OTP
|
||||
await client.emailOtp.sendVerificationOtp({
|
||||
email,
|
||||
type: 'email-verification',
|
||||
})
|
||||
|
||||
// Redirect to the verify page
|
||||
router.push(`/verify?email=${encodeURIComponent(email)}`)
|
||||
return
|
||||
} catch (verifyErr) {
|
||||
errorMessage = 'Failed to send verification code. Please try again later.'
|
||||
}
|
||||
}
|
||||
} else if (err.message?.includes('not found')) {
|
||||
errorMessage = 'No account found with this email. Please sign up first.'
|
||||
} else if (err.message?.includes('invalid password')) {
|
||||
|
||||
@@ -35,7 +35,10 @@ export default function SignupPage() {
|
||||
|
||||
try {
|
||||
await client.signUp.email({ email, password, name })
|
||||
router.push('/verify-request')
|
||||
|
||||
// No need to manually send OTP as it's handled by sendVerificationOnSignUp
|
||||
// Redirect directly to the verify page instead of verify-request
|
||||
router.push(`/verify?email=${encodeURIComponent(email)}`)
|
||||
} catch (err: any) {
|
||||
let errorMessage = 'Failed to create account'
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export default function VerifyRequestPage() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h1 className="text-2xl font-bold text-center mb-8">Sim Studio</h1>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Check your email</CardTitle>
|
||||
<CardDescription>
|
||||
A verification link has been sent to your email address.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Click the link in the email to verify your account. If you don't see it, check your
|
||||
spam folder.
|
||||
</p>
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/login">Back to login</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -3,52 +3,167 @@
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
|
||||
// Extract the content into a separate component
|
||||
function VerifyContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
|
||||
const { addNotification } = useNotificationStore()
|
||||
const [otp, setOtp] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isVerified, setIsVerified] = useState(false)
|
||||
const [isSendingInitialOtp, setIsSendingInitialOtp] = useState(false)
|
||||
|
||||
// Get email from URL query param
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('token')
|
||||
if (!token) {
|
||||
setStatus('error')
|
||||
return
|
||||
const emailParam = searchParams.get('email')
|
||||
if (emailParam) {
|
||||
setEmail(decodeURIComponent(emailParam))
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
client
|
||||
.verifyEmail({ query: { token } })
|
||||
.then(() => {
|
||||
setStatus('success')
|
||||
// Redirect to workflow page after a short delay
|
||||
setTimeout(() => router.push('/w'), 2000)
|
||||
// Send initial OTP code if this is the first load
|
||||
useEffect(() => {
|
||||
if (email && !isSendingInitialOtp) {
|
||||
setIsSendingInitialOtp(true)
|
||||
// Send verification OTP on initial page load
|
||||
client.emailOtp
|
||||
.sendVerificationOtp({
|
||||
email,
|
||||
type: 'email-verification',
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send initial verification code:', error)
|
||||
addNotification?.(
|
||||
'error',
|
||||
'Failed to send verification code. Please use the resend button.',
|
||||
null
|
||||
)
|
||||
})
|
||||
}
|
||||
}, [email, isSendingInitialOtp, addNotification])
|
||||
|
||||
// Enable the verify button when all 6 digits are entered
|
||||
const isOtpComplete = otp.length === 6
|
||||
|
||||
async function verifyCode() {
|
||||
if (!isOtpComplete || !email) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Call the verification API with the OTP code
|
||||
await client.emailOtp.verifyEmail({
|
||||
email,
|
||||
otp,
|
||||
})
|
||||
.catch(() => setStatus('error'))
|
||||
}, [searchParams, router])
|
||||
|
||||
setIsVerified(true)
|
||||
|
||||
// Redirect to dashboard after a short delay
|
||||
setTimeout(() => router.push('/w'), 2000)
|
||||
} catch (error: any) {
|
||||
let errorMessage = 'Verification failed. Please check your code and try again.'
|
||||
|
||||
if (error.message?.includes('expired')) {
|
||||
errorMessage = 'The verification code has expired. Please request a new one.'
|
||||
} else if (error.message?.includes('invalid')) {
|
||||
errorMessage = 'Invalid verification code. Please check and try again.'
|
||||
} else if (error.message?.includes('attempts')) {
|
||||
errorMessage = 'Too many failed attempts. Please request a new code.'
|
||||
}
|
||||
|
||||
addNotification?.('error', errorMessage, null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function resendCode() {
|
||||
if (!email) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
client.emailOtp
|
||||
.sendVerificationOtp({
|
||||
email,
|
||||
type: 'email-verification',
|
||||
})
|
||||
.then(() => {})
|
||||
.catch(() => {
|
||||
addNotification?.(
|
||||
'error',
|
||||
'Failed to resend verification code. Please try again later.',
|
||||
null
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{status === 'loading' && 'Verifying your email...'}
|
||||
{status === 'success' && 'Email verified!'}
|
||||
{status === 'error' && 'Verification failed'}
|
||||
</CardTitle>
|
||||
<CardTitle>{isVerified ? 'Email Verified!' : 'Verify your email'}</CardTitle>
|
||||
<CardDescription>
|
||||
{status === 'loading' && 'Please wait while we verify your email address.'}
|
||||
{status === 'success' && 'You will be redirected to the dashboard shortly.'}
|
||||
{status === 'error' && 'The verification link is invalid or has expired.'}
|
||||
{isVerified
|
||||
? 'Your email has been verified. Redirecting to dashboard...'
|
||||
: `A verification code has been sent to ${email || 'your email'}`}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{status === 'error' && (
|
||||
<Button onClick={() => router.push('/login')} className="w-full">
|
||||
Back to login
|
||||
|
||||
{!isVerified && (
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Enter the 6-digit code from your email to verify your account. If you don't see it,
|
||||
check your spam folder.
|
||||
</p>
|
||||
<div className="flex justify-center py-4">
|
||||
<InputOTP maxLength={6} value={otp} onChange={setOtp} disabled={isLoading}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
<Button onClick={verifyCode} className="w-full" disabled={!isOtpComplete || isLoading}>
|
||||
{isLoading ? 'Verifying...' : 'Verify Email'}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
{!isVerified && (
|
||||
<CardFooter className="flex justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Didn't receive a code?{' '}
|
||||
<button
|
||||
className="text-primary hover:underline font-medium"
|
||||
onClick={resendCode}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
</p>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -62,9 +177,12 @@ export default function VerifyPage() {
|
||||
fallback={
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Loading...</CardTitle>
|
||||
<CardDescription>Please wait while we load the verification page.</CardDescription>
|
||||
<CardTitle>Verify your email</CardTitle>
|
||||
<CardDescription>Loading verification page...</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-10 animate-pulse bg-gray-200 rounded"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
>
|
||||
|
||||
70
components/ui/input-otp.tsx
Normal file
70
components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { OTPInput, OTPInputContext } from 'input-otp'
|
||||
import { Minus } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
||||
containerClassName
|
||||
)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = 'InputOTP'
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = 'InputOTPGroup'
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-12 w-12 items-center justify-center border-y border-r border-input text-lg font-semibold shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
isActive && 'z-10 ring-2 ring-primary ring-offset-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-6 w-px animate-caret-blink bg-primary duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = 'InputOTPSlot'
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" className="text-muted-foreground" {...props}>
|
||||
<Minus />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = 'InputOTPSeparator'
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
@@ -1,8 +1,8 @@
|
||||
import { genericOAuthClient } from 'better-auth/client/plugins'
|
||||
import { emailOTPClient, genericOAuthClient } from 'better-auth/client/plugins'
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
|
||||
export const client = createAuthClient({
|
||||
plugins: [genericOAuthClient()],
|
||||
plugins: [genericOAuthClient(), emailOTPClient()],
|
||||
})
|
||||
export const { useSession } = client
|
||||
|
||||
|
||||
76
lib/auth.ts
76
lib/auth.ts
@@ -2,7 +2,7 @@ import { headers } from 'next/headers'
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
||||
import { nextCookies } from 'better-auth/next-js'
|
||||
import { genericOAuth } from 'better-auth/plugins'
|
||||
import { emailOTP, genericOAuth } from 'better-auth/plugins'
|
||||
import { Resend } from 'resend'
|
||||
import { db } from '@/db'
|
||||
import * as schema from '@/db/schema'
|
||||
@@ -42,6 +42,7 @@ export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
sendVerificationOnSignUp: false,
|
||||
throwOnMissingCredentials: true,
|
||||
throwOnInvalidCredentials: true,
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
@@ -62,41 +63,48 @@ export const auth = betterAuth({
|
||||
}
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url, token }, request) => {
|
||||
try {
|
||||
if (!user.email) {
|
||||
throw new Error('User email is required')
|
||||
}
|
||||
|
||||
const result = await resend.emails.send({
|
||||
from: 'Sim Studio <onboarding@simstudio.ai>',
|
||||
to: user.email,
|
||||
subject: 'Verify your email',
|
||||
html: `
|
||||
<h2>Welcome to Sim Studio!</h2>
|
||||
<p>Click the link below to verify your email:</p>
|
||||
<a href="${url}">${url}</a>
|
||||
<p>If you didn't create an account, you can safely ignore this email.</p>
|
||||
`,
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Failed to send verification email')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending verification email:', {
|
||||
error,
|
||||
user: user.email,
|
||||
url,
|
||||
token,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
nextCookies(),
|
||||
emailOTP({
|
||||
sendVerificationOTP: async (data: {
|
||||
email: string
|
||||
otp: string
|
||||
type: 'sign-in' | 'email-verification' | 'forget-password'
|
||||
}) => {
|
||||
try {
|
||||
if (!data.email) {
|
||||
throw new Error('Email is required')
|
||||
}
|
||||
|
||||
const result = await resend.emails.send({
|
||||
from: 'Sim Studio <onboarding@simstudio.ai>',
|
||||
to: data.email,
|
||||
subject: 'Verify your email',
|
||||
html: `
|
||||
<h2>Welcome to Sim Studio!</h2>
|
||||
<p>Your verification code is:</p>
|
||||
<h1 style="font-size: 32px; letter-spacing: 2px; text-align: center; padding: 16px; background-color: #f8f9fa; border-radius: 4px;">${data.otp}</h1>
|
||||
<p>This code will expire in 15 minutes.</p>
|
||||
<p>If you didn't create an account, you can safely ignore this email.</p>
|
||||
`,
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Failed to send verification code')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending verification code:', {
|
||||
error,
|
||||
email: data.email,
|
||||
otp: data.otp,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
sendVerificationOnSignUp: true,
|
||||
otpLength: 6, // Explicitly set the OTP length
|
||||
expiresIn: 15 * 60, // 15 minutes in seconds
|
||||
}),
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
|
||||
221
package-lock.json
generated
221
package-lock.json
generated
@@ -42,6 +42,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.469.0",
|
||||
@@ -1258,6 +1259,111 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz",
|
||||
"integrity": "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz",
|
||||
"integrity": "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz",
|
||||
"integrity": "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz",
|
||||
"integrity": "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz",
|
||||
"integrity": "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz",
|
||||
"integrity": "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz",
|
||||
"integrity": "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.6.0",
|
||||
"license": "MIT",
|
||||
@@ -6559,6 +6665,16 @@
|
||||
"version": "1.3.8",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/input-otp": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
|
||||
"integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/inquirer": {
|
||||
"version": "8.2.6",
|
||||
"license": "MIT",
|
||||
@@ -11516,111 +11632,6 @@
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz",
|
||||
"integrity": "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz",
|
||||
"integrity": "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz",
|
||||
"integrity": "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz",
|
||||
"integrity": "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz",
|
||||
"integrity": "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz",
|
||||
"integrity": "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz",
|
||||
"integrity": "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.469.0",
|
||||
|
||||
@@ -107,6 +107,10 @@ export default {
|
||||
filter: 'opacity(0.7)',
|
||||
},
|
||||
},
|
||||
'caret-blink': {
|
||||
'0%,70%,100%': { opacity: '1' },
|
||||
'20%,50%': { opacity: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'slide-down': 'slide-down 0.3s ease-out',
|
||||
@@ -115,6 +119,7 @@ export default {
|
||||
'fade-up': 'fade-up 0.5s ease-out forwards',
|
||||
'rocket-pulse': 'rocket-pulse 1.5s ease-in-out infinite',
|
||||
'run-glow': 'run-glow 2s ease-in-out infinite',
|
||||
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user