Refine auth, API proxy, and server routing

This commit is contained in:
Developer
2026-05-18 15:16:46 +08:00
parent 4ad1766f10
commit d10dd53884
9 changed files with 88 additions and 17 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ module.exports = {
}, },
settings: { settings: {
react: { react: {
version: 'detect', version: '18.2',
}, },
}, },
rules: { rules: {
+4
View File
@@ -4,6 +4,10 @@ node_modules/
# Database # Database
*.db *.db
*.sqlite *.sqlite
*.db-shm
*.db-wal
*.sqlite-shm
*.sqlite-wal
# Logs # Logs
*.log *.log
+49 -5
View File
@@ -1,7 +1,49 @@
const API_BASE = ''; // 使用相对路径 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() { function getAuthHeaders() {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; const token = getValidToken();
return token ? { Authorization: `Bearer ${token}` } : {}; return token ? { Authorization: `Bearer ${token}` } : {};
} }
@@ -24,6 +66,9 @@ async function apiCall(url, options = {}) {
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
if (res.status === 401 && !url.startsWith('/api/auth/')) {
redirectToLogin();
}
throw new Error(data.error || '请求失败'); throw new Error(data.error || '请求失败');
} }
@@ -60,8 +105,7 @@ export function logout() {
} }
export function isLoggedIn() { export function isLoggedIn() {
if (typeof window === 'undefined') return false; return !!getValidToken();
return !!localStorage.getItem('token');
} }
// 账目 // 账目
@@ -106,7 +150,7 @@ export async function deleteRecordsByCategory(category) {
// 搜索 // 搜索
export async function searchRecords(keyword) { 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) { export async function importData(data) {
return apiCall('/api/import', { return apiCall('/api/export', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
+3
View File
@@ -1,5 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
devIndicators: {
buildActivity: false,
},
async rewrites() { async rewrites() {
return [ return [
{ {
+12 -2
View File
@@ -1,7 +1,17 @@
export default async function handler(req, res) { 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, { const response = await fetch(backendUrl, {
method: req.method, method: req.method,
+1 -1
View File
@@ -12,7 +12,7 @@ import { formatMoney, parseDateTime, getDatePart } from '../lib/utils';
export default function Search() { export default function Search() {
const router = useRouter(); const router = useRouter();
const { showToast } = useToast(); const { showToast } = useToast();
const confirm = useConfirm(); const { confirm } = useConfirm();
const { categories, error: categoriesError } = useCategories(); const { categories, error: categoriesError } = useCategories();
// 显示分类加载错误 // 显示分类加载错误
+4 -3
View File
@@ -31,9 +31,6 @@ app.use(cors({
})); }));
app.use(express.json()); 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) => { app.use((req, res, next) => {
@@ -55,6 +52,10 @@ app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() }); 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 静态文件 // 前端路由 - Next.js 静态文件
app.get('/_next/static/:path(*)', (req, res) => { app.get('/_next/static/:path(*)', (req, res) => {
const filePath = path.join(__dirname, '../frontend/.next/static', req.params.path); const filePath = path.join(__dirname, '../frontend/.next/static', req.params.path);
+10 -4
View File
@@ -1,7 +1,13 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { AppError } = require('./errorHandler'); 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) { function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]; const token = req.headers.authorization?.split(' ')[1];
@@ -10,7 +16,7 @@ function authenticate(req, res, next) {
} }
try { try {
const decoded = jwt.verify(token, JWT_SECRET); const decoded = jwt.verify(token, tokenSecret);
req.userId = decoded.userId; req.userId = decoded.userId;
next(); next();
} catch (err) { } catch (err) {
@@ -19,11 +25,11 @@ function authenticate(req, res, next) {
} }
function generateToken(userId) { function generateToken(userId) {
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' }); return jwt.sign({ userId }, tokenSecret, { expiresIn: '7d' });
} }
module.exports = { module.exports = {
authenticate, authenticate,
generateToken, generateToken,
JWT_SECRET, JWT_SECRET: tokenSecret,
}; };
+4 -1
View File
@@ -48,7 +48,10 @@ router.post('/', authenticate, asyncHandler(async (req, res) => {
const result = stmt.run(req.userId, type, amount, category.trim(), date, normalizeNote(note)); const result = stmt.run(req.userId, type, amount, category.trim(), date, normalizeNote(note));
console.log(`Record created: user=${req.userId}, id=${result.lastInsertRowid}`); 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);
})); }));
// 更新账目 // 更新账目