diff --git a/.eslintrc.js b/.eslintrc.js index e8d64b9..2051a1c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,7 @@ module.exports = { }, settings: { react: { - version: 'detect', + version: '18.2', }, }, rules: { diff --git a/.gitignore b/.gitignore index dabc948..72197d4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ node_modules/ # Database *.db *.sqlite +*.db-shm +*.db-wal +*.sqlite-shm +*.sqlite-wal # Logs *.log diff --git a/frontend/lib/api.js b/frontend/lib/api.js index 3199b14..f136bee 100644 --- a/frontend/lib/api.js +++ b/frontend/lib/api.js @@ -1,7 +1,49 @@ const API_BASE = ''; // 使用相对路径 +function readTokenPayload(token) { + try { + const payload = token.split('.')[1]; + if (!payload) return null; + + const normalizedPayload = payload.replace(/-/g, '+').replace(/_/g, '/'); + const paddedPayload = normalizedPayload.padEnd( + normalizedPayload.length + ((4 - (normalizedPayload.length % 4)) % 4), + '=' + ); + + return JSON.parse(atob(paddedPayload)); + } catch (err) { + return null; + } +} + +function getValidToken() { + if (typeof window === 'undefined') return null; + + const token = localStorage.getItem('token'); + if (!token) return null; + + const payload = readTokenPayload(token); + if (!payload?.exp || payload.exp * 1000 <= Date.now()) { + localStorage.removeItem('token'); + return null; + } + + return token; +} + +function redirectToLogin() { + if (typeof window === 'undefined') return; + + localStorage.removeItem('token'); + + if (window.location.pathname !== '/login') { + window.location.replace('/login'); + } +} + function getAuthHeaders() { - const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; + const token = getValidToken(); return token ? { Authorization: `Bearer ${token}` } : {}; } @@ -24,6 +66,9 @@ async function apiCall(url, options = {}) { const data = await res.json(); if (!res.ok) { + if (res.status === 401 && !url.startsWith('/api/auth/')) { + redirectToLogin(); + } throw new Error(data.error || '请求失败'); } @@ -60,8 +105,7 @@ export function logout() { } export function isLoggedIn() { - if (typeof window === 'undefined') return false; - return !!localStorage.getItem('token'); + return !!getValidToken(); } // 账目 @@ -106,7 +150,7 @@ export async function deleteRecordsByCategory(category) { // 搜索 export async function searchRecords(keyword) { - return apiCall(`/api/records/search?q=${encodeURIComponent(keyword)}`); + return apiCall(`/api/search?q=${encodeURIComponent(keyword)}`); } // 统计 @@ -177,7 +221,7 @@ export async function exportData() { } export async function importData(data) { - return apiCall('/api/import', { + return apiCall('/api/export', { method: 'POST', body: JSON.stringify(data), }); diff --git a/frontend/next.config.js b/frontend/next.config.js index 4ef981c..afa4dad 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,5 +1,8 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + devIndicators: { + buildActivity: false, + }, async rewrites() { return [ { diff --git a/frontend/pages/api/[...path].js b/frontend/pages/api/[...path].js index e0510d3..cf731a2 100644 --- a/frontend/pages/api/[...path].js +++ b/frontend/pages/api/[...path].js @@ -1,7 +1,17 @@ export default async function handler(req, res) { - const { 0: path } = req.query; + const path = Array.isArray(req.query.path) + ? req.query.path.join('/') + : req.query.path; - const backendUrl = `http://localhost:3501/${path}`; + if (!path) { + res.status(400).json({ error: 'Missing API path' }); + return; + } + + const query = { ...req.query }; + delete query.path; + const searchParams = new URLSearchParams(query); + const backendUrl = `http://localhost:3501/api/${path}${searchParams.size ? `?${searchParams}` : ''}`; const response = await fetch(backendUrl, { method: req.method, diff --git a/frontend/pages/search.js b/frontend/pages/search.js index cf1c231..1cb09fa 100644 --- a/frontend/pages/search.js +++ b/frontend/pages/search.js @@ -12,7 +12,7 @@ import { formatMoney, parseDateTime, getDatePart } from '../lib/utils'; export default function Search() { const router = useRouter(); const { showToast } = useToast(); - const confirm = useConfirm(); + const { confirm } = useConfirm(); const { categories, error: categoriesError } = useCategories(); // 显示分类加载错误 diff --git a/server/index.js b/server/index.js index a975137..790ac66 100644 --- a/server/index.js +++ b/server/index.js @@ -31,9 +31,6 @@ app.use(cors({ })); app.use(express.json()); -app.use(express.static(path.join(__dirname, '../public'))); -app.use(express.static(path.join(__dirname, '../frontend/.next'))); -app.use(express.static(path.join(__dirname, '../frontend/.next/server/pages'))); // 请求日志 app.use((req, res, next) => { @@ -55,6 +52,10 @@ app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); +app.use(express.static(path.join(__dirname, '../public'))); +app.use(express.static(path.join(__dirname, '../frontend/.next'))); +app.use(express.static(path.join(__dirname, '../frontend/.next/server/pages'))); + // 前端路由 - Next.js 静态文件 app.get('/_next/static/:path(*)', (req, res) => { const filePath = path.join(__dirname, '../frontend/.next/static', req.params.path); diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 35afba5..a06073b 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -1,7 +1,13 @@ const jwt = require('jsonwebtoken'); const { AppError } = require('./errorHandler'); -const JWT_SECRET = process.env.JWT_SECRET || 'accountbook_secret_key_2024'; +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET && process.env.NODE_ENV === 'production') { + throw new Error('JWT_SECRET must be set in production'); +} + +const tokenSecret = JWT_SECRET || 'accountbook_secret_key_2024'; function authenticate(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; @@ -10,7 +16,7 @@ function authenticate(req, res, next) { } try { - const decoded = jwt.verify(token, JWT_SECRET); + const decoded = jwt.verify(token, tokenSecret); req.userId = decoded.userId; next(); } catch (err) { @@ -19,11 +25,11 @@ function authenticate(req, res, next) { } function generateToken(userId) { - return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' }); + return jwt.sign({ userId }, tokenSecret, { expiresIn: '7d' }); } module.exports = { authenticate, generateToken, - JWT_SECRET, + JWT_SECRET: tokenSecret, }; diff --git a/server/routes/records.js b/server/routes/records.js index dd35fb7..d00a802 100644 --- a/server/routes/records.js +++ b/server/routes/records.js @@ -48,7 +48,10 @@ router.post('/', authenticate, asyncHandler(async (req, res) => { const result = stmt.run(req.userId, type, amount, category.trim(), date, normalizeNote(note)); console.log(`Record created: user=${req.userId}, id=${result.lastInsertRowid}`); - res.json({ id: result.lastInsertRowid, message: '添加成功' }); + const createdRecord = db.prepare('SELECT * FROM records WHERE id = ? AND user_id = ?') + .get(result.lastInsertRowid, req.userId); + + res.json(createdRecord); })); // 更新账目