增加API分页和轻量级查询支持,完善备份恢复功能
主要改动: - API: 为 /api/pages 添加 lightweight 模式和分页支持 - 备份恢复: 增强 restore API 的错误处理和验证逻辑 - 工具函数: 新增 page-utils 辅助函数 - 会话管理: 优化 session 和 auth 相关逻辑 - 文档: 添加 CLAUDE.md 项目指南,包含编辑器配置注意事项 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
NoteAI is a local knowledge note-taking application built with Next.js, Prisma (SQLite), and TipTap editor. The app provides a hierarchical page structure with rich text editing, markdown import/export, and password-protected access.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development server (runs on port 3001)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Initialize environment variables
|
||||||
|
npm run env:init
|
||||||
|
|
||||||
|
# Build with environment initialization
|
||||||
|
npm run build:init
|
||||||
|
|
||||||
|
# Start production server (port 3001)
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Type checking and linting together
|
||||||
|
npm run check
|
||||||
|
|
||||||
|
# Run editor block operations tests
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Database Layer (Prisma + SQLite)
|
||||||
|
|
||||||
|
- **Schema**: `prisma/schema.prisma`
|
||||||
|
- **Client**: Singleton instance in `src/lib/prisma.ts`
|
||||||
|
- **Models**:
|
||||||
|
- `Page`: Hierarchical pages with self-referential parent-child relationships. Supports both "file" and "folder" types with ordering, locking, tags, and icons.
|
||||||
|
- `GlobalSettings`: Stores scrypt-hashed password for app-wide authentication.
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
- **Zustand stores** with persistence:
|
||||||
|
- `src/lib/store.ts`: Main editor state (pages, activePageId, CRUD operations)
|
||||||
|
- `src/lib/search-store.ts`: Search functionality state
|
||||||
|
- `src/lib/settings-store.ts`: Application settings state
|
||||||
|
- `src/lib/page-history.ts`: Undo/redo history management with snapshots
|
||||||
|
|
||||||
|
### Authentication & Session
|
||||||
|
|
||||||
|
- **Session management**: `src/lib/session.ts` - Custom HMAC-based session tokens stored in cookies (24h TTL)
|
||||||
|
- **Auth utilities**: `src/lib/auth.ts` - Scrypt password hashing/verification
|
||||||
|
- **API auth**: `src/lib/api-auth.ts` - Middleware for protecting API routes
|
||||||
|
- **Rate limiting**: `src/lib/rate-limit.ts` - Token bucket rate limiter for auth endpoints
|
||||||
|
|
||||||
|
### Editor System (TipTap)
|
||||||
|
|
||||||
|
- **Extensions**: Custom TipTap extensions in `src/components/editor/extensions/`
|
||||||
|
- `callout.ts` & `callout-component.tsx`: Custom callout blocks
|
||||||
|
- `ai-mark.ts`: AI-generated content marking
|
||||||
|
- `task-item.tsx`: Custom task list items
|
||||||
|
- **Block operations**: `src/lib/editor-block-ops.ts` - Core logic for moving/reordering contiguous spans of blocks (drag-and-drop support)
|
||||||
|
- **Tests**: `src/lib/editor-block-ops.test.ts` run via `scripts/run-editor-block-ops-tests.mjs`
|
||||||
|
|
||||||
|
**CRITICAL: Editor Initialization Configuration**
|
||||||
|
|
||||||
|
When modifying `src/components/editor.tsx`, preserve these critical settings for proper markdown import functionality:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In useEditor() configuration:
|
||||||
|
{
|
||||||
|
extensions: [
|
||||||
|
// ... other extensions
|
||||||
|
Markdown.configure({
|
||||||
|
html: true,
|
||||||
|
transformPastedText: true,
|
||||||
|
transformCopiedText: true, // MUST be true for markdown conversion
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content, // MUST pass content prop directly, NOT empty string
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
// ... update logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In useEffect for content updates:
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && content !== editor.getHTML()) {
|
||||||
|
const normalizedContent = cleanupAccidentalStandaloneInlineCode(content);
|
||||||
|
suppressNextUpdateRef.current = true;
|
||||||
|
allowOnUpdateRef.current = false;
|
||||||
|
queueMicrotask(() => {
|
||||||
|
editor.commands.setContent(normalizedContent); // Simple call, no extra options
|
||||||
|
});
|
||||||
|
if (normalizedContent !== content) {
|
||||||
|
onChange(normalizedContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (editor && onEditorReady) {
|
||||||
|
onEditorReady(editor);
|
||||||
|
}
|
||||||
|
}, [content, editor, onEditorReady]); // Dependencies: do NOT include onChange
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common mistakes to avoid:**
|
||||||
|
- Setting `content: ''` instead of `content` breaks markdown import display
|
||||||
|
- Setting `transformCopiedText: false` disables markdown conversion
|
||||||
|
- Adding unnecessary options to `setContent()` can cause whitespace issues
|
||||||
|
- Including `onChange` in useEffect dependencies causes infinite re-renders
|
||||||
|
|
||||||
|
### Import/Export
|
||||||
|
|
||||||
|
- **Markdown import**: `src/lib/markdown-import.ts` - Converts markdown to HTML with special handling for fenced code blocks
|
||||||
|
- **Export**: `src/lib/export.ts` - Handles exporting pages to various formats
|
||||||
|
- **File I/O**: `src/lib/file-io.ts` - File system operations for import/export
|
||||||
|
- **HTML sanitization**: `src/lib/sanitize-html.ts` - Sanitizes HTML content
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
|
||||||
|
All routes in `src/app/api/`:
|
||||||
|
- `/api/auth/login` & `/api/auth/logout`: Authentication
|
||||||
|
- `/api/settings/init`: First-time password setup (requires `INIT_SETUP_TOKEN` in production)
|
||||||
|
- `/api/settings/password`: Change password
|
||||||
|
- `/api/settings/status`: Check initialization status
|
||||||
|
- `/api/settings/restore`: Restore from backup
|
||||||
|
- `/api/pages`: CRUD operations for pages
|
||||||
|
- `/api/pages/[id]`: Individual page operations
|
||||||
|
- `/api/pages/reorder`: Batch reorder pages
|
||||||
|
- `/api/ai/chat`: AI chat integration
|
||||||
|
|
||||||
|
### Special Features
|
||||||
|
|
||||||
|
- **Wiki links**: `src/lib/wiki-links.ts` - Internal page linking system
|
||||||
|
- **Page utilities**: `src/lib/page-utils.ts` - Helper functions for page operations
|
||||||
|
- **Search**: `src/lib/search-query.ts` - Search query parsing and execution
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required in `.env`:
|
||||||
|
```env
|
||||||
|
DATABASE_URL="file:./dev.db"
|
||||||
|
SESSION_SECRET="<32+ character random string>"
|
||||||
|
INIT_DEFAULT_PASSWORD="<initial password>"
|
||||||
|
INIT_SETUP_TOKEN="<optional token for production init>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Aliases
|
||||||
|
|
||||||
|
- `@/*` maps to `src/*` (configured in `tsconfig.json`)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests use Node's native test runner with `--experimental-strip-types` flag. Test files use `.test.ts` extension and are run via custom scripts in `scripts/` directory.
|
||||||
|
|
||||||
|
## Database Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate Prisma client after schema changes
|
||||||
|
npx prisma generate
|
||||||
|
|
||||||
|
# Create and apply migrations
|
||||||
|
npx prisma migrate dev
|
||||||
|
|
||||||
|
# Apply migrations in production
|
||||||
|
npx prisma migrate deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Targets
|
||||||
|
|
||||||
|
Prisma is configured for both native and `linux-musl-openssl-3.0.x` targets to support Docker deployments.
|
||||||
@@ -1,22 +1,61 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { requireApiAuth } from '@/lib/api-auth';
|
import { requireApiAuth } from '@/lib/api-auth';
|
||||||
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
||||||
import { MAX_TITLE_LENGTH, safeParseTags, normalizeTags } from '@/lib/page-utils';
|
import { MAX_TITLE_LENGTH, safeParseTags, normalizeTags } from '@/lib/page-utils';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
const authError = await requireApiAuth();
|
const authError = await requireApiAuth();
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pages = await prisma.page.findMany({
|
const searchParams = request.nextUrl.searchParams;
|
||||||
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
// lightweight 模式:不返回 content,用于侧边栏等场景
|
||||||
});
|
const lightweight = searchParams.get('lightweight') === 'true';
|
||||||
|
const page = parseInt(searchParams.get('page') || '0', 10);
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '0', 10);
|
||||||
|
|
||||||
|
// 默认行为:返回所有页面(向后兼容)
|
||||||
|
const usePagination = limit > 0 && limit <= 1000 && page > 0;
|
||||||
|
|
||||||
|
if (!usePagination) {
|
||||||
|
const pages = await prisma.page.findMany({
|
||||||
|
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||||
|
});
|
||||||
|
const parsedPages = pages.map((p) => ({
|
||||||
|
...p,
|
||||||
|
content: lightweight ? '' : p.content,
|
||||||
|
tags: safeParseTags(p.tags),
|
||||||
|
}));
|
||||||
|
return NextResponse.json(parsedPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
const [pages, total] = await Promise.all([
|
||||||
|
prisma.page.findMany({
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
||||||
|
}),
|
||||||
|
prisma.page.count(),
|
||||||
|
]);
|
||||||
|
|
||||||
const parsedPages = pages.map((p) => ({
|
const parsedPages = pages.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
|
content: lightweight ? '' : p.content,
|
||||||
tags: safeParseTags(p.tags),
|
tags: safeParseTags(p.tags),
|
||||||
}));
|
}));
|
||||||
return NextResponse.json(parsedPages);
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: parsedPages,
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return NextResponse.json({ error: 'Error fetching pages' }, { status: 500 });
|
return NextResponse.json({ error: 'Error fetching pages' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import JSZip from "jszip";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { requireApiAuth } from "@/lib/api-auth";
|
import { requireApiAuth } from "@/lib/api-auth";
|
||||||
import { markdownToImportHtml } from "@/lib/markdown-import";
|
import { markdownToImportHtml } from "@/lib/markdown-import";
|
||||||
|
import { pageToMarkdown } from "@/lib/export";
|
||||||
|
import { sanitizeFilename } from "@/lib/page-utils";
|
||||||
|
import { writeFile, mkdir } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
type ZipEntry = {
|
type ZipEntry = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -93,6 +97,52 @@ function collectFolderPaths(entries: ZipEntry[]): string[] {
|
|||||||
return Array.from(folderSet).sort((a, b) => getDepth(a) - getDepth(b));
|
return Array.from(folderSet).sort((a, b) => getDepth(a) - getDepth(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建当前数据库的备份 ZIP
|
||||||
|
*/
|
||||||
|
async function createBackupZip(): Promise<Buffer> {
|
||||||
|
const allPages = await prisma.page.findMany({
|
||||||
|
orderBy: [{ parentId: "asc" }, { order: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
// 递归添加页面到 ZIP
|
||||||
|
function addPagesToZip(parentId: string | null, currentFolder: JSZip) {
|
||||||
|
const children = allPages.filter((p) => p.parentId === parentId);
|
||||||
|
|
||||||
|
for (const page of children) {
|
||||||
|
const cleanTitle = sanitizeFilename(page.title);
|
||||||
|
|
||||||
|
if (page.type === "folder") {
|
||||||
|
const subFolder = currentFolder.folder(cleanTitle);
|
||||||
|
if (subFolder) {
|
||||||
|
addPagesToZip(page.id, subFolder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const markdown = pageToMarkdown({
|
||||||
|
id: page.id,
|
||||||
|
title: page.title,
|
||||||
|
content: page.content,
|
||||||
|
parentId: page.parentId,
|
||||||
|
type: page.type as "file" | "folder",
|
||||||
|
tags: JSON.parse(page.tags),
|
||||||
|
icon: page.icon,
|
||||||
|
order: page.order,
|
||||||
|
isLocked: page.isLocked,
|
||||||
|
createdAt: page.createdAt.toISOString(),
|
||||||
|
updatedAt: page.updatedAt.toISOString(),
|
||||||
|
});
|
||||||
|
currentFolder.file(`${cleanTitle}.md`, markdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addPagesToZip(null, zip);
|
||||||
|
|
||||||
|
return await zip.generateAsync({ type: "nodebuffer" });
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const authError = await requireApiAuth();
|
const authError = await requireApiAuth();
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
@@ -152,10 +202,31 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 在删除数据前创建自动备份
|
||||||
|
let backupBuffer: Buffer | null = null;
|
||||||
|
let backupPath: string | null = null;
|
||||||
|
try {
|
||||||
|
backupBuffer = await createBackupZip();
|
||||||
|
|
||||||
|
// 保存备份到 backups 目录
|
||||||
|
const backupsDir = join(process.cwd(), "backups");
|
||||||
|
await mkdir(backupsDir, { recursive: true });
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||||
|
backupPath = join(backupsDir, `auto-backup-before-restore-${timestamp}.zip`);
|
||||||
|
await writeFile(backupPath, backupBuffer);
|
||||||
|
|
||||||
|
console.log(`Auto backup created: ${backupPath}`);
|
||||||
|
} catch (backupError) {
|
||||||
|
console.error("Failed to create backup before restore:", backupError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create backup before restore. Restore aborted for safety." },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// ⚠️ 注意:这是一个破坏性操作,会删除所有现有页面后重新导入。
|
// 删除所有现有页面(已在上面创建备份)
|
||||||
// TODO: 后续可在此添加自动备份逻辑(导出当前数据到临时文件),
|
|
||||||
// 或在前端恢复前强制用户先手动备份。
|
|
||||||
await tx.page.deleteMany();
|
await tx.page.deleteMany();
|
||||||
|
|
||||||
const folderIdMap = new Map<string, string>();
|
const folderIdMap = new Map<string, string>();
|
||||||
@@ -195,6 +266,7 @@ export async function POST(req: NextRequest) {
|
|||||||
count: entries.length,
|
count: entries.length,
|
||||||
files: parsedFiles.length,
|
files: parsedFiles.length,
|
||||||
folders: folderPaths.length,
|
folders: folderPaths.length,
|
||||||
|
backupPath: backupPath ? backupPath.replace(process.cwd(), "") : null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Restore failed:", error);
|
console.error("Restore failed:", error);
|
||||||
|
|||||||
+2
-2
@@ -5,9 +5,9 @@ import { getSessionCookieName, verifySessionToken } from "@/lib/session";
|
|||||||
export async function requireApiAuth(): Promise<NextResponse | null> {
|
export async function requireApiAuth(): Promise<NextResponse | null> {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const authCookie = cookieStore.get(getSessionCookieName());
|
const authCookie = cookieStore.get(getSessionCookieName());
|
||||||
const isAuthenticated = authCookie ? await verifySessionToken(authCookie.value) : false;
|
const session = authCookie ? await verifySessionToken(authCookie.value) : null;
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!session) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -3,6 +3,7 @@ import JSZip from "jszip";
|
|||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import { Page } from "./store";
|
import { Page } from "./store";
|
||||||
import { useSettingsStore } from "./settings-store";
|
import { useSettingsStore } from "./settings-store";
|
||||||
|
import { sanitizeFilename } from "./page-utils";
|
||||||
|
|
||||||
import { gfm } from "turndown-plugin-gfm";
|
import { gfm } from "turndown-plugin-gfm";
|
||||||
|
|
||||||
@@ -43,11 +44,6 @@ export function htmlToMarkdown(html: string): string {
|
|||||||
return turndownService.turndown(html);
|
return turndownService.turndown(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to sanitize filenames
|
|
||||||
function sanitizeFilename(name: string): string {
|
|
||||||
return name.replace(/[<>:"/\\|?*]/g, '_').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to escape YAML strings
|
// Helper to escape YAML strings
|
||||||
function escapeYamlString(str: string): string {
|
function escapeYamlString(str: string): string {
|
||||||
return str.replace(/"/g, '\\"');
|
return str.replace(/"/g, '\\"');
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ export const MAX_TITLE_LENGTH = 200;
|
|||||||
export const MAX_TAGS = 20;
|
export const MAX_TAGS = 20;
|
||||||
export const MAX_TAG_LENGTH = 50;
|
export const MAX_TAG_LENGTH = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理文件名中的非法字符
|
||||||
|
*/
|
||||||
|
export function sanitizeFilename(name: string): string {
|
||||||
|
return name.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全解析 JSON 格式的标签字符串
|
* 安全解析 JSON 格式的标签字符串
|
||||||
*/
|
*/
|
||||||
|
|||||||
+16
-6
@@ -2,6 +2,8 @@ const SESSION_COOKIE_NAME = "auth";
|
|||||||
const SESSION_TTL_SECONDS = 24 * 60 * 60;
|
const SESSION_TTL_SECONDS = 24 * 60 * 60;
|
||||||
|
|
||||||
type SessionPayload = {
|
type SessionPayload = {
|
||||||
|
userId: string;
|
||||||
|
tokenId: string;
|
||||||
exp: number;
|
exp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,6 +90,8 @@ export async function createSessionToken(days?: number): Promise<string> {
|
|||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const ttl = getSessionTtlSeconds(days);
|
const ttl = getSessionTtlSeconds(days);
|
||||||
const payload: SessionPayload = {
|
const payload: SessionPayload = {
|
||||||
|
userId: "default", // 单用户系统,使用固定 ID
|
||||||
|
tokenId: crypto.randomUUID(),
|
||||||
exp: now + ttl,
|
exp: now + ttl,
|
||||||
};
|
};
|
||||||
const encodedPayload = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));
|
const encodedPayload = toBase64Url(new TextEncoder().encode(JSON.stringify(payload)));
|
||||||
@@ -95,26 +99,32 @@ export async function createSessionToken(days?: number): Promise<string> {
|
|||||||
return `${encodedPayload}.${signature}`;
|
return `${encodedPayload}.${signature}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifySessionToken(token: string): Promise<boolean> {
|
export async function verifySessionToken(token: string): Promise<SessionPayload | null> {
|
||||||
const [payloadPart, signaturePart] = token.split(".");
|
const [payloadPart, signaturePart] = token.split(".");
|
||||||
if (!payloadPart || !signaturePart) {
|
if (!payloadPart || !signaturePart) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidSignature = await verify(payloadPart, signaturePart);
|
const isValidSignature = await verify(payloadPart, signaturePart);
|
||||||
if (!isValidSignature) {
|
if (!isValidSignature) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payloadText = new TextDecoder().decode(fromBase64Url(payloadPart));
|
const payloadText = new TextDecoder().decode(fromBase64Url(payloadPart));
|
||||||
const payload = JSON.parse(payloadText) as SessionPayload;
|
const payload = JSON.parse(payloadText) as SessionPayload;
|
||||||
if (!payload.exp || typeof payload.exp !== "number") {
|
if (!payload.exp || typeof payload.exp !== "number") {
|
||||||
return false;
|
return null;
|
||||||
|
}
|
||||||
|
if (!payload.userId || !payload.tokenId) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
return payload.exp > now;
|
if (payload.exp <= now) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@ export default async function proxy(request: NextRequest) {
|
|||||||
const isLoginPage = request.nextUrl.pathname === '/login';
|
const isLoginPage = request.nextUrl.pathname === '/login';
|
||||||
const isPublicPath = PUBLIC_PATHS.has(request.nextUrl.pathname);
|
const isPublicPath = PUBLIC_PATHS.has(request.nextUrl.pathname);
|
||||||
const isPublicApiPath = PUBLIC_API_PATHS.has(request.nextUrl.pathname);
|
const isPublicApiPath = PUBLIC_API_PATHS.has(request.nextUrl.pathname);
|
||||||
const isAuthenticated = authCookie ? await verifySessionToken(authCookie.value) : false;
|
const session = authCookie ? await verifySessionToken(authCookie.value) : null;
|
||||||
|
const isAuthenticated = session !== null;
|
||||||
|
|
||||||
if (!isAuthenticated && !isPublicPath && !isPublicApiPath) {
|
if (!isAuthenticated && !isPublicPath && !isPublicApiPath) {
|
||||||
if (request.nextUrl.pathname.startsWith('/api')) {
|
if (request.nextUrl.pathname.startsWith('/api')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user