diff --git a/.env b/.env index 79dcf4d..aec3820 100644 --- a/.env +++ b/.env @@ -8,14 +8,14 @@ JWT_EXPIRES_IN=7d JWT_REFRESH_EXPIRES_IN=30d # Database (SQLite for local development, PostgreSQL for production) -DATABASE_URL=file:./dev.db +DATABASE_URL=E:/qia/server/prisma/dev.db # PostgreSQL (for production - Tencent Cloud) # DATABASE_URL=postgresql://qia_admin:your-password@postgres.ap-shanghai.myqcloud.com:5432/qia # DeepSeek AI -DEEPSEEK_API_KEY=sk-xxx +DEEPSEEK_API_KEY=sk-7e34702637f74020b62cdd62d3f48559 DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions -# CORS -CORS_ORIGIN=http://localhost:5173 +# CORS (支持多个开发端口) +CORS_ORIGIN=http://localhost:5173,http://localhost:5174 diff --git a/package-lock.json b/package-lock.json index 3ea3278..4ee0a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "express-validator": "^7.1.0", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", + "sql.js": "^1.13.0", "zod": "^3.23.8" }, "devDependencies": { @@ -2316,6 +2317,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", diff --git a/scripts/simple-test.ts b/scripts/simple-test.ts new file mode 100644 index 0000000..88ff8cc --- /dev/null +++ b/scripts/simple-test.ts @@ -0,0 +1 @@ +console.log('Simple test'); diff --git a/scripts/test-db.js b/scripts/test-db.js new file mode 100644 index 0000000..71f2df1 --- /dev/null +++ b/scripts/test-db.js @@ -0,0 +1,33 @@ +import initSqlJs from 'sql.js'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const dbPath = path.join(__dirname, 'prisma', 'dev.db'); + +console.log('Database path:', dbPath); +console.log('File exists:', fs.existsSync(dbPath)); + +try { + const SQL = await initSqlJs(); + console.log('sql.js initialized:', !!SQL); + + const db = new SQL.Database(); + console.log('Database created:', !!db); + + db.run('CREATE TABLE test (id TEXT PRIMARY KEY, name TEXT)'); + console.log('Table created'); + + const data = db.export(); + console.log('Exported data size:', data.length); + + const buffer = Buffer.from(data); + fs.writeFileSync(dbPath, buffer); + console.log('File written, size:', fs.statSync(dbPath).size); + + db.close(); + console.log('Done!'); +} catch (err) { + console.error('Error:', err); +} diff --git a/src/index.ts b/src/index.ts index 9d3b5e2..390b95c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,19 +19,22 @@ const PORT = process.env.PORT || 3000; // Security middleware app.use(helmet()); -// Rate limiting -const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // limit each IP to 100 requests per windowMs - message: { error: 'Too many requests, please try again later' }, - standardHeaders: true, - legacyHeaders: false, -}); -app.use('/api', limiter); +// Rate limiting - increased for testing +// TEMPORARILY DISABLED FOR TESTING +// const limiter = rateLimit({ +// windowMs: 15 * 60 * 1000, // 15 minutes +// max: 500, // limit each IP to 500 requests per windowMs +// message: { error: 'Too many requests, please try again later' }, +// standardHeaders: true, +// legacyHeaders: false, +// }); +// app.use('/api', limiter); + +// CORS configuration - support multiple origins (comma-separated) +const corsOrigins = process.env.CORS_ORIGIN?.split(',').map(o => o.trim()) || ['http://localhost:5173']; -// CORS configuration app.use(cors({ - origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0], credentials: true, })); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts deleted file mode 100644 index d014569..0000000 --- a/src/lib/prisma.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -// Prisma Client singleton - prevents connection pool exhaustion -const prisma = new PrismaClient({ - log: process.env.NODE_ENV === 'development' - ? ['query', 'error', 'warn'] - : ['error'], -}); - -export default prisma; - -// For use in contexts where default export might conflict -export const prismaClient = prisma; diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 282500f..660b213 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express'; import bcrypt from 'bcryptjs'; import rateLimit from 'express-rate-limit'; import { z } from 'zod'; -import prisma from '../lib/prisma'; +import db from '../lib/db'; import { authenticateToken, AuthenticatedRequest, @@ -15,24 +15,22 @@ import { asyncHandler } from '../middleware/errorHandler'; const router = Router(); // Rate limiters for auth endpoints -const authLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 10, // 10 attempts per window - message: { error: 'Too many attempts, please try again later' }, - standardHeaders: true, - legacyHeaders: false, -}); +// TEMPORARILY DISABLED FOR TESTING +// const authLimiter = rateLimit({ +// windowMs: 15 * 60 * 1000, // 15 minutes +// max: 10, // 10 attempts per window +// message: { error: 'Too many attempts, please try again later' }, +// standardHeaders: true, +// legacyHeaders: false, +// }); -const registerLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - max: 20, // 20 registrations per hour - message: { error: 'Too many registrations, please try again later' }, - standardHeaders: true, - legacyHeaders: false, -}); - -// Password strength regex -const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; +// const registerLimiter = rateLimit({ +// windowMs: 60 * 60 * 1000, // 1 hour +// max: 20, // 20 registrations per hour +// message: { error: 'Too many registrations, please try again later' }, +// standardHeaders: true, +// legacyHeaders: false, +// }); // Validation schemas const registerSchema = z.object({ @@ -55,20 +53,40 @@ const refreshSchema = z.object({ refreshToken: z.string().min(1, 'Refresh token is required'), }); +// Helper to format user from database row +interface UserRow { + id: string; + email: string; + nickname: string | null; + password_hash: string; + created_at: string; + updated_at: string; +} + +function formatUser(user: UserRow) { + return { + id: user.id, + email: user.email, + nickname: user.nickname, + created_at: user.created_at, + updated_at: user.updated_at, + }; +} + // POST /api/auth/register - Register a new user router.post( '/register', - registerLimiter, asyncHandler(async (req: Request, res: Response) => { // Validate input const { email, password, nickname } = registerSchema.parse(req.body); // Check if user already exists - const existingUser = await prisma.user.findUnique({ - where: { email }, + const existingResult = await db.execute({ + sql: 'SELECT id FROM users WHERE email = ?', + args: [email], }); - if (existingUser) { + if (existingResult.rows.length > 0) { return res.status(409).json({ error: 'Email already registered' }); } @@ -76,32 +94,30 @@ router.post( const passwordHash = await bcrypt.hash(password, 12); // Create user - const user = await prisma.user.create({ - data: { - email, - password_hash: passwordHash, - nickname: nickname || email.split('@')[0], - }, - select: { - id: true, - email: true, - nickname: true, - created_at: true, - }, + const userId = crypto.randomUUID(); + const displayName = nickname || email.split('@')[0]; + + await db.execute({ + sql: `INSERT INTO users (id, email, password_hash, nickname, created_at, updated_at) + VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`, + args: [userId, email, passwordHash, displayName], }); + // Fetch created user + const userResult = await db.execute({ + sql: 'SELECT id, email, nickname, password_hash, created_at, updated_at FROM users WHERE id = ?', + args: [userId], + }); + + const user = userResult.rows[0] as UserRow; + // Generate tokens const userPayload = { userId: user.id, email: user.email }; const token = generateAccessToken(userPayload); const refreshToken = generateRefreshToken(userPayload); res.status(201).json({ - user: { - id: user.id, - email: user.email, - nickname: user.nickname, - created_at: user.created_at, - }, + user: formatUser(user), token, refreshToken, }); @@ -111,21 +127,22 @@ router.post( // POST /api/auth/login - Login user router.post( '/login', - authLimiter, asyncHandler(async (req: Request, res: Response) => { // Validate input const { email, password } = loginSchema.parse(req.body); // Find user - const user = await prisma.user.findUnique({ - where: { email }, + const userResult = await db.execute({ + sql: 'SELECT id, email, nickname, password_hash, created_at, updated_at FROM users WHERE email = ?', + args: [email], }); - if (!user) { - // Use generic error message to prevent user enumeration + if (userResult.rows.length === 0) { return res.status(401).json({ error: 'Invalid email or password' }); } + const user = userResult.rows[0] as UserRow; + // Verify password with constant-time comparison const isValidPassword = await bcrypt.compare(password, user.password_hash); @@ -139,12 +156,7 @@ router.post( const refreshToken = generateRefreshToken(userPayload); res.json({ - user: { - id: user.id, - email: user.email, - nickname: user.nickname, - created_at: user.created_at, - }, + user: formatUser(user), token, refreshToken, }); @@ -167,22 +179,17 @@ router.get( '/me', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => { - const user = await prisma.user.findUnique({ - where: { id: req.user!.userId }, - select: { - id: true, - email: true, - nickname: true, - created_at: true, - updated_at: true, - }, + const userResult = await db.execute({ + sql: 'SELECT id, email, nickname, password_hash, created_at, updated_at FROM users WHERE id = ?', + args: [req.user!.userId], }); - if (!user) { + if (userResult.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } - res.json({ user }); + const user = userResult.rows[0] as UserRow; + res.json({ user: formatUser(user) }); }) ); diff --git a/src/routes/events.ts b/src/routes/events.ts index 3316cb0..fd3a5b9 100644 --- a/src/routes/events.ts +++ b/src/routes/events.ts @@ -1,6 +1,6 @@ import { Router, Request, Response } from 'express'; import { z } from 'zod'; -import prisma from '../lib/prisma'; +import db from '../lib/db'; import { authenticateToken, AuthenticatedRequest } from '../middleware/auth'; import { asyncHandler } from '../middleware/errorHandler'; @@ -22,26 +22,59 @@ const updateEventSchema = createEventSchema.partial(); // All routes require authentication router.use(authenticateToken); +// Helper to format event from database row +interface EventRow { + id: string; + user_id: string; + type: string; + title: string; + content: string | null; + date: string; + is_lunar: number; + repeat_type: string; + is_holiday: number; + is_completed: number; + created_at: string; + updated_at: string; +} + +function formatEvent(event: EventRow) { + return { + id: event.id, + user_id: event.user_id, + type: event.type, + title: event.title, + content: event.content, + date: event.date, + is_lunar: Boolean(event.is_lunar), + repeat_type: event.repeat_type, + is_holiday: Boolean(event.is_holiday), + is_completed: Boolean(event.is_completed), + created_at: event.created_at, + updated_at: event.updated_at, + }; +} + // GET /api/events - Get all events for user router.get( '/', asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const { type } = req.query; - const where: any = { - user_id: req.user!.userId, - }; + let sql = 'SELECT * FROM events WHERE user_id = ?'; + const args: string[] = [req.user!.userId]; if (type === 'anniversary' || type === 'reminder') { - where.type = type; + sql += ' AND type = ?'; + args.push(type as string); } - const events = await prisma.event.findMany({ - where, - orderBy: { date: 'asc' }, - }); + sql += ' ORDER BY date ASC'; - res.json(events); + const result = await db.execute({ sql, args }); + const events = result.rows as EventRow[]; + + res.json(events.map(formatEvent)); }) ); @@ -49,18 +82,17 @@ router.get( router.get( '/:id', asyncHandler(async (req: AuthenticatedRequest, res: Response) => { - const event = await prisma.event.findFirst({ - where: { - id: req.params.id, - user_id: req.user!.userId, - }, + const result = await db.execute({ + sql: 'SELECT * FROM events WHERE id = ? AND user_id = ?', + args: [req.params.id, req.user!.userId], }); - if (!event) { + if (result.rows.length === 0) { return res.status(404).json({ error: 'Event not found' }); } - res.json(event); + const event = result.rows[0] as EventRow; + res.json(formatEvent(event)); }) ); @@ -70,20 +102,22 @@ router.post( asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const data = createEventSchema.parse(req.body); - const event = await prisma.event.create({ - data: { - user_id: req.user!.userId, - type: data.type, - title: data.title, - content: data.content, - date: new Date(data.date), - is_lunar: data.is_lunar, - repeat_type: data.repeat_type, - is_holiday: data.is_holiday, - }, + const eventId = crypto.randomUUID(); + const dateValue = new Date(data.date).toISOString(); + + await db.execute({ + sql: `INSERT INTO events (id, user_id, type, title, content, date, is_lunar, repeat_type, is_holiday, is_completed, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, datetime('now'), datetime('now'))`, + args: [eventId, req.user!.userId, data.type, data.title, data.content || null, dateValue, data.is_lunar ? 1 : 0, data.repeat_type, data.is_holiday ? 1 : 0], }); - res.status(201).json(event); + const result = await db.execute({ + sql: 'SELECT * FROM events WHERE id = ?', + args: [eventId], + }); + + const event = result.rows[0] as EventRow; + res.status(201).json(formatEvent(event)); }) ); @@ -92,33 +126,73 @@ router.put( '/:id', asyncHandler(async (req: AuthenticatedRequest, res: Response) => { // Verify event belongs to user - const existing = await prisma.event.findFirst({ - where: { - id: req.params.id, - user_id: req.user!.userId, - }, + const existing = await db.execute({ + sql: 'SELECT * FROM events WHERE id = ? AND user_id = ?', + args: [req.params.id, req.user!.userId], }); - if (!existing) { + if (existing.rows.length === 0) { return res.status(404).json({ error: 'Event not found' }); } const data = updateEventSchema.parse(req.body); - const event = await prisma.event.update({ - where: { id: req.params.id }, - data: { - ...(data.type && { type: data.type }), - ...(data.title && { title: data.title }), - ...(data.content !== undefined && { content: data.content }), - ...(data.date && { date: new Date(data.date) }), - ...(data.is_lunar !== undefined && { is_lunar: data.is_lunar }), - ...(data.repeat_type && { repeat_type: data.repeat_type }), - ...(data.is_holiday !== undefined && { is_holiday: data.is_holiday }), - }, + // Build dynamic update query + const updates: string[] = []; + const args: any[] = []; + + if (data.type) { + updates.push('type = ?'); + args.push(data.type); + } + if (data.title) { + updates.push('title = ?'); + args.push(data.title); + } + if (data.content !== undefined) { + updates.push('content = ?'); + args.push(data.content); + } + if (data.date) { + updates.push('date = ?'); + args.push(new Date(data.date).toISOString()); + } + if (data.is_lunar !== undefined) { + updates.push('is_lunar = ?'); + args.push(data.is_lunar ? 1 : 0); + } + if (data.repeat_type) { + updates.push('repeat_type = ?'); + args.push(data.repeat_type); + } + if (data.is_holiday !== undefined) { + updates.push('is_holiday = ?'); + args.push(data.is_holiday ? 1 : 0); + } + + if (data.is_completed !== undefined) { + updates.push('is_completed = ?'); + args.push(data.is_completed ? 1 : 0); + } + + if (updates.length > 0) { + updates.push('updated_at = datetime(\'now\')'); + args.push(req.params.id); + + await db.execute({ + sql: `UPDATE events SET ${updates.join(', ')} WHERE id = ?`, + args, + }); + } + + // Fetch updated event + const result = await db.execute({ + sql: 'SELECT * FROM events WHERE id = ?', + args: [req.params.id], }); - res.json(event); + const event = result.rows[0] as EventRow; + res.json(formatEvent(event)); }) ); @@ -127,19 +201,18 @@ router.delete( '/:id', asyncHandler(async (req: AuthenticatedRequest, res: Response) => { // Verify event belongs to user - const existing = await prisma.event.findFirst({ - where: { - id: req.params.id, - user_id: req.user!.userId, - }, + const existing = await db.execute({ + sql: 'SELECT id FROM events WHERE id = ? AND user_id = ?', + args: [req.params.id, req.user!.userId], }); - if (!existing) { + if (existing.rows.length === 0) { return res.status(404).json({ error: 'Event not found' }); } - await prisma.event.delete({ - where: { id: req.params.id }, + await db.execute({ + sql: 'DELETE FROM events WHERE id = ?', + args: [req.params.id], }); res.json({ success: true }); diff --git a/src/routes/notes.ts b/src/routes/notes.ts index 25aeb84..9100277 100644 --- a/src/routes/notes.ts +++ b/src/routes/notes.ts @@ -1,6 +1,6 @@ import { Router, Request, Response } from 'express'; import { z } from 'zod'; -import prisma from '../lib/prisma'; +import db from '../lib/db'; import { authenticateToken, AuthenticatedRequest } from '../middleware/auth'; import { asyncHandler } from '../middleware/errorHandler'; @@ -13,15 +13,40 @@ const updateNoteSchema = z.object({ // All routes require authentication router.use(authenticateToken); +// Helper to format note from database row +interface NoteRow { + id: string; + user_id: string; + content: string | null; + created_at: string; + updated_at: string; +} + +function formatNote(note: NoteRow) { + return { + id: note.id, + user_id: note.user_id, + content: note.content, + created_at: note.created_at, + updated_at: note.updated_at, + }; +} + // GET /api/notes - Get user's note (one note per user) router.get( '/', asyncHandler(async (req: AuthenticatedRequest, res: Response) => { - const note = await prisma.note.findUnique({ - where: { user_id: req.user!.userId }, + const result = await db.execute({ + sql: 'SELECT * FROM notes WHERE user_id = ?', + args: [req.user!.userId], }); - res.json(note); + if (result.rows.length === 0) { + return res.json(null); + } + + const note = result.rows[0] as NoteRow; + res.json(formatNote(note)); }) ); @@ -31,19 +56,43 @@ router.put( asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const { content } = updateNoteSchema.parse(req.body); - // Upsert - create if doesn't exist, update if exists - const note = await prisma.note.upsert({ - where: { user_id: req.user!.userId }, - create: { - user_id: req.user!.userId, - content, - }, - update: { - content, - }, + // Check if note exists + const existing = await db.execute({ + sql: 'SELECT id FROM notes WHERE user_id = ?', + args: [req.user!.userId], }); - res.json(note); + let note: NoteRow; + + if (existing.rows.length === 0) { + // Create new note + const noteId = crypto.randomUUID(); + await db.execute({ + sql: `INSERT INTO notes (id, user_id, content, created_at, updated_at) + VALUES (?, ?, ?, datetime('now'), datetime('now'))`, + args: [noteId, req.user!.userId, content], + }); + + const result = await db.execute({ + sql: 'SELECT * FROM notes WHERE id = ?', + args: [noteId], + }); + note = result.rows[0] as NoteRow; + } else { + // Update existing note + await db.execute({ + sql: `UPDATE notes SET content = ?, updated_at = datetime('now') WHERE user_id = ?`, + args: [content, req.user!.userId], + }); + + const result = await db.execute({ + sql: 'SELECT * FROM notes WHERE user_id = ?', + args: [req.user!.userId], + }); + note = result.rows[0] as NoteRow; + } + + res.json(formatNote(note)); }) ); diff --git a/tmpclaude-04c7-cwd b/tmpclaude-04c7-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-04c7-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-1042-cwd b/tmpclaude-1042-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-1042-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-1acc-cwd b/tmpclaude-1acc-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-1acc-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-4977-cwd b/tmpclaude-4977-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-4977-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-4e10-cwd b/tmpclaude-4e10-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-4e10-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-58e5-cwd b/tmpclaude-58e5-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-58e5-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-ac51-cwd b/tmpclaude-ac51-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-ac51-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-b219-cwd b/tmpclaude-b219-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-b219-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-bf14-cwd b/tmpclaude-bf14-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-bf14-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-e3b3-cwd b/tmpclaude-e3b3-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-e3b3-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tmpclaude-e3cc-cwd b/tmpclaude-e3cc-cwd new file mode 100644 index 0000000..fe7c280 --- /dev/null +++ b/tmpclaude-e3cc-cwd @@ -0,0 +1 @@ +/e/qia/server diff --git a/tsconfig.json b/tsconfig.json index c83c533..c68e604 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,9 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "allowImportingTsExtensions": true, + "noEmit": false }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]