mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feature(auth): added basic scaffolding for auth using better-auth, drizzle ORM, resend for email verification
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Github } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { client } from '@/lib/auth-client'
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setIsLoading(true)
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
|
||||
try {
|
||||
await client.signIn.email({ email, password })
|
||||
router.push('/dashboard')
|
||||
} catch (err) {
|
||||
setError('Invalid email or password')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function signInWithGithub() {
|
||||
try {
|
||||
await client.signIn.social({ provider: 'github' })
|
||||
} catch (err) {
|
||||
setError('Failed to sign in with GitHub')
|
||||
}
|
||||
}
|
||||
|
||||
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>Welcome back</CardTitle>
|
||||
<CardDescription>Enter your credentials to access your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<Button variant="outline" onClick={signInWithGithub} className="w-full">
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" name="password" type="password" required />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p className="text-sm text-gray-500 text-center w-full">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" className="text-primary hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { NotificationList } from '@/app/w/components/notifications/notifications'
|
||||
|
||||
export default function SignupPage() {
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { addNotification } = useNotificationStore()
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
const name = formData.get('name') as string
|
||||
|
||||
try {
|
||||
await client.signUp.email({ email, password, name })
|
||||
router.push('/verify-request')
|
||||
} catch (err: any) {
|
||||
// Handle specific error messages
|
||||
let errorMessage = 'Something went wrong. Please try again.'
|
||||
|
||||
if (err.message?.includes('Password is too short')) {
|
||||
errorMessage = 'Password must be at least 8 characters long'
|
||||
} else if (err.message?.includes('existing email')) {
|
||||
errorMessage = 'An account with this email already exists'
|
||||
}
|
||||
|
||||
addNotification('error', errorMessage, null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
|
||||
<NotificationList />
|
||||
<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>Create an account</CardTitle>
|
||||
<CardDescription>Enter your details to get started</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={onSubmit}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" name="name" type="text" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" name="password" type="password" required />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-4">
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Creating account...' : 'Create account'}
|
||||
</Button>
|
||||
<p className="text-sm text-gray-500">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import { 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 { client } from '@/lib/auth-client'
|
||||
|
||||
export default function VerifyPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('token')
|
||||
if (!token) {
|
||||
setStatus('error')
|
||||
return
|
||||
}
|
||||
|
||||
client
|
||||
.verifyEmail({ query: { token } })
|
||||
.then(() => {
|
||||
setStatus('success')
|
||||
// Redirect to dashboard after a short delay
|
||||
setTimeout(() => router.push('/w/1'), 2000)
|
||||
})
|
||||
.catch(() => setStatus('error'))
|
||||
}, [searchParams, router])
|
||||
|
||||
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>
|
||||
{status === 'loading' && 'Verifying your email...'}
|
||||
{status === 'success' && 'Email verified!'}
|
||||
{status === 'error' && 'Verification failed'}
|
||||
</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.'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{status === 'error' && (
|
||||
<Button onClick={() => router.push('/login')} className="w-full">
|
||||
Back to login
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { toNextJsHandler } from 'better-auth/next-js'
|
||||
import { auth } from '@/lib/auth'
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth.handler)
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BrainIcon } from '@/components/icons'
|
||||
import { BlockConfig } from '../types'
|
||||
|
||||
export const MemoryBlock: BlockConfig = {
|
||||
type: 'memory',
|
||||
toolbar: {
|
||||
title: 'Memory',
|
||||
description: 'Add memory store',
|
||||
bgColor: '#FF65BF',
|
||||
icon: BrainIcon,
|
||||
category: 'blocks',
|
||||
},
|
||||
tools: {
|
||||
access: [],
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
code: { type: 'string', required: true },
|
||||
timeout: { type: 'number', required: false },
|
||||
memoryLimit: { type: 'number', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
result: 'any',
|
||||
stdout: 'string',
|
||||
executionTime: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'code',
|
||||
type: 'code',
|
||||
layout: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
|
||||
const connectionString = process.env.DATABASE_URL!
|
||||
|
||||
// Disable prefetch as it is not supported for "Transaction" pool mode
|
||||
const client = postgres(connectionString, { prepare: false })
|
||||
export const db = drizzle(client)
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Current sql file was generated after introspecting the database
|
||||
-- If you want to run this migration please uncomment this code before executing migrations
|
||||
/*
|
||||
CREATE TABLE "verification" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp,
|
||||
"updated_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean NOT NULL,
|
||||
"image" text,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp,
|
||||
"refresh_token_expires_at" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "session" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"user_id" text NOT NULL,
|
||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
*/
|
||||
@@ -0,0 +1,312 @@
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"prevId": "",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"columns": ["email"],
|
||||
"nullsNotDistinct": false,
|
||||
"name": "user_email_unique"
|
||||
}
|
||||
},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"schemaTo": "public",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"schemaTo": "public",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"columns": ["token"],
|
||||
"nullsNotDistinct": false,
|
||||
"name": "session_token_unique"
|
||||
}
|
||||
},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1739697832964,
|
||||
"tag": "0000_careless_black_knight",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { relations } from 'drizzle-orm/relations'
|
||||
import { account, session, user } from './schema'
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
accounts: many(account),
|
||||
sessions: many(session),
|
||||
}))
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}))
|
||||
@@ -0,0 +1,73 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { boolean, foreignKey, pgTable, text, timestamp, unique } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const verification = pgTable('verification', {
|
||||
id: text().primaryKey().notNull(),
|
||||
identifier: text().notNull(),
|
||||
value: text().notNull(),
|
||||
expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }),
|
||||
})
|
||||
|
||||
export const user = pgTable(
|
||||
'user',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
name: text().notNull(),
|
||||
email: text().notNull(),
|
||||
emailVerified: boolean('email_verified').notNull(),
|
||||
image: text(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(),
|
||||
},
|
||||
(table) => [unique('user_email_unique').on(table.email)]
|
||||
)
|
||||
|
||||
export const account = pgTable(
|
||||
'account',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id').notNull(),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { mode: 'string' }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { mode: 'string' }),
|
||||
scope: text(),
|
||||
password: text(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'account_user_id_user_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
]
|
||||
)
|
||||
|
||||
export const session = pgTable(
|
||||
'session',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(),
|
||||
token: text().notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id').notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'session_user_id_user_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
unique('session_token_unique').on(table.token),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
import { boolean, integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const user = pgTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull().unique(),
|
||||
emailVerified: boolean('email_verified').notNull(),
|
||||
image: text('image'),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
})
|
||||
|
||||
export const session = pgTable('session', {
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
})
|
||||
|
||||
export const account = pgTable('account', {
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
})
|
||||
|
||||
export const verification = pgTable('verification', {
|
||||
id: text('id').primaryKey(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
createdAt: timestamp('created_at'),
|
||||
updatedAt: timestamp('updated_at'),
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Config } from 'drizzle-kit'
|
||||
|
||||
export default {
|
||||
schema: './db/schema.ts',
|
||||
out: './db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
} satisfies Config
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
|
||||
export const client = createAuthClient()
|
||||
export const { useSession } = client
|
||||
|
||||
// Export commonly used hooks and methods
|
||||
export const { signIn, signUp, signOut } = client
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
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 { Resend } from 'resend'
|
||||
import { db } from '@/db'
|
||||
import * as schema from '@/db/schema'
|
||||
|
||||
type EmailHandler = {
|
||||
user: { email: string }
|
||||
url: string
|
||||
}
|
||||
|
||||
// If there is no resend key, it might be a local dev environment
|
||||
// In that case, we don't want to send emails and just log them
|
||||
const resend = process.env.RESEND_API_KEY
|
||||
? new Resend(process.env.RESEND_API_KEY)
|
||||
: { emails: { send: async (...args: any[]) => console.log(args) } }
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: 'pg',
|
||||
schema,
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
sendVerificationEmail: async ({ user, url }: EmailHandler) => {
|
||||
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>
|
||||
`,
|
||||
})
|
||||
},
|
||||
sendResetPassword: async ({ user, url }: EmailHandler) => {
|
||||
await resend.emails.send({
|
||||
from: 'Sim Studio <team@simstudio.ai>',
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
html: `
|
||||
<h2>Reset Your Password</h2>
|
||||
<p>Click the link below to reset your password:</p>
|
||||
<a href="${url}">${url}</a>
|
||||
<p>If you didn't request this, you can safely ignore this email.</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
},
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
},
|
||||
},
|
||||
plugins: [nextCookies()],
|
||||
pages: {
|
||||
signIn: '/login',
|
||||
signUp: '/signup',
|
||||
error: '/error',
|
||||
verify: '/verify',
|
||||
verifyRequest: '/verify-request',
|
||||
},
|
||||
})
|
||||
|
||||
// Server-side auth helpers
|
||||
export async function getSession() {
|
||||
return await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
})
|
||||
}
|
||||
|
||||
export const signIn = auth.api.signInEmail
|
||||
export const signUp = auth.api.signUpEmail
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSessionCookie } from 'better-auth'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const sessionCookie = getSessionCookie(request)
|
||||
if (!sessionCookie) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// TODO: Add protected routes
|
||||
export const config = {
|
||||
matcher: ['/dashboard/:path*'],
|
||||
}
|
||||
Generated
+1654
-2
File diff suppressed because it is too large
Load Diff
+10
-2
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
@@ -11,7 +11,9 @@
|
||||
"test:watch": "jest --watch",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"db:push": "drizzle-kit push:pg",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
@@ -27,18 +29,22 @@
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"better-auth": "^1.1.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next": "15.1.3",
|
||||
"openai": "^4.83.0",
|
||||
"postgres": "^3.4.5",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"resend": "^4.1.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.1"
|
||||
@@ -51,6 +57,8 @@
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
|
||||
Reference in New Issue
Block a user