feature(auth): added basic scaffolding for auth using better-auth, drizzle ORM, resend for email verification

This commit is contained in:
Waleed Latif
2025-02-16 02:02:01 -08:00
parent ad546519b6
commit 51f26d4e1c
19 changed files with 2653 additions and 4 deletions
+111
View File
@@ -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>
)
}
+101
View File
@@ -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>
)
}
+32
View File
@@ -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>
)
}
+59
View File
@@ -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>
)
}
+4
View File
@@ -0,0 +1,4 @@
import { toNextJsHandler } from 'better-auth/next-js'
import { auth } from '@/lib/auth'
export const { GET, POST } = toNextJsHandler(auth.handler)
+39
View File
@@ -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',
},
],
},
}
+8
View File
@@ -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;
*/
+312
View File
@@ -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": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1739697832964,
"tag": "0000_careless_black_knight",
"breakpoints": true
}
]
}
+21
View File
@@ -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],
}),
}))
+73
View File
@@ -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),
]
)
+51
View File
@@ -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'),
})
+10
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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
+15
View File
@@ -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*'],
}
+1654 -2
View File
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -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",