diff --git a/.gitignore b/.gitignore index 5ef6a520..a8d1d34e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,11 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# others +notebooks +.idea +.obsidian +martin + +/src/generated/prisma diff --git a/README.md b/README.md index e215bc4c..dd93b34c 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,6 @@ First, run the development server: ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/package-lock.json b/package-lock.json index 425958f3..4ae8245a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "bayesbond", "version": "0.1.0", "dependencies": { + "@auth/prisma-adapter": "^2.10.0", + "@prisma/client": "^6.12.0", + "bcryptjs": "^3.0.2", "next": "15.4.4", + "next-auth": "^5.0.0-beta.29", "react": "19.1.0", "react-dom": "19.1.0" }, @@ -49,6 +53,45 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.10.0.tgz", + "integrity": "sha512-EliOQoTjGK87jWWqnJvlQjbR4PjQZQqtwRwPAe108WwT9ubuuJJIrL68aNnQr4hFESz6P7SEX2bZy+y2yL37Gw==", + "dependencies": { + "@auth/core": "0.40.0" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -904,6 +947,35 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@prisma/client": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.12.0.tgz", + "integrity": "sha512-wn98bJ3Cj6edlF4jjpgXwbnQIo/fQLqqQHPk2POrZPxTlhY3+n90SSIF3LMRVa8VzRFC/Gec3YKJRxRu+AIGVA==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2036,6 +2108,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3834,6 +3914,14 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4390,6 +4478,32 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.29.tgz", + "integrity": "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==", + "dependencies": { + "@auth/core": "0.40.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0-0", + "nodemailer": "^6.6.5", + "react": "^18.2.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4417,6 +4531,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/oauth4webapi": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.0.tgz", + "integrity": "sha512-OwXPTXjKPOldTpAa19oksrX9TYHA0rt+VcUFTkJ7QKwgmevPpNm9Cn5vFZUtIo96FiU6AfPuUUGzoXqgOzibWg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4686,6 +4808,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5538,7 +5677,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 57c0ec53..8c9fd6b0 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,23 @@ "lint": "next lint" }, "dependencies": { + "@auth/prisma-adapter": "^2.10.0", + "@prisma/client": "^6.12.0", + "bcryptjs": "^3.0.2", + "next": "15.4.4", + "next-auth": "^5.0.0-beta.29", "react": "19.1.0", - "react-dom": "19.1.0", - "next": "15.4.4" + "react-dom": "19.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.4.4", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..d58fc4ec --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,57 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? @map("email_verified") + image String? + password String? + accounts Account[] + sessions Session[] +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 00000000..95cc195e --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts new file mode 100644 index 00000000..ce558f79 --- /dev/null +++ b/src/app/api/auth/signup/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { hash } from 'bcryptjs'; + +export async function POST(req: Request) { + try { + const { name, email, password } = await req.json(); + + // Check if user already exists + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + return NextResponse.json( + { message: 'User already exists' }, + { status: 400 } + ); + } + + // Hash password + const hashedPassword = await hash(password, 12); + + // Create user + const user = await prisma.user.create({ + data: { + name, + email, + password: hashedPassword, + }, + }); + + const { password: _, ...userWithoutPassword } = user; + + return NextResponse.json( + { user: userWithoutPassword, message: 'User created successfully' }, + { status: 201 } + ); + } catch (error) { + console.error('Signup error:', error); + return NextResponse.json( + { message: 'Something went wrong' }, + { status: 500 } + ); + } +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 00000000..b4f6ec36 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { signOut, useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function DashboardPage() { + const { data: session, status } = useSession(); + const router = useRouter(); + + useEffect(() => { + if (status === 'unauthenticated') { + router.push('/login'); + } + }, [status, router]); + + if (status === 'loading') { + return ( +
+
+
+ ); + } + + if (!session) { + return null; // Will be redirected by the useEffect + } + + return ( +
+ + +
+
+
+

Welcome, {session.user?.name || 'User'}!

+

You are now logged in.

+

Email: {session.user?.email}

+
+
+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87eb..30a47661 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { Providers } from "@/providers"; import "./globals.css"; const geistSans = Geist({ @@ -13,21 +14,19 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "BayesBond", + description: "Your application description", }; export default function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { +}) { return ( - - {children} + + {children} ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 00000000..ee8b513d --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { signIn } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +export default function LoginPage() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + try { + const result = await signIn('credentials', { + redirect: false, + email, + password, + }); + + if (result?.error) { + setError(result.error); + return; + } + + router.push('/dashboard'); + } catch (err) { + setError('An error occurred. Please try again.'); + } + }; + + return ( +
+
+
+

+ Sign in to your account +

+

+ Or{' '} + + create a new account + +

+
+ {error && ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ )} +
+ +
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+
+ + +
+ +
+ + Forgot your password? + +
+
+ +
+ +
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index a9328942..41ca9387 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,67 @@ -import Image from "next/image"; +import Link from 'next/link'; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - +
+
+
+

+ Welcome to BayesBond +

+

+ A modern web application with authentication and more. +

+
+ + Get Started + + + Sign In + +
-
- + +
+

Features

+
+
+
+ + + +
+

Fast

+

Built with Next.js for optimal performance and SEO.

+
+ +
+
+ + + +
+

Secure

+

Built-in authentication with NextAuth.js.

+
+ +
+
+ + + +
+

Modern

+

Built with the latest web technologies.

+
+
+
+
); } diff --git a/src/app/page_init.tsx b/src/app/page_init.tsx new file mode 100644 index 00000000..2dde49eb --- /dev/null +++ b/src/app/page_init.tsx @@ -0,0 +1,103 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + src/app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ +
+ + Vercel logomark + Deploy now + + + Read our docs + +
+
+ +
+ ); +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 00000000..092b77ab --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +export default function SignupPage() { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + const response = await fetch('/api/auth/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Something went wrong'); + } + + // Redirect to login page after successful signup + router.push('/login'); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Create a new account +

