This commit is contained in:
MartinBraquet
2025-07-27 17:36:52 +02:00
parent 94e4691b0e
commit 2e6958f79b
90 changed files with 10747 additions and 871 deletions

12
.gitignore vendored
View File

@@ -31,7 +31,8 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env
.env.local
# vercel
.vercel
@@ -39,12 +40,3 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# others
notebooks
.idea
.obsidian
martin
/src/generated/prisma
*.db

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="dev" uuid="a53b1101-3c0d-4a17-9889-5a84449c92c5">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/prisma/dev.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false" editorAttributes="WARNING_ATTRIBUTES" />
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/meeting-intellectuals.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="$USER_HOME$/miniconda3" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="$USER_HOME$/miniconda3" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/meeting-intellectuals.iml" filepath="$PROJECT_DIR$/.idea/meeting-intellectuals.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
.obsidian/app.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

1
.obsidian/appearance.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

31
.obsidian/core-plugins.json vendored Normal file
View File

@@ -0,0 +1,31 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"webviewer": false
}

148
.obsidian/hotkeys.json vendored Normal file
View File

@@ -0,0 +1,148 @@
{
"insert-template": [
{
"modifiers": [
"Alt"
],
"key": "T"
}
],
"editor:delete-paragraph": [
{
"modifiers": [
"Mod"
],
"key": "Y"
}
],
"obsidian-languagetool-plugin:ltcheck-text": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "C"
}
],
"obsidian-languagetool-plugin:ltaccept-suggestion-1": [
{
"modifiers": [
"Mod",
"Shift"
],
"key": "C"
}
],
"editor:set-heading-1": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "1"
}
],
"editor:set-heading-2": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "2"
}
],
"editor:set-heading-3": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "3"
}
],
"editor:set-heading-4": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "4"
}
],
"editor:set-heading-5": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "5"
}
],
"editor:set-heading-6": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "6"
}
],
"app:go-back": [
{
"modifiers": [
"Mod",
"Alt"
],
"key": "ArrowLeft"
},
{
"modifiers": [
"Alt",
"Shift"
],
"key": "ArrowLeft"
}
],
"app:go-forward": [
{
"modifiers": [
"Mod",
"Alt"
],
"key": "ArrowRight"
},
{
"modifiers": [
"Alt",
"Shift"
],
"key": "ArrowRight"
}
],
"editor:context-menu": [
{
"modifiers": [
"Alt"
],
"key": "Enter"
}
],
"editor:follow-link": [
{
"modifiers": [
"Alt",
"Shift"
],
"key": "Enter"
}
],
"editor:cycle-list-checklist": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "B"
}
]
}

168
.obsidian/workspace.json vendored Normal file
View File

