feat: add utils, error handler and auth middleware
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { AppError } = require('./errorHandler');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'accountbook_secret_key_2024';
|
||||
|
||||
function authenticate(req, res, next) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
throw new AppError('请先登录', 401, 'UNAUTHORIZED');
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
req.userId = decoded.userId;
|
||||
next();
|
||||
} catch (err) {
|
||||
throw new AppError('登录已过期', 401, 'TOKEN_EXPIRED');
|
||||
}
|
||||
}
|
||||
|
||||
function generateToken(userId) {
|
||||
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
generateToken,
|
||||
JWT_SECRET,
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
this.isOperational = true;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
function errorHandler(err, req, res, next) {
|
||||
console.error({
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
userId: req.userId,
|
||||
});
|
||||
|
||||
if (err instanceof AppError && err.isOperational) {
|
||||
return res.status(err.statusCode).json({
|
||||
error: err.message,
|
||||
code: err.code,
|
||||
});
|
||||
}
|
||||
|
||||
// 数据库唯一性冲突
|
||||
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
return res.status(400).json({
|
||||
error: '数据已存在',
|
||||
code: 'DUPLICATE_ENTRY',
|
||||
});
|
||||
}
|
||||
|
||||
// 其他数据库错误
|
||||
if (err.code && err.code.startsWith('SQLITE_')) {
|
||||
return res.status(500).json({
|
||||
error: '数据库错误',
|
||||
code: 'DATABASE_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
// 默认错误响应
|
||||
res.status(500).json({
|
||||
error: process.env.NODE_ENV === 'production'
|
||||
? '服务器内部错误'
|
||||
: err.message,
|
||||
code: 'INTERNAL_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
errorHandler,
|
||||
asyncHandler,
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
const VALID_RECORD_TYPES = ['income', 'expense'];
|
||||
const VALID_STATS_PERIODS = ['week', 'month', 'year'];
|
||||
const VALID_STATS_TYPES = ['income', 'expense', 'all'];
|
||||
const MONTH_PATTERN = /^\d{4}-\d{2}$/;
|
||||
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2})?$/;
|
||||
|
||||
function isNonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isValidMonth(value) {
|
||||
return typeof value === 'string' && MONTH_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function isValidDateTime(value) {
|
||||
return typeof value === 'string' && DATE_TIME_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function normalizeNote(value) {
|
||||
return value === undefined || value === null ? '' : value;
|
||||
}
|
||||
|
||||
function validateRecordPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return '请求数据格式错误';
|
||||
}
|
||||
|
||||
const { type, amount, category, date, note } = payload;
|
||||
|
||||
if (!VALID_RECORD_TYPES.includes(type)) {
|
||||
return '账目类型错误';
|
||||
}
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return '金额需大于0';
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(category)) {
|
||||
return '请填写分类';
|
||||
}
|
||||
|
||||
if (!isValidDateTime(date)) {
|
||||
return '日期格式错误';
|
||||
}
|
||||
|
||||
if (note !== undefined && note !== null && typeof note !== 'string') {
|
||||
return '备注格式错误';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateRecurringPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return '请求数据格式错误';
|
||||
}
|
||||
|
||||
const { type, amount, category, day, note, is_active } = payload;
|
||||
|
||||
if (!VALID_RECORD_TYPES.includes(type)) {
|
||||
return '账单类型错误';
|
||||
}
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return '金额需大于0';
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(category)) {
|
||||
return '请填写分类';
|
||||
}
|
||||
|
||||
if (!Number.isInteger(day) || day < 1 || day > 28) {
|
||||
return '日期需在1到28之间';
|
||||
}
|
||||
|
||||
if (note !== undefined && note !== null && typeof note !== 'string') {
|
||||
return '备注格式错误';
|
||||
}
|
||||
|
||||
if (is_active !== undefined && ![0, 1, true, false].includes(is_active)) {
|
||||
return '启用状态错误';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCategoryPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return '请求数据格式错误';
|
||||
}
|
||||
|
||||
const { type, name, icon } = payload;
|
||||
|
||||
if (!VALID_RECORD_TYPES.includes(type)) {
|
||||
return '分类类型错误';
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(name)) {
|
||||
return '请填写分类名称';
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(icon)) {
|
||||
return '请选择分类图标';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateImportPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return '导入数据格式错误';
|
||||
}
|
||||
|
||||
const { records, recurring, customCategories } = payload;
|
||||
|
||||
if (records !== undefined) {
|
||||
if (!Array.isArray(records)) {
|
||||
return 'records 必须是数组';
|
||||
}
|
||||
|
||||
for (const record of records) {
|
||||
const error = validateRecordPayload(record);
|
||||
if (error) {
|
||||
return `records 数据错误: ${error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (recurring !== undefined) {
|
||||
if (!Array.isArray(recurring)) {
|
||||
return 'recurring 必须是数组';
|
||||
}
|
||||
|
||||
for (const bill of recurring) {
|
||||
const error = validateRecurringPayload(bill);
|
||||
if (error) {
|
||||
return `recurring 数据错误: ${error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (customCategories !== undefined) {
|
||||
if (!Array.isArray(customCategories)) {
|
||||
return 'customCategories 必须是数组';
|
||||
}
|
||||
|
||||
for (const category of customCategories) {
|
||||
const error = validateCategoryPayload(category);
|
||||
if (error) {
|
||||
return `customCategories 数据错误: ${error}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isNonEmptyString,
|
||||
isValidMonth,
|
||||
isValidDateTime,
|
||||
normalizeNote,
|
||||
validateRecordPayload,
|
||||
validateRecurringPayload,
|
||||
validateCategoryPayload,
|
||||
validateImportPayload,
|
||||
VALID_RECORD_TYPES,
|
||||
VALID_STATS_PERIODS,
|
||||
VALID_STATS_TYPES,
|
||||
};
|
||||
Reference in New Issue
Block a user