+

+ Or{' '} + + sign in to your existing account + +

+
+ {error && ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ )} +
+
+
+ + setName(e.target.value)} + /> +
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ +
+
+
+
+ ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 00000000..e55ce818 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,70 @@ +import { PrismaAdapter } from "@auth/prisma-adapter"; +import { NextAuthOptions } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { prisma } from "./prisma"; +import { compare } from 'bcryptjs'; + +export const authOptions: NextAuthOptions = { + adapter: PrismaAdapter(prisma), + session: { + strategy: "jwt", + }, + pages: { + signIn: "/login", + signOut: "/", + error: "/login", + }, + providers: [ + CredentialsProvider({ + name: "Credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) { + throw new Error("Please enter your email and password"); + } + + const user = await prisma.user.findUnique({ + where: { + email: credentials.email, + }, + }); + + if (!user || !user.password) { + throw new Error("No user found with this email"); + } + + const isValid = await compare(credentials.password, user.password); + + if (!isValid) { + throw new Error("Incorrect password"); + } + + return { + id: user.id, + email: user.email, + name: user.name, + image: user.image, + }; + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.id = user.id; + } + return token; + }, + async session({ session, token }) { + if (session?.user) { + session.user.id = token.id as string; + } + return session; + }, + }, + secret: process.env.NEXTAUTH_SECRET, + debug: process.env.NODE_ENV === "development", +} as const; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 00000000..5c11b3c1 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,26 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + // eslint-disable-next-line no-var + var prisma: PrismaClient | undefined; +} + +export const prisma = global.prisma || new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + global.prisma = prisma; +} + +async function connectDB() { + try { + await prisma.$connect(); + console.log('🚀 Database connected successfully'); + } catch (error) { + console.log(error); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +export default connectDB; diff --git a/src/providers.tsx b/src/providers.tsx new file mode 100644 index 00000000..52df575d --- /dev/null +++ b/src/providers.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { SessionProvider } from 'next-auth/react'; + +export function Providers({ children }: { children: React.ReactNode }) { + return {children}; +}