Update
12
.gitignore
vendored
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
31
.obsidian/core-plugins.json
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
19
app/api/auth/[...nextauth]/route.ts
Normal 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
@@ -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 });
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
21
app/globals.css
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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't set up yet. Follow the
|
||||
instructions below to get started.
|
||||
</p>
|
||||
|
||||
<SetupInstructions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
app/setup/setup-instructions.tsx
Normal 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
@@ -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'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'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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
2
martin/Date Me Directory.html
Normal file
BIN
nextjs-auth-starter.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
468
notebooks/meeting_rational.ipynb
Normal file
435
notebooks/meeting_rational_all.ipynb
Normal file
75
notebooks/meeting_rational_qualitative.ipynb
Normal file
BIN
notebooks/rational_qualitative.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
50
old/.gitignore
vendored
Normal 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
@@ -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
@@ -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;
|
||||
45
old/next.config.js
Normal 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
@@ -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
31
old/package.json
Normal 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
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
3
old/prisma/migrations/migration_lock.toml
Normal 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
@@ -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])
|
||||
}
|
||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |
23
old/src/app/api/auth/[...nextauth]/route.ts
Normal 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;
|
||||
@@ -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
|
After Width: | Height: | Size: 25 KiB |
19
old/src/app/header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
126
old/src/auth.config.ts
Normal 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;
|
||||
35
old/src/middleware.ts
Normal 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',
|
||||
],
|
||||
};
|
||||
28
old/tsconfig.json
Normal 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
30
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
28
prisma/migrations/20250727152804_init/migration.sql
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
@@ -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};
|
||||
@@ -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
@@ -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;
|
||||
@@ -19,10 +19,9 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"sourceMap": true
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||