feat: complete backend API with JWT auth, events, notes, AI routes

- Add Express.js server with TypeScript
- Configure Prisma ORM with PostgreSQL schema
- Implement JWT authentication (register, login, logout, refresh)
- Add rate limiting for auth endpoints (10 attempts/15min)
- Password strength validation (8+ chars, uppercase, lowercase, number)
- Events CRUD API (anniversaries and reminders)
- Notes API (single note per user)
- AI parse endpoint with DeepSeek integration
- Security: Helmet, rate limiting, input validation, error handling
- Fix: JWT_SECRET environment variable validation

Code review: Architect approved
Tests: Build verified
This commit is contained in:
ddshi 2026-01-29 13:08:48 +08:00
commit 55627762e1
13 changed files with 972 additions and 0 deletions

18
.env Normal file
View File

@ -0,0 +1,18 @@
# Server
PORT=3000
NODE_ENV=development
# JWT
JWT_SECRET=dev-secret-key-do-not-use-in-production
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
# Database (PostgreSQL - update with your local or Tencent Cloud credentials)
DATABASE_URL=postgresql://qia_admin:your-password@postgres.ap-shanghai.myqcloud.com:5432/qia
# DeepSeek AI
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
# CORS
CORS_ORIGIN=http://localhost:5173

22
.env.example Normal file
View File

@ -0,0 +1,22 @@
# Server
PORT=3000
NODE_ENV=development
# JWT
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRES_IN=7d
JWT_REFRESH_EXPIRES_IN=30d
# Database (腾讯云PostgreSQL)
DB_HOST=postgres.ap-shanghai.myqcloud.com
DB_PORT=5432
DB_NAME=qia
DB_USER=qia_admin
DB_PASSWORD=your-database-password
# DeepSeek AI
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
# CORS
CORS_ORIGIN=http://localhost:5173

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "qia-server",
"version": "0.1.0",
"description": "Backend API for 掐日子(qia) app",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.0",
"express-rate-limit": "^7.4.0",
"express-validator": "^7.1.0",
"helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/express-rate-limit": "^6.0.0",
"@types/helmet": "^4.0.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.5.5",
"prisma": "^5.22.0",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
}
}

96
prisma/schema.prisma Normal file
View File

@ -0,0 +1,96 @@
// 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"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User model
model User {
id String @id @default(uuid())
email String @unique
password_hash String
nickname String?
created_at DateTime @default(now())
updated_at DateTime @updated_at
// Relations
events Event[]
notes Note[]
conversations AICoachConversation[]
@@map("users")
}
// Event model (for both Anniversary and Reminder)
model Event {
id String @id @default(uuid())
user_id String
type EventType // 'anniversary' | 'reminder'
title String
content String? // Only for reminders
date DateTime // For anniversaries: the date; For reminders: the reminder date
is_lunar Boolean @default(false)
repeat_type RepeatType @default(none)
is_holiday Boolean @default(false) // Only for anniversaries
is_completed Boolean @default(false) // Only for reminders
created_at DateTime @default(now())
updated_at DateTime @updated_at
// Relations
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id])
@@index([type])
@@index([date])
@@map("events")
}
// Note model (for quick notes)
model Note {
id String @id @default(uuid())
user_id String
content String @db.Text // HTML content from rich text editor
created_at DateTime @default(now())
updated_at DateTime @updated_at
// Relations
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
// One note per user (based on PRD - 便签编辑区)
@@unique([user_id])
@@map("notes")
}
// AI Conversation model
model AICoachConversation {
id String @id @default(uuid())
user_id String
message String
response String
parsed_data Json? // AI parsed event data
created_at DateTime @default(now())
// Relations
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id])
@@map("ai_coach_conversations")
}
// Enums
enum EventType {
anniversary
reminder
}
enum RepeatType {
yearly
monthly
none
}

66
src/index.ts Normal file
View File

@ -0,0 +1,66 @@
import 'dotenv/config';
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
// Import routes
import authRoutes from './routes/auth';
import eventRoutes from './routes/events';
import noteRoutes from './routes/notes';
import aiRoutes from './routes/ai';
// Import error handler
import { errorHandler } from './middleware/errorHandler';
const app: Application = express();
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);
// CORS configuration
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
}));
// Body parser with size limit (prevent DoS attacks)
app.use(express.json({ limit: '10kb' }));
// Health check
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// API Routes
app.use('/api/auth', authRoutes);
app.use('/api/events', eventRoutes);
app.use('/api/notes', noteRoutes);
app.use('/api/ai', aiRoutes);
// 404 handler
app.use((_req: Request, res: Response) => {
res.status(404).json({ error: 'Not found' });
});
// Error handler
app.use(errorHandler);
// Start server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log(`📝 Environment: ${process.env.NODE_ENV || 'development'}`);
});
export default app;

13
src/lib/prisma.ts Normal file
View File

@ -0,0 +1,13 @@
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;

66
src/middleware/auth.ts Normal file
View File