@@ -0,0 +1,168 @@
{
"main": {
"id": "1bfbade88fcfec55",
"type": "split",
"children": [
{
"id": "7facf39ed5015822",
"type": "tabs",
"children": [
{
"id": "c408eff3df45f809",
"type": "leaf",
"state": {
"type": "empty",
"state": {},
"icon": "lucide-file",
"title": "New tab"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "be7671ba7092d719",
"type": "split",
"children": [
{
"id": "d35c2a0a402ecd5d",
"type": "tabs",
"children": [
{
"id": "4f11d12d34891ea7",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "Files"
}
},
{
"id": "eaaec87adeac134e",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Search"
}
},
{
"id": "15a237b845d3e20b",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Bookmarks"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "50eec151107f9ed7",
"type": "split",
"children": [
{
"id": "5b09bb3294264e7f",
"type": "tabs",
"children": [
{
"id": "b2fe533e95f324e8",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Backlinks"
}
},
{
"id": "985abb1f14124830",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Outgoing links"
}
},
{
"id": "6a75acc48fb81d4f",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "55ebdc78da69d9f1",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "Outline"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:Open quick switcher": false,
"graph:Open graph view": false,
"canvas:Create new canvas": false,
"daily-notes:Open today's daily note": false,
"templates:Insert template": false,
"command-palette:Open command palette": false
}
},
"active": "c408eff3df45f809",
"lastOpenFiles": [
"README.md"
]
}

168
README.md
View File

@@ -1,30 +1,168 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Next.js & Prisma Postgres Auth Starter
## Getting Started
This repository provides a boilerplate to quickly set up a Next.js demo application with authentication using [NextAuth.js v4](https://next-auth.js.org/), [Prisma Postgres](https://www.prisma.io/postgres) and [Prisma ORM](https://www.prisma.io/orm), and deploy it to Vercel. It includes an easy setup process and example routes that demonstrate basic CRUD operations against the database.
First, run the development server:
## Features
- Next.js 15 app with App Router, Server Actions & API Routes
- Data modeling, database migrations, seeding & querying
- Log in and sign up authentication flows
- CRUD operations to create, view and delete blog posts
- Pagination, filtering & relations queries
## Getting started
### 1. Install dependencies
After cloning the repo and navigating into it, install dependencies:
```
npm install
```
### 1. Create a Prisma Postgres instance
Create a Prisma Postgres instance by running the following command:
```
npx prisma init --db
```
This command is interactive and will prompt you to:
1. Log in to the [Prisma Console](https://console.prisma.io)
1. Select a **region** for your Prisma Postgres instance
1. Give a **name** to your Prisma project
Once the command has terminated, copy the **Database URL** from the terminal output. You'll need it in the next step when you configure your `.env` file.
<!-- Create a Prisma Postgres database instance using [Prisma Data Platform](https://console.prisma.io):
1. Navigate to [Prisma Data Platform](https://console.prisma.io).
2. Click **New project** to create a new project.
3. Enter a name for your project in the **Name** field.
4. Inside the **Prisma Postgres** section, click **Get started**.
5. Choose a region close to your location from the **Region** dropdown.
6. Click **Create project** to set up your database. This redirects you to the database setup page.
7. In the **Set up database access** section, copy the `DATABASE_URL`. You will use this in the next steps. -->
### 2. Set up your `.env` file
You now need to configure your database connection via an environment variable.
First, create an `.env` file:
```bash
touch .env
```
Then update the `.env` file by replacing the existing `DATABASE_URL` value with the one you previously copied. It will look similar to this:
```bash
DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=PRISMA_POSTGRES_API_KEY"
```
To ensure your authentication works properly, you'll also need to set [env vars for NextAuth.js](https://next-auth.js.org/configuration/options):
```bash
AUTH_SECRET="RANDOM_32_CHARACTER_STRING"
```
You can generate a random 32 character string for the `AUTH_SECRET` secret with this command:
```
npx auth secret
```
In the end, your entire `.env` file should look similar to this (but using _your own values_ for the env vars):
```bash
DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5IjoiMWEzMjBiYTEtYjg2Yy00ZTA5LThmZTktZDBhODA3YjQwZjBkIiwidGVuYW50X2lkIjoiY2RhYmM3ZTU1NzdmMmIxMmM0ZTI1Y2IwNWJhZmZhZmU4NjAxNzkxZThlMzhlYjI1NDgwNmIzZjI5NmU1NTkzNiIsImludGVybmFsX3NlY3JldCI6ImI3YmQzMjFhLTY2ODQtNGRiMC05ZWRiLWIyMGE2ZTQ0ZDMwMSJ9.JgKXQBatjjh7GIG3_fRHDnia6bDv8BdwvaX5F-XdBfw"
AUTH_SECRET="gTwLSXFeNWFRpUTmxlRniOfegXYw445pd0k6JqXd7Ag="
```
### 3. Migrate the database
Run the following commands to set up your database and Prisma schema:
```bash
npx prisma migrate dev --name init
```
<!--
<details>
<summary>Expand for <code>yarn</code>, <code>pnpm</code> or <code>bun</code></summary>
```bash
# Using yarn
yarn prisma migrate dev --name init
# Using pnpm
pnpm prisma migrate dev --name init
# Using bun
bun prisma migrate dev --name init
```
</details> -->
### 4. Seed the database
Add initial data to your database:
```bash
npx prisma db seed
```
<details>
<summary>Expand for <code>yarn</code>, <code>pnpm</code> or <code>bun</code></summary>
```bash
# Using yarn
yarn prisma db seed
# Using pnpm
pnpm prisma db seed
# Using bun
bun prisma db seed
```
</details>
### 5. Run the app
Start the development server:
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
<details>
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
<summary>Expand for <code>yarn</code>, <code>pnpm</code> or <code>bun</code></summary>
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
```bash
# Using yarn
yarn dev
## Learn More
# Using pnpm
pnpm run dev
To learn more about Next.js, take a look at the following resources:
# Using bun
bun run dev
```
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
</details>
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
Once the server is running, visit `http://localhost:3000` to start using the app.
## Deploy on Vercel
## Next steps
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
- [Prisma ORM documentation](https://www.prisma.io/docs/orm)
- [Prisma Client API reference](https://www.prisma.io/docs/orm/prisma-client)
- [Join our Discord community](https://discord.com/invite/prisma)
- [Follow us on Twitter](https://twitter.com/prisma)

52
app/Header.tsx Normal file
View File

@@ -0,0 +1,52 @@
"use client";
import Link from "next/link";
import { useSession, signOut } from "next-auth/react";
export default function Header() {
const { data: session } = useSession();
return (
<header className="w-full bg-white shadow-md py-4 px-8">
<nav className="flex justify-between items-center">
<Link href="/" className="text-xl font-bold text-gray-800 hover:text-blue-600 transition-colors">
BayesBond
</Link>
<div className="flex items-center space-x-4">
<Link
href="/posts"
className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition"
>
Posts
</Link>
{session ? (
<>
<Link
href="/posts/new"
className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition"
>
New Post
</Link>
<div className="flex items-center space-x-4">
<div className="text-sm text-gray-500">
{session.user?.name && <div>{session.user.name}</div>}
<div>{session.user?.email}</div>
</div>
<button
onClick={() => signOut()}
className="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition"
>
Sign Out
</button>
</div>
</>
) : (
<Link href="/login" className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition">
Sign In
</Link>
)}
</div>
</nav>
</header>
);
}

View File

@@ -0,0 +1,19 @@
import NextAuth from "next-auth";
// import { authOptions } from "@/app/api/auth/[...nextauth]/auth";
import { authOptions } from "@/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
declare module "next-auth" {
interface Session {
user: { id: string; name: string; email: string };
// user: { id: string };
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
}
}

22
app/api/posts/route.ts Normal file
View File

@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const postsPerPage = 5;
const offset = (page - 1) * postsPerPage;
// Fetch paginated posts
const posts = await prisma.post.findMany({
skip: offset,
take: postsPerPage,
orderBy: { createdAt: "desc" },
include: { author: { select: { name: true } } },
});
const totalPosts = await prisma.post.count();
const totalPages = Math.ceil(totalPosts / postsPerPage);
return NextResponse.json({ posts, totalPages });
}

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

21
app/globals.css Normal file
View File

@@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

28
app/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
// app/layout.tsx
import "./globals.css";
import Header from "./Header";
import Providers from "./providers";
export const metadata = {
title: "BayesBond",
description: "A blog app using Next.js and Prisma",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">{children}</main>
</div>
</Providers>
</body>
</html>
);
}

92
app/login/page.tsx Normal file
View File

@@ -0,0 +1,92 @@
"use client";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function LoginPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
try {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const response = await signIn("credentials", {
...Object.fromEntries(formData),
redirect: false,
});
if (response?.error) {
setError("Invalid credentials");
return;
}
router.push("/");
router.refresh();
} catch {
setError("An error occurred during login");
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
{error && (
<div className="text-red-500 text-sm text-center">{error}</div>
)}
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Sign in
</button>
</div>
</form>
<div className="text-center">
<Link href="/register" className="text-blue-600 hover:underline">
No account? Register.
</Link>
</div>
</div>
</div>
);
}

57
app/page.tsx Normal file
View File

@@ -0,0 +1,57 @@
export const dynamic = "force-dynamic"; // This disables SSG and ISR
import prisma from "@/lib/prisma";
import Link from "next/link";
import { redirect } from "next/navigation";
import { checkPostTableExists } from "@/lib/db-utils";
export default async function Home() {
// Check if the post table exists
const tableExists = await checkPostTableExists();
// If the post table doesn't exist, redirect to setup page
if (!tableExists) {
redirect("/setup");
}
const posts = await prisma.post.findMany({
orderBy: {
createdAt: "desc",
},
take: 6,
include: {
author: {
select: {
name: true,
},
},
},
});
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-24 px-8">
<h1 className="text-5xl font-extrabold mb-12 text-[#333333]">Recent Posts</h1>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 w-full max-w-6xl mb-8">
{posts.map((post) => (
<Link key={post.id} href={`/posts/${post.id}`} className="group">
<div className="border rounded-lg shadow-md bg-white p-6 hover:shadow-lg transition-shadow duration-300">
<h2 className="text-2xl font-semibold text-gray-900 group-hover:underline mb-2">{post.title}</h2>
<p className="text-sm text-gray-500">by {post.author ? post.author.name : "Anonymous"}</p>
<p className="text-xs text-gray-400 mb-4">
{new Date(post.createdAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
<div className="relative">
<p className="text-gray-700 leading-relaxed line-clamp-2">{post.content || "No content available."}</p>
<div className="absolute bottom-0 left-0 w-full h-12 bg-gradient-to-t from-gray-50 to-transparent" />
</div>
</div>
</Link>
))}
</div>
</div>
);
}

68
app/posts/[id]/page.tsx Normal file
View File

@@ -0,0 +1,68 @@
export const dynamic = "force-dynamic"; // This disables SSG and ISR
import prisma from "@/lib/prisma";
import { notFound, redirect } from "next/navigation";
export default async function Post({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const postId = parseInt(id);
const post = await prisma.post.findUnique({
where: { id: postId },
include: {
author: true,
},
});
if (!post) {
notFound();
}
// Server action to delete the post
async function deletePost() {
"use server";
await prisma.post.delete({
where: {
id: postId,
},
});
redirect("/posts");
}
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-8">
<article className="max-w-3xl w-full bg-white shadow-lg rounded-lg p-8">
{/* Post Title */}
<h1 className="text-5xl font-extrabold text-gray-900 mb-4">
{post.title}
</h1>
{/* Author Information */}
<p className="text-lg text-gray-600 mb-4">
by <span className="font-medium text-gray-800">{post.author?.name || "Anonymous"}</span>
</p>
{/* Content Section */}
<div className="text-lg text-gray-800 leading-relaxed space-y-6 border-t pt-6">
{post.content ? (
<p>{post.content}</p>
) : (
<p className="italic text-gray-500">No content available for this post.</p>
)}
</div>
</article>
{/* Delete Button */}
<form action={deletePost} className="mt-6">
<button
type="submit"
className="px-6 py-3 bg-red-500 text-white font-semibold rounded-lg hover:bg-red-600 transition-colors"
>
Delete Post
</button>
</form>
</div>
);
}

23
app/posts/new/actions.ts Normal file
View File

@@ -0,0 +1,23 @@
"use server";
import prisma from "@/lib/prisma";
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/auth";
export async function createPost(formData: FormData) {
const session = await getServerSession(authOptions);
if (!session?.user) {
throw new Error("You must be logged in to create a post");
}
await prisma.post.create({
data: {
title: formData.get("title") as string,
content: formData.get("content") as string,
authorId: session.user.id,
},
});
redirect("/posts");
}

43
app/posts/new/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
"use client";
import Form from "next/form";
import { createPost } from "./actions";
export default function NewPost() {
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form action={createPost} className="space-y-6">
<div>
<label htmlFor="title" className="flex text-lg font-medium mb-2 items-center">
Title
<span className="ml-2 px-2 py-1 text-xs font-semibold text-white bg-gray-500 rounded-lg">
Required
</span>
</label>
<input
type="text"
id="title"
name="title"
required
placeholder="Enter your post title ..."
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg font-medium mb-2">Content</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here ..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button type="submit" className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600">
Create Post
</button>
</Form>
</div>
);
}

114
app/posts/page.tsx Normal file
View File

@@ -0,0 +1,114 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect, useState, Suspense } from "react";
import Link from "next/link";
interface Post {
id: number;
title: string;
content?: string;
createdAt: string;
author?: {
name: string;
};
}
// Disable static generation
export const dynamic = "force-dynamic";
function PostsList() {
const searchParams = useSearchParams();
const page = parseInt(searchParams.get("page") || "1");
const [posts, setPosts] = useState<Post[]>([]);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
setIsLoading(true);
try {
const res = await fetch(`/api/posts?page=${page}`);
if (!res.ok) {
throw new Error("Failed to fetch posts");
}
const data = await res.json();
setPosts(data.posts);
setTotalPages(data.totalPages);
} catch (error) {
console.error("Error fetching posts:", error);
} finally {
setIsLoading(false);
}
}
fetchPosts();
}, [page]);
return (
<>
{isLoading ? (
<div className="flex items-center justify-center space-x-2 min-h-[200px]">
<div className="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
<p className="text-gray-600">Loading...</p>
</div>
) : (
<>
{posts.length === 0 ? (
<p className="text-gray-600">No posts available.</p>
) : (
<ul className="space-y-6 w-full max-w-4xl mx-auto">
{posts.map((post) => (
<li key={post.id} className="border p-6 rounded-lg shadow-md bg-white">
<Link href={`/posts/${post.id}`} className="text-2xl font-semibold text-gray-900 hover:underline">
{post.title}
</Link>
<p className="text-sm text-gray-500">by {post.author?.name || "Anonymous"}</p>
<p className="text-xs text-gray-400">
{new Date(post.createdAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
</li>
))}
</ul>
)}
{/* Pagination Controls */}
<div className="flex justify-center space-x-4 mt-8">
{page > 1 && (
<Link href={`/posts?page=${page - 1}`}>
<button className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">Previous</button>
</Link>
)}
{page < totalPages && (
<Link href={`/posts?page=${page + 1}`}>
<button className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">Next</button>
</Link>
)}
</div>
</>
)}
</>
);
}
export default function PostsPage() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-start p-8">
<Suspense
fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
<p className="ml-3 text-gray-600">Loading page...</p>
</div>
}
>
<PostsList />
</Suspense>
</div>
);
}

7
app/providers.tsx Normal file
View File

@@ -0,0 +1,7 @@
"use client";
import { SessionProvider } from "next-auth/react";
export default function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

105
app/register/page.tsx Normal file
View File

@@ -0,0 +1,105 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { signIn } from "next-auth/react";
export default function RegisterPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
try {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const signInResult = await signIn("credentials", {
...Object.fromEntries(formData),
redirect: false,
});
if (signInResult?.error) {
setError("Failed to sign in after registration");
return;
}
router.push("/");
router.refresh();
} catch (error) {
setError(error instanceof Error ? error.message : "Registration failed");
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="name" className="sr-only">
Name
</label>
<input
id="name"
name="name"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Full name"
/>
</div>
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
{error && (
<div className="text-red-500 text-sm text-center">{error}</div>
)}
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Register
</button>
</div>
</form>
<div className="text-center">
<Link href="/login" className="text-blue-600 hover:underline">
Already have an account? Sign in
</Link>
</div>
</div>
</div>
);
}

44
app/setup/code-block.tsx Normal file
View File

@@ -0,0 +1,44 @@
"use client";
import { useState } from "react";
import { ClipboardCopy } from "lucide-react";
interface CodeBlockProps {
code: string;
}
export function CodeBlock({ code }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
return (
<div className="relative bg-gray-900 rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 bg-gray-800">
<span className="text-xs text-gray-400">Terminal</span>
<button
onClick={copyToClipboard}
className="text-gray-400 hover:text-white transition-colors"
aria-label="Copy to clipboard"
>
{copied ? (
<span className="text-green-400 text-xs">Copied!</span>
) : (
<ClipboardCopy size={16} />
)}
</button>
</div>
<pre className="p-4 overflow-x-auto text-gray-300 text-sm">
<code>{code}</code>
</pre>
</div>
);
}

19
app/setup/page.tsx Normal file
View File

@@ -0,0 +1,19 @@
import SetupInstructions from "./setup-instructions";
export default function SetupPage() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-8">
<div className="max-w-3xl w-full bg-white rounded-lg shadow-lg p-8">
<h1 className="text-3xl font-bold text-center mb-6 text-gray-900">
Welcome to BayesBond
</h1>
<p className="text-gray-600 mb-8 text-center">
It looks like your database isn&apos;t set up yet. Follow the
instructions below to get started.
</p>
<SetupInstructions />
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
"use client";
import SetupSteps from "./setup-steps";
export default function SetupInstructions() {
return (
<div className="space-y-8">
<SetupSteps />
</div>
);
}

115
app/setup/setup-steps.tsx Normal file
View File

@@ -0,0 +1,115 @@
import { CodeBlock } from "./code-block";
export default function SetupSteps() {
return (
<div className="space-y-8">
<section>
<h2 className="text-2xl font-semibold mb-4 text-gray-800">
Getting Started
</h2>
<p className="text-gray-600 mb-4">
Follow these steps to set up your Next.js & Prisma Postgres Auth
Starter:
</p>
</section>
<section>
<h3 className="text-xl font-semibold mb-3 text-gray-800">
1. Install Dependencies
</h3>
<p className="text-gray-600 mb-3">
After cloning the repo and navigating into it, install dependencies:
</p>
<CodeBlock code="npm install" />
</section>
<section>
<h3 className="text-xl font-semibold mb-3 text-gray-800">
2. Create a Prisma Postgres Instance
</h3>
<p className="text-gray-600 mb-3">
Create a Prisma Postgres instance by running the following command:
</p>
<CodeBlock code="npx prisma init --db" />
<p className="text-gray-600 mt-3">
This command is interactive and will prompt you to:
</p>
<ol className="list-decimal list-inside mt-2 space-y-1 text-gray-600">
<li>Log in to the Prisma Console</li>
<li>
Select a <strong>region</strong> for your Prisma Postgres instance
</li>
<li>
Give a <strong>name</strong> to your Prisma project
</li>
</ol>
<p className="text-gray-600 mt-3">
Once the command has terminated, copy the{" "}
<strong>Database URL</strong> from the terminal output. You&apos;ll
need it in the next step.
</p>
</section>
<section>
<h3 className="text-xl font-semibold mb-3 text-gray-800">
3. Set Up Your .env File
</h3>
<p className="text-gray-600 mb-3">
You need to configure your database connection via an environment
variable.
</p>
<p className="text-gray-600 mb-3">
First, create an <code>.env</code> file:
</p>
<CodeBlock code="touch .env" />
<p className="text-gray-600 mb-3 mt-3">
Then update the <code>.env</code> file by replacing the existing{" "}
<code>DATABASE_URL</code> value with the one you previously copied:
</p>
<CodeBlock
code={`DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=PRISMA_POSTGRES_API_KEY"`}
/>
<p className="text-gray-600 mb-3 mt-3">
To ensure your authentication works properly, you&apos;ll also need to
set env vars for NextAuth.js:
</p>
<CodeBlock code={`AUTH_SECRET="RANDOM_32_CHARACTER_STRING"`} />
<p className="text-gray-600 mb-3 mt-3">
You can generate a random 32 character string for the{" "}
<code>AUTH_SECRET</code> with this command:
</p>
<CodeBlock code="npx auth secret" />
</section>
<section>
<h3 className="text-xl font-semibold mb-3 text-gray-800">
4. Migrate the Database
</h3>
<p className="text-gray-600 mb-3">
Run the following command to set up your database and Prisma schema:
</p>
<CodeBlock code="npx prisma migrate dev --name init" />
</section>
<section>
<h3 className="text-xl font-semibold mb-3 text-gray-800">
5. Seed the Database
</h3>
<p className="text-gray-600 mb-3">Add initial data to your database:</p>
<CodeBlock code="npx prisma db seed" />
</section>
<section>
<h3 className="text-xl font-semibold mb-3 text-gray-800">
6. Run the App
</h3>
<p className="text-gray-600 mb-3">Start the development server:</p>
<CodeBlock code="npm run dev" />
<p className="text-gray-600 mt-3">
Once the server is running, visit <code>http://localhost:3000</code>{" "}
to start using the app.
</p>
</section>
</div>
);
}

57
app/users/new/page.tsx Normal file
View File

@@ -0,0 +1,57 @@
export const dynamic = "force-dynamic"; // This disables SSG and ISR
import prisma from "@/lib/prisma";
import { redirect } from "next/navigation";
import Form from "next/form";
export default function NewUser() {
async function createUser(formData: FormData) {
"use server";
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await prisma.user.create({
data: { name, email, password: "" }, // password will be added by NextAuth
});
redirect("/");
}
return (
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md mt-12">
<h1 className="text-3xl font-bold mb-6">Create New User</h1>
<Form action={createUser} className="space-y-6">
<div>
<label htmlFor="name" className="block text-lg font-medium mb-2">Name</label>
<input
type="text"
id="name"
name="name"
placeholder="Enter user name ..."
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="email" className="flex text-lg font-medium mb-2 items-center">
Email
<span className="ml-2 px-2 py-1 text-xs font-semibold text-white bg-gray-500 rounded-lg">
Required
</span>
</label>
<input
type="email"
id="email"
name="email"
required
placeholder="Enter user email ..."
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button type="submit" className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600">
Create User
</button>
</Form>
</div>
);
}

58
auth.ts Normal file
View File

@@ -0,0 +1,58 @@
import CredentialsProvider from "next-auth/providers/credentials";
import { type NextAuthOptions } from "next-auth";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
export const authOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
name: { label: "Name", type: "name" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Invalid credentials");
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user) {
return await prisma.user.create({
data: {
name: credentials.name ?? credentials.email,
email: credentials.email,
password: await bcrypt.hash(credentials.password, 10),
},
});
}
const isCorrectPassword = await bcrypt.compare(
credentials.password,
user.password
);
if (!isCorrectPassword) {
throw new Error("Invalid credentials");
}
return user;
},
}),
],
pages: {
signIn: "/login",
},
callbacks: {
async jwt({ token, user }) {
return { ...token, id: token.id ?? user?.id };
},
async session({ session, token }) {
return { ...session, user: { ...session.user, id: token.id } };
},
},
} satisfies NextAuthOptions;

18
lib/db-utils.ts Normal file
View File

@@ -0,0 +1,18 @@
"use server";
import prisma from "@/lib/prisma";
/**
* Checks if the Post table exists in the database
* @returns Promise<boolean> - true if the table exists, false otherwise
*/
export async function checkPostTableExists(): Promise<boolean> {
try {
// Try to query the post table
await prisma.post.findFirst();
return true;
} catch {
// If there's an error, the table likely doesn't exist
return false;
}
}

10
lib/prisma.ts Normal file
View File

@@ -0,0 +1,10 @@
import { PrismaClient } from '@prisma/client'
import { withAccelerate } from '@prisma/extension-accelerate'
const prisma = new PrismaClient().$extends(withAccelerate())
const globalForPrisma = global as unknown as { prisma: typeof prisma }
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
export default prisma

View File

File diff suppressed because one or more lines are too long

BIN
nextjs-auth-starter.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

50
old/.gitignore vendored Normal file
View File

@@ -0,0 +1,50 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# others
notebooks
.idea
.obsidian
martin
/src/generated/prisma
*.db

30
old/README.md Normal file
View File

@@ -0,0 +1,30 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

16
old/eslint.config.mjs Normal file
View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

45
old/next.config.js Normal file
View File

@@ -0,0 +1,45 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
async headers() {
return [
{
// Apply these headers to all routes in your application
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
async rewrites() {
return [
{
source: '/api/auth/:path*',
destination: '/api/auth/route',
},
];
},
};
module.exports = nextConfig;

7
old/next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

5887
old/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

31
old/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "bayesbond",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"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"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.4.4",
"tailwindcss": "^4",
"typescript": "^5"
}
}

5
old/postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

61
old/prisma/schema.prisma Normal file
View File

@@ -0,0 +1,61 @@
// 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 = "sqlite"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
displayName String
avatarUrl String?
bio String?
@@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])
}

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 128 B

