diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..6a0233e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+DATABASE_URL="file:./dev.db"
+SESSION_SECRET="replace-with-a-long-random-secret"
diff --git a/.gitignore b/.gitignore
index f390d12..1df4829 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
+!.env.example
# vercel
.vercel
diff --git a/README.md b/README.md
index e215bc4..ee03569 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +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).
+# NoteAI
-## Getting Started
+基于 Next.js + Prisma + TipTap 的本地知识笔记应用。
-First, run the development server:
+## 环境变量
+
+在项目根目录创建 `.env`:
+
+```env
+DATABASE_URL="file:./dev.db"
+SESSION_SECRET="replace-with-a-long-random-secret"
+```
+
+- `SESSION_SECRET` 必填,用于服务端签名登录会话。
+- 生产环境请使用长度至少 32 的随机字符串。
+
+## 开发
```bash
npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
```
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+默认端口为 `3001`。
-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.
+```bash
+npx tsc --noEmit
+npm run lint
+```
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 05e726d..cd5fa36 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -5,6 +5,13 @@ import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
+ {
+ files: ["src/components/editor/**/*.{ts,tsx}", "src/components/editor.tsx"],
+ rules: {
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ },
+ },
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
diff --git a/prisma/dev.db b/prisma/dev.db
index 40121f1..1f27d18 100644
Binary files a/prisma/dev.db and b/prisma/dev.db differ
diff --git a/scripts/init-settings.js b/scripts/init-settings.js
index 065fe9e..be81391 100644
--- a/scripts/init-settings.js
+++ b/scripts/init-settings.js
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-require-imports */
const { PrismaClient } = require('@prisma/client');
const { scrypt, randomBytes } = require('crypto');
const { promisify } = require('util');
diff --git a/scripts/reset-password.js b/scripts/reset-password.js
index ba8fe71..af6f496 100644
--- a/scripts/reset-password.js
+++ b/scripts/reset-password.js
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-require-imports */
const { PrismaClient } = require('@prisma/client');
const { scrypt, randomBytes } = require('crypto');
const { promisify } = require('util');
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index c5c3bd8..afdd6d9 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -1,31 +1,38 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { verifyPassword } from "@/lib/auth";
+import { createSessionToken, getSessionCookieName, getSessionTtlSeconds } from "@/lib/session";
export async function POST(req: Request) {
try {
- const { password } = await req.json();
-
- // Ensure settings exist, if not, maybe we should init or fail?
- // Ideally init should happen on app startup or manual trigger, but for simplicity:
- // If no settings exist, check against "admin" (fallback) but DO NOT create DB entry implicitely here for security,
- // unless we strictly define that "admin" is the default.
- // Let's assume DB must be populated.
+ const { password, rememberMe, durationDays } = await req.json();
const settings = await prisma.globalSettings.findUnique({
where: { id: "default" },
});
- const isValid = settings
- ? await verifyPassword(password, settings.password)
- : password === "admin"; // Fallback only if DB empty
+ if (!settings) {
+ return NextResponse.json({ error: "Settings not initialized" }, { status: 503 });
+ }
+
+ const isValid = await verifyPassword(password, settings.password);
if (isValid) {
- return NextResponse.json({ success: true });
+ const days = rememberMe ? Number(durationDays) || 1 : 1;
+ const token = await createSessionToken(days);
+ const response = NextResponse.json({ success: true });
+ response.cookies.set(getSessionCookieName(), token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ maxAge: getSessionTtlSeconds(days),
+ });
+ return response;
} else {
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
}
- } catch (error) {
+ } catch {
return NextResponse.json({ error: "Login failed" }, { status: 500 });
}
}
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
index 01177a1..d3a749d 100644
--- a/src/app/api/auth/logout/route.ts
+++ b/src/app/api/auth/logout/route.ts
@@ -1,9 +1,10 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
+import { getSessionCookieName } from "@/lib/session";
export async function POST() {
const cookieStore = await cookies();
- cookieStore.delete("auth");
+ cookieStore.delete(getSessionCookieName());
return NextResponse.json({ success: true });
}
diff --git a/src/app/api/pages/[id]/route.ts b/src/app/api/pages/[id]/route.ts
index c810f2c..846f990 100644
--- a/src/app/api/pages/[id]/route.ts
+++ b/src/app/api/pages/[id]/route.ts
@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
+import type { Prisma } from '@prisma/client';
export async function GET(
request: Request,
@@ -12,7 +13,7 @@ export async function GET(
});
if (!page) return NextResponse.json({ error: 'Page not found' }, { status: 404 });
return NextResponse.json({ ...page, tags: JSON.parse(page.tags || "[]") });
- } catch (error) {
+ } catch {
return NextResponse.json({ error: 'Error fetching page' }, { status: 500 });
}
}
@@ -25,7 +26,7 @@ export async function PUT(
try {
const body = await request.json();
// Separate update logic for flexibility (e.g. only updating title)
- const updateData: any = {};
+ const updateData: Prisma.PageUncheckedUpdateInput = {};
if (body.title !== undefined) updateData.title = body.title;
if (body.content !== undefined) updateData.content = body.content;
if (body.parentId !== undefined) updateData.parentId = body.parentId;
@@ -38,7 +39,7 @@ export async function PUT(
data: updateData,
});
return NextResponse.json({ ...page, tags: JSON.parse(page.tags || "[]") });
- } catch (error) {
+ } catch {
return NextResponse.json({ error: 'Error updating page' }, { status: 500 });
}
}
@@ -53,7 +54,7 @@ export async function DELETE(
where: { id },
});
return NextResponse.json({ success: true });
- } catch (error) {
+ } catch {
return NextResponse.json({ error: 'Error deleting page' }, { status: 500 });
}
}
diff --git a/src/app/api/pages/reorder/route.ts b/src/app/api/pages/reorder/route.ts
index 9e5afca..f7a59c7 100644
--- a/src/app/api/pages/reorder/route.ts
+++ b/src/app/api/pages/reorder/route.ts
@@ -21,7 +21,7 @@ export async function PUT(request: Request) {
);
return NextResponse.json({ success: true });
- } catch (error) {
+ } catch {
return NextResponse.json({ error: 'Error reordering pages' }, { status: 500 });
}
}
diff --git a/src/app/api/pages/route.ts b/src/app/api/pages/route.ts
index 86fef59..dc09c1d 100644
--- a/src/app/api/pages/route.ts
+++ b/src/app/api/pages/route.ts
@@ -11,7 +11,7 @@ export async function GET() {
tags: JSON.parse(p.tags || "[]")
}));
return NextResponse.json(parsedPages);
- } catch (error) {
+ } catch {
return NextResponse.json({ error: 'Error fetching pages' }, { status: 500 });
}
}
@@ -41,7 +41,7 @@ export async function POST(request: Request) {
},
});
return NextResponse.json({ ...page, tags: JSON.parse(page.tags || "[]") });
- } catch (error) {
+ } catch {
return NextResponse.json({ error: 'Error creating page' }, { status: 500 });
}
}
diff --git a/src/app/api/settings/password/route.ts b/src/app/api/settings/password/route.ts
index e31e352..4f0cea6 100644
--- a/src/app/api/settings/password/route.ts
+++ b/src/app/api/settings/password/route.ts
@@ -6,8 +6,7 @@ export async function PUT(req: Request) {
try {
const { currentPassword, newPassword } = await req.json();
- // Cast to any to bypass build error until server restart allows prisma generate to run
- const settings = await (prisma as any).globalSettings.findUnique({
+ const settings = await prisma.globalSettings.findUnique({
where: { id: "default" },
});
@@ -22,13 +21,13 @@ export async function PUT(req: Request) {
const hashedPassword = await hashPassword(newPassword);
- await (prisma as any).globalSettings.update({
+ await prisma.globalSettings.update({
where: { id: "default" },
data: { password: hashedPassword },
});
return NextResponse.json({ success: true });
- } catch (error) {
+ } catch {
return NextResponse.json({ error: "Failed to change password" }, { status: 500 });
}
}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index ee77d9d..a5d9db4 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,4 +1,4 @@
-"use client";
+"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
@@ -8,7 +8,7 @@ export default function LoginPage() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [rememberMe, setRememberMe] = useState(false);
- const [duration, setDuration] = useState("1"); // days
+ const [duration, setDuration] = useState("1");
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
@@ -19,23 +19,21 @@ export default function LoginPage() {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ password }),
+ body: JSON.stringify({
+ password,
+ rememberMe,
+ durationDays: parseInt(duration, 10),
+ }),
});
if (res.ok) {
- let cookieString = "auth=true; path=/";
- if (rememberMe) {
- const seconds = parseInt(duration) * 24 * 60 * 60;
- cookieString += `; max-age=${seconds}`;
- }
- document.cookie = cookieString;
router.push("/");
} else {
const data = await res.json();
- setError(data.error || "登录失败");
+ setError(data.error || "Login failed");
}
- } catch (err) {
- setError("发生错误,请重试");
+ } catch {
+ setError("Something went wrong. Please try again.");
}
};
@@ -46,8 +44,8 @@ export default function LoginPage() {
-
欢迎回来
-
请输入访问密码以进入您的个人空间
+
Welcome back
+
Enter your access password to continue.
- NoteAI • 您的私人第二大脑
+ NoteAI - Your private second brain
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 06a57e2..32dc2aa 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -3,7 +3,7 @@
import { SidebarContent, ResizableSidebar } from "@/components/sidebar";
import { Editor } from "@/components/editor";
import { useEditorStore } from "@/lib/store";
-import { FileDown, Hash, X, Plus, ChevronLeft, ChevronRight, Sparkles, Lock, Unlock } from "lucide-react";
+import { Hash, X, Plus, ChevronLeft, ChevronRight, Sparkles, Lock, Unlock } from "lucide-react";
import { useSettingsStore } from "@/lib/settings-store";
import { exportPageAsMarkdown } from "@/lib/export";
import { useState } from "react";
@@ -46,14 +46,6 @@ export default function Home() {
- {/* Mobile Back Button */}
-
updatePage(null as any, {} as any)}>
- {/* Note: updatePage isn't the right way to clear selection. We need setPageId(null).
- But store only exposes updatePage. Let's fix store usage or use a store action if available.
- Actually, looking at store.ts, we need `setActivePageId`.
- */}
-