@ -0,0 +1,66 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export interface JwtPayload {
userId: string;
email: string;
}
export interface AuthenticatedRequest extends Request {
user?: JwtPayload;
}
// JWT configuration with environment validation
const JWT_SECRET_ENV = process.env.JWT_SECRET;
if (!JWT_SECRET_ENV) {
throw new Error('FATAL: JWT_SECRET environment variable is required. Set it in .env file.');
}
export const JWT_SECRET = JWT_SECRET_ENV;
export const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
export const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || '30d';
// Verify JWT token middleware
export const authenticateToken = (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
};
// Generate access token
export const generateAccessToken = (payload: JwtPayload): string => {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
};
// Generate refresh token
export const generateRefreshToken = (payload: JwtPayload): string => {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_REFRESH_EXPIRES_IN,
});
};
// Verify refresh token
export const verifyRefreshToken = (token: string): JwtPayload | null => {
try {
return jwt.verify(token, JWT_SECRET) as JwtPayload;
} catch {
return null;
}
};

View File

@ -0,0 +1,70 @@
import { Request, Response, NextFunction } from 'express';
import { ZodError } from 'zod';
import { Prisma } from '@prisma/client';
interface AppError extends Error {
statusCode?: number;
isOperational?: boolean;
}
export const errorHandler = (
err: AppError,
_req: Request,
res: Response,
_next: NextFunction
) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
console.error(`[Error] ${statusCode}: ${message}`);
if (process.env.NODE_ENV === 'development') {
console.error(err.stack);
}
// Handle Zod validation errors
if (err instanceof ZodError) {
return res.status(400).json({
error: 'Validation Error',
details: err.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
});
}
// Handle Prisma errors
if (err instanceof Prisma.PrismaClientKnownRequestError) {
// P2002: Unique constraint violation
if (err.code === 'P2002') {
return res.status(409).json({
error: 'Duplicate entry',
field: err.meta?.target,
});
}
// P2025: Record not found
if (err.code === 'P2025') {
return res.status(404).json({
error: 'Record not found',
});
}
}
// Handle Prisma validation errors
if (err instanceof Prisma.PrismaClientValidationError) {
return res.status(400).json({
error: 'Invalid data provided',
});
}
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
export const asyncHandler =
(fn: Function) =>
(req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

155
src/routes/ai.ts Normal file
View File

@ -0,0 +1,155 @@
import { Router, Request, Response } from 'express';
import { z } from 'zod';
import prisma from '../lib/prisma';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import { asyncHandler } from '../middleware/errorHandler';
const router = Router();
const parseMessageSchema = z.object({
message: z.string().min(1, 'Message is required').max(1000),
});
// All routes require authentication
router.use(authenticateToken);
// POST /api/ai/parse - Parse natural language and create event
router.post(
'/parse',
asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const { message } = parseMessageSchema.parse(req.body);
// Call DeepSeek API
const aiResponse = await callDeepSeek(message);
// Save conversation
const conversation = await prisma.aICoachConversation.create({
data: {
user_id: req.user!.userId,
message,
response: aiResponse.response,
parsed_data: aiResponse.parsed,
},
});
res.json({
parsed: aiResponse.parsed,
response: aiResponse.response,
conversation_id: conversation.id,
});
})
);
// DeepSeek API call helper
async function callDeepSeek(message: string): Promise<{
parsed: any;
response: string;
}> {
const apiKey = process.env.DEEPSEEK_API_KEY;
const apiUrl = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/chat/completions';
if (!apiKey || apiKey === 'sk-xxx') {
// Mock response for development
return mockParseResponse(message);
}
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'deepseek-chat',
messages: [
{
role: 'system',
content: `You are a helpful assistant that helps users create events (anniversaries or reminders) from natural language.
Parse the user's message and respond with a JSON object in the following format:
{
"parsed": {
"type": "anniversary" | "reminder",
"title": "event title",
"date": "ISO date string",
"is_lunar": true | false,
"repeat_type": "yearly" | "monthly" | "none"
},
"response": "A friendly confirmation message"
}`
},
{
role: 'user',
content: message
}
],
temperature: 0.3,
}),
});
if (!response.ok) {
throw new Error(`DeepSeek API error: ${response.statusText}`);
}
const data = await response.json();
const content = data.choices[0]?.message?.content || '';
// Extract JSON from response
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
return {
parsed: parsed.parsed || parsed,
response: parsed.response || content,
};
}
return mockParseResponse(message);
} catch (error) {
console.error('DeepSeek API error:', error);
return mockParseResponse(message);
}
}
// Mock response for development without API key
function mockParseResponse(message: string): { parsed: any; response: string } {
// Simple mock parser for testing
const lowerMessage = message.toLowerCase();
// Detect if it's a reminder (contains "提醒", "提醒我", etc.)
const isReminder = lowerMessage.includes('提醒') || lowerMessage.includes('提醒我');
// Detect lunar date
const isLunar = lowerMessage.includes('农历');
// Detect repeat type
let repeatType = 'none';
if (lowerMessage.includes('每年') || lowerMessage.includes(' yearly') || lowerMessage.includes('周年')) {
repeatType = 'yearly';
} else if (lowerMessage.includes('每月') || lowerMessage.includes(' monthly')) {
repeatType = 'monthly';
}
// Extract title (simple heuristic - first phrase before date)
const title = message.split(/[\s\d\u4e00-\u9fa5]+/)[0] || message.substring(0, 50);
// Mock date (tomorrow)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return {
parsed: {
type: isReminder ? 'reminder' : 'anniversary',
title: title.trim(),
date: tomorrow.toISOString(),
is_lunar: isLunar,
repeat_type: repeatType,
},
response: `我帮你记的是:
**${title.trim()}**
📅 ${tomorrow.toLocaleDateString('zh-CN')}
🔄 ${repeatType === 'yearly' ? '每年重复' : repeatType === 'monthly' ? '每月重复' : '不重复'}`
};
}
export default router;