View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,23 @@
import NextAuth from 'next-auth';
import { authConfig } from '@/auth.config';
// Merge the auth config with additional options
const handler = NextAuth({
...authConfig,
// Debug mode in development
debug: process.env.NODE_ENV === 'development',
// Trust the host header (only in development)
trustHost: true,
// Cookie settings (already defined in authConfig, but can be overridden here if needed)
});
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = handler;
export { GET, POST, auth, signIn, signOut };
export default handler;

View File

@@ -1,34 +1,83 @@
'use client';
import {signOut, useSession} from "next-auth/react";
import {useRouter} from 'next/navigation';
import {useEffect} from 'react';
import { signOut, useSession, getSession } from "next-auth/react";
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import Link from 'next/link';
export default function DashboardPage() {
console.log('DashboardPage');
const {data: session, status} = useSession();
console.log('DashboardPage rendering');
const { data: session, status, update } = useSession();
const router = useRouter();
const searchParams = useSearchParams();
const [isClient, setIsClient] = useState(false);
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login');
}
}, [status, router]);
console.log('=== DASHBOARD SESSION DEBUG ===');
console.log('Session status:', status);
console.log('Session data:', session);
console.log('Is client:', isClient);
console.log('Callback URL:', callbackUrl);
const checkSession = async () => {
try {
const serverSession = await getSession();
console.log('Server session:', serverSession);
if (status === 'unauthenticated' || !session) {
console.log('No active session, redirecting to login');
router.push(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`);
return;
}
if (status === 'loading') {
console.log("LOADING");
if (status === 'authenticated' && session) {
console.log('User authenticated:', session.user);
// Force update the session to ensure it's fresh
await update();
}
} catch (error) {
console.error('Error checking session:', error);
router.push(`/login?error=SessionError&callbackUrl=${encodeURIComponent(callbackUrl)}`);
} finally {
setIsClient(true);
}
};
checkSession();
}, [status, session, router, update, callbackUrl]);
// Show loading state while checking authentication
if (status === 'loading' || !isClient) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 p-4">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-indigo-500 mx-auto mb-4"></div>
<h2 className="text-xl font-semibold text-gray-800 mb-2">Loading Dashboard</h2>
<p className="text-gray-600">Please wait while we verify your session...</p>
</div>
</div>
);
}
console.log(status, session);
if (status === 'unauthenticated' || !session) {
// router.push('/login');
return null;
// If unauthenticated but still showing the page (should be caught by useEffect)
if (status === 'unauthenticated' || !session) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 p-4">
<div className="text-center max-w-md">
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6">
<p className="font-bold">Session Expired</p>
<p>You need to be signed in to view this page.</p>
</div>
<Link
href={`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Go to Login
</Link>
</div>
</div>
);
}
return (

BIN
old/src/app/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

19
old/src/app/header.tsx Normal file
View File

@@ -0,0 +1,19 @@
import Link from "next/link";
import Image from "next/image";
export default function Header() {
return (
<header className="flex items-center p-4 shadow-md bg-white">
<Link href="/" className="flex items-center">
<Image
src="/globe.svg"
alt="Home"
width={40}
height={40}
className="cursor-pointer hover:opacity-80 transition"
/>
</Link>
<h1 className="ml-3 text-xl font-bold">BayesBond</h1>
</header>
);
}

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Providers } from "@/providers";
import Header from "@/app/header";
import "./globals.css";
const geistSans = Geist({
@@ -26,6 +27,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Header />
<Providers>{children}</Providers>
</body>
</html>

View File

@@ -70,12 +70,17 @@ export default function LoginPage() {
throw new Error('Please enter a valid email address');
}
// Attempt to sign in
// First, get the CSRF token
const csrfResponse = await fetch('/api/auth/csrf');
const { csrfToken } = await csrfResponse.json();
// Then attempt to sign in with the CSRF token
const result = await signIn('credentials', {
redirect: false,
email: email.trim(),
password,
callbackUrl,
csrfToken,
});
// Handle the result

View File

126
old/src/auth.config.ts Normal file
View File

@@ -0,0 +1,126 @@
import type { NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { prisma } from "./lib/prisma";
import { compare } from "bcryptjs";
export const authConfig = {
// Use JWT strategy for session management
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
// Configure pages
pages: {
signIn: "/login",
error: "/login",
},
// Configure providers
providers: [
Credentials({
id: "credentials",
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
console.log("Missing credentials");
throw new Error("Email and password are required");
}
try {
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.password) {
console.log("User not found or no password set");
throw new Error("Invalid email or password");
}
const isValid = await compare(
credentials.password as string,
user.password
);
if (!isValid) {
console.log("Invalid password");
throw new Error("Invalid email or password");
}
console.log("User authenticated successfully:", { id: user.id, email: user.email });
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
} catch (error) {
console.error("Authentication error:", error);
throw error;
}
},
}),
],
callbacks: {
async session({ session, token }) {
console.log("Session callback - token:", token);
if (token && session.user) {
session.user.id = token.sub!;
session.user.name = token.name;
session.user.email = token.email;
}
return session;
},
async jwt({ token, user, trigger, session }) {
console.log("JWT callback - token:", token, "user:", user);
if (user) {
token.sub = user.id;
token.name = user.name;
token.email = user.email;
}
return token;
},
async redirect({ url, baseUrl }) {
console.log("Redirect callback - url:", url, "baseUrl:", baseUrl);
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same origin
else if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
},
// CSRF protection
cookies: {
sessionToken: {
name: `__Secure-next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
},
},
csrfToken: {
name: `__Host-next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
},
},
},
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
pages: {
signIn: "/login",
error: "/login",
},
secret: process.env.NEXTAUTH_SECRET,
debug: process.env.NODE_ENV === 'development',
useSecureCookies: process.env.NODE_ENV === 'production',
} satisfies NextAuthConfig;

View File

View File

35
old/src/middleware.ts Normal file
View File

@@ -0,0 +1,35 @@
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';
export default withAuth(
function middleware(req) {
// Add security headers
const requestHeaders = new Headers(req.headers);
requestHeaders.set('x-middleware-cache', 'no-cache');
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
},
{
callbacks: {
authorized: ({ token }) => {
// If there's a token, the user is authenticated
return !!token;
},
},
pages: {
signIn: '/login',
},
}
);
// Configure which paths should be protected
export const config = {
matcher: [
'/dashboard/:path*',
'/api/auth/session',
],
};

View File

28
old/tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"],
"sourceMap": true
}

2193
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,39 @@
{
"name": "bayesbond",
"name": "my-nextjs-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"build": "npx prisma migrate deploy || [ \"$DATABASE_URL\" = \"prisma+postgres://accelerate.prisma-data.net/?api_key=API_KEY\" ] && next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"postinstall": "npx prisma generate --no-engine"
},
"dependencies": {
"@auth/prisma-adapter": "^2.10.0",
"@prisma/client": "^6.12.0",
"@prisma/extension-accelerate": "^2.0.2",
"bcryptjs": "^3.0.2",
"next": "15.4.4",
"next-auth": "^5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0"
"lucide-react": "^0.503.0",
"next": "^15.4.4",
"next-auth": "^4.24.11",
"react": "^19.1.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^3.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.4.4",
"tailwindcss": "^4",
"eslint-config-next": "15.1.7",
"postcss": "^8",
"prisma": "^6.12.0",
"tailwindcss": "^3.4.1",
"tsx": "^4.19.2",
"typescript": "^5"
},
"prisma": {
"seed": "npx tsx prisma/seed.ts"
}
}

View File

@@ -1,5 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" TEXT,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
provider = "postgresql"

View File

@@ -1,61 +1,33 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
displayName String
avatarUrl String?
bio String?
@@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[]
id String @id @default(cuid())
name String?
email String @unique
password String
posts Post[]
}
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])
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String?
published Boolean @default(false)
authorId String?
author User? @relation(fields: [authorId], references: [id])
}

