Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d10dd53884 | ||
|
|
4ad1766f10 |
+1
-1
@@ -20,7 +20,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: 'detect',
|
version: '18.2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ node_modules/
|
|||||||
# Database
|
# Database
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'accountbook',
|
||||||
|
cwd: '/opt/accountbook',
|
||||||
|
script: 'npm',
|
||||||
|
args: 'start',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: '3500',
|
||||||
|
},
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_restarts: 10,
|
||||||
|
min_uptime: '10s',
|
||||||
|
out_file: '/opt/accountbook/logs/accountbook.out.log',
|
||||||
|
error_file: '/opt/accountbook/logs/accountbook.err.log',
|
||||||
|
merge_logs: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
+49
-5
@@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3500",
|
"dev": "next dev -H 0.0.0.0 -p 3500",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start -p 3500"
|
"start": "next start -H 0.0.0.0 -p 3500"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 更新账目
|
// 更新账目
|
||||||
|
|||||||
Reference in New Issue
Block a user