209
src/routes/auth.ts Normal file
View File

@ -0,0 +1,209 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcrypt';
import rateLimit from 'express-rate-limit';
import { z } from 'zod';
import prisma from '../lib/prisma';
import {
authenticateToken,
AuthenticatedRequest,
generateAccessToken,
generateRefreshToken,
verifyRefreshToken,
} from '../middleware/auth';
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,
});
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,}$/;
// Validation schemas
const registerSchema = z.object({
email: z.string().email('Invalid email format'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/\d/, 'Password must contain at least one number'),
nickname: z.string().optional(),
});
const loginSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(1, 'Password is required'),
});
const refreshSchema = z.object({
refreshToken: z.string().min(1, 'Refresh token is required'),
});
// 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 },
});
if (existingUser) {
return res.status(409).json({ error: 'Email already registered' });
}
// Hash password with bcrypt (work factor 12)
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,
},
});
// 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,
},
token,
refreshToken,
});
})
);
// 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 },
});
if (!user) {
// Use generic error message to prevent user enumeration
return res.status(401).json({ error: 'Invalid email or password' });
}
// Verify password with constant-time comparison
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid email or password' });
}
// Generate tokens
const userPayload = { userId: user.id, email: user.email };
const token = generateAccessToken(userPayload);
const refreshToken = generateRefreshToken(userPayload);
res.json({
user: {
id: user.id,
email: user.email,
nickname: user.nickname,
created_at: user.created_at,
},
token,
refreshToken,
});
})
);
// POST /api/auth/logout - Logout user
router.post(
'/logout',
authenticateToken,
asyncHandler(async (_req: Request, res: Response) => {
// In a stateless JWT auth, logout is handled client-side by removing the token
// For production, consider implementing a token blacklist
res.json({ success: true, message: 'Logged out successfully' });
})
);
// GET /api/auth/me - Get current user
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,
},
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user });
})
);
// POST /api/auth/refresh - Refresh access token
router.post(
'/refresh',
asyncHandler(async (req: Request, res: Response) => {
const { refreshToken } = refreshSchema.parse(req.body);
// Verify refresh token
const payload = verifyRefreshToken(refreshToken);
if (!payload) {
return res.status(401).json({ error: 'Invalid or expired refresh token' });
}
// Generate new access token
const newToken = generateAccessToken(payload);
res.json({ token: newToken });
})
);
export default router;

149
src/routes/events.ts Normal file
View File

@ -0,0 +1,149 @@
import { Router, Request, Response } from 'express';
import { z } from 'zod';
import prisma from '../lib/prisma';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import { asyncHandler } from '../middleware/errorHandler';
const router = Router();
// Validation schemas
const createEventSchema = z.object({
type: z.enum(['anniversary', 'reminder']),
title: z.string().min(1, 'Title is required').max(200),
content: z.string().optional(),
date: z.string().datetime(), // ISO datetime string
is_lunar: z.boolean().default(false),
repeat_type: z.enum(['yearly', 'monthly', 'none']).default('none'),
is_holiday: z.boolean().default(false),
});
const updateEventSchema = createEventSchema.partial();
// All routes require authentication
router.use(authenticateToken);
// 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,
};
if (type === 'anniversary' || type === 'reminder') {
where.type = type;
}
const events = await prisma.event.findMany({
where,
orderBy: { date: 'asc' },
});
res.json(events);
})
);
// GET /api/events/:id - Get single event
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,
},
});
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
res.json(event);
})
);
// POST /api/events - Create new event
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,
},
});
res.status(201).json(event);
})
);
// PUT /api/events/:id - Update event
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,
},
});
if (!existing) {
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 }),
},
});
res.json(event);
})
);
// DELETE /api/events/:id - Delete event
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,
},
});
if (!existing) {
return res.status(404).json({ error: 'Event not found' });
}
await prisma.event.delete({
where: { id: req.params.id },
});
res.json({ success: true });
})
);
export default router;

50
src/routes/notes.ts Normal file
View File

@ -0,0 +1,50 @@
import { Router, Request, Response } from 'express';
import { z } from 'zod';
import prisma from '../lib/prisma';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import { asyncHandler } from '../middleware/errorHandler';
const router = Router();
const updateNoteSchema = z.object({
content: z.string(),
});
// All routes require authentication
router.use(authenticateToken);
// 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 },
});
res.json(note);
})
);
// PUT /api/notes - Create or update note
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,
},
});
res.json(note);
})
);
export default router;

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}