164
prisma/seed.ts Normal file
View File

@@ -0,0 +1,164 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
// Create 5 users with hashed passwords
const users = await Promise.all([
prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
password: await bcrypt.hash('password123', 10),
},
}),
prisma.user.create({
data: {
email: 'bob@example.com',
name: 'Bob',
password: await bcrypt.hash('password123', 10),
},
}),
prisma.user.create({
data: {
email: 'charlie@example.com',
name: 'Charlie',
password: await bcrypt.hash('password123', 10),
},
}),
prisma.user.create({
data: {
email: 'diana@example.com',
name: 'Diana',
password: await bcrypt.hash('password123', 10),
},
}),
prisma.user.create({
data: {
email: 'edward@example.com',
name: 'Edward',
password: await bcrypt.hash('password123', 10),
},
}),
]);
const userIdMapping = {
alice: users[0].id,
bob: users[1].id,
charlie: users[2].id,
diana: users[3].id,
edward: users[4].id,
};
// Create 15 posts distributed among users
await prisma.post.createMany({
data: [
// Alice's posts
{
title: 'Getting Started with TypeScript and Prisma',
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce id erat a lorem tincidunt ultricies. Vivamus porta bibendum nulla vel accumsan.',
published: true,
authorId: userIdMapping.alice
},
{
title: 'How ORMs Simplify Complex Queries',
content: 'Duis sagittis urna ut sapien tristique convallis. Aenean vel ligula felis. Phasellus bibendum sem at elit dictum volutpat.',
published: false,
authorId: userIdMapping.alice
},
// Bob's posts
{
title: 'Mastering Prisma: Efficient Database Migrations',
content: 'Ut ullamcorper nec erat id auctor. Nullam nec ligula in ex feugiat tincidunt. Cras accumsan vehicula tortor ut eleifend.',
published: true,
authorId: userIdMapping.bob
},
{
title: 'Best Practices for Type Safety in ORMs',
content: 'Aliquam erat volutpat. Suspendisse potenti. Maecenas fringilla elit vel eros laoreet, et tempor sapien vulputate.',
published: true,
authorId: userIdMapping.bob
},
{
title: 'TypeScript Utility Types for Database Models',
content: 'Donec ac magna facilisis, vestibulum ligula at, elementum nisl. Morbi volutpat eget velit eu egestas.',
published: false,
authorId: userIdMapping.bob
},
// Charlie's posts (no posts for Charlie)
// Diana's posts
{
title: 'Exploring Database Indexes and Their Performance Impact',
content: 'Vivamus ac velit tincidunt, sollicitudin erat quis, fringilla enim. Aenean posuere est a risus placerat suscipit.',
published: true,
authorId: userIdMapping.diana
},
{
title: 'Choosing the Right Database for Your TypeScript Project',
content: 'Sed vel suscipit lorem. Duis et arcu consequat, sagittis justo quis, pellentesque risus. Curabitur sed consequat est.',
published: false,
authorId: userIdMapping.diana
},
{
title: 'Designing Scalable Schemas with Prisma',
content: 'Phasellus ut erat nec elit ultricies egestas. Vestibulum rhoncus urna eget magna varius pharetra.',
published: true,
authorId: userIdMapping.diana
},
{
title: 'Handling Relations Between Models in ORMs',
content: 'Integer luctus ac augue at tristique. Curabitur varius nisl vitae mi fringilla, vel tincidunt nunc dictum.',
published: false,
authorId: userIdMapping.diana
},
// Edward's posts
{
title: 'Why TypeORM Still Has Its Place in 2025',
content: 'Morbi non arcu nec velit cursus feugiat sit amet sit amet mi. Etiam porttitor ligula id sem molestie, in tempor arcu bibendum.',
published: true,
authorId: userIdMapping.edward
},
{
title: 'NoSQL vs SQL: The Definitive Guide for Developers',
content: 'Suspendisse a ligula sit amet risus ullamcorper tincidunt. Curabitur tincidunt, sapien id fringilla auctor, risus libero gravida odio, nec volutpat libero orci nec lorem.',
published: true,
authorId: userIdMapping.edward
},
{
title: 'Optimizing Queries with Prisma\'s Select and Include',
content: 'Proin vel diam vel nisi facilisis malesuada. Sed vitae diam nec magna mollis commodo a vitae nunc.',
published: false,
authorId: userIdMapping.edward
},
{
title: 'PostgreSQL Optimizations Every Developer Should Know',
content: 'Nullam mollis quam sit amet lacus interdum, at suscipit libero pellentesque. Suspendisse in mi vitae magna finibus pretium.',
published: true,
authorId: userIdMapping.edward
},
{
title: 'Scaling Applications with Partitioned Tables in PostgreSQL',
content: 'Cras vitae tortor in mauris tristique elementum non id ipsum. Nunc vitae pulvinar purus.',
published: true,
authorId: userIdMapping.edward
},
],
});
console.log('Seeding completed.');
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -1,25 +0,0 @@
import NextAuth from 'next-auth';
import { authConfig } from '@/auth';
// Initialize NextAuth with the configuration
export const {
handlers: { GET, POST },
} = NextAuth({
...authConfig,
// Enable debug logs in development
debug: process.env.NODE_ENV === 'development',
// Ensure cookies are secure in production
cookies: {
sessionToken: {
name: `__Secure-next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax', // CSRF protection
path: '/',
secure: process.env.NODE_ENV === 'production',
},
},
},
});
export { GET, POST};

View File

@@ -1,67 +0,0 @@
import type { NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { prisma } from "./lib/prisma";
import { compare } from "bcryptjs";
export default {
providers: [
Credentials({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Email and password are required");
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.password) {
throw new Error("Invalid email or password");
}
const isValid = await compare(
credentials.password as string,
user.password
);
if (!isValid) {
throw new Error("Invalid email or password");
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
},
}),
],
callbacks: {
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.sub!;
}
return session;
},
async jwt({ token, user }) {
if (user) {
token.sub = user.id;
}
return token;
},
},
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
error: "/login",
},
secret: process.env.NEXTAUTH_SECRET,
} satisfies NextAuthConfig;

18
tailwind.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [ ],
} satisfies Config;

View File

@@ -19,10 +19,9 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"],
"sourceMap": true
"exclude": ["node_modules"]
}