feat: 后端用户认证和权限过滤
- 新增 auth 模块:jumpToken 代理交换、用户信息获取、JWT 签发 - 三级权限:full(所有权限/数智中心/BI-Leader)、department(BI-Leader-Dep)、personal - 添加 managerId 到车辆数据模型,支持个人级别按 userId 精确过滤 - auth 中间件保护所有 /api/* 端点(跳过 /api/health 和 /api/auth/*) - 所有路由集成 filterByPermission 权限过滤 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,7 @@ function mergeVehicles(
|
||||
customer: info?.customer || null,
|
||||
department: info?.department || null,
|
||||
manager: info?.manager || null,
|
||||
managerId: info?.manager_id || null,
|
||||
rentStatus: info?.rent_status || null,
|
||||
entity: info?.entity || null,
|
||||
project: info?.project || null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { getCache, queryDateMileage, buildDateFilters } from './cache.js';
|
||||
import { filterByPermission } from '../../auth/permissions.js';
|
||||
import type { AuthUser } from '../../auth/types.js';
|
||||
import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js';
|
||||
|
||||
const app = new Hono();
|
||||
@@ -86,6 +88,13 @@ app.get('/', async (c) => {
|
||||
filters = cache.filters;
|
||||
}
|
||||
|
||||
// 权限过滤
|
||||
const user = (c as any).get('user') as AuthUser | undefined;
|
||||
if (user) {
|
||||
allVehicles = filterByPermission(allVehicles, user);
|
||||
filters = buildDateFilters(allVehicles); // 重算筛选选项以匹配权限范围
|
||||
}
|
||||
|
||||
const filtered = applyFilters(allVehicles, filterParams);
|
||||
|
||||
const stats = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import pool from '../../db.js';
|
||||
import mileagePool from '../../mileage-db.js';
|
||||
import { getCache } from './cache.js';
|
||||
import { fetchVehicleInfoByPlates } from './vehicle-info.js';
|
||||
import { filterByPermission } from '../../auth/permissions.js';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -170,7 +171,9 @@ app.get('/:id/vehicles', async (c) => {
|
||||
};
|
||||
});
|
||||
|
||||
return c.json(result);
|
||||
const user = (c as any).get('user') as import('../../auth/types.js').AuthUser | undefined;
|
||||
const filtered = user ? filterByPermission(result, user) : result;
|
||||
return c.json(filtered);
|
||||
} catch (e: unknown) {
|
||||
console.error('target vehicles error:', e);
|
||||
return c.json([], 500);
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface CachedVehicle {
|
||||
customer: string | null;
|
||||
department: string | null;
|
||||
manager: string | null;
|
||||
managerId: string | null;
|
||||
rentStatus: string | null;
|
||||
entity: string | null;
|
||||
project: string | null;
|
||||
@@ -68,6 +69,7 @@ export interface VehicleInfoRow {
|
||||
customer: string | null;
|
||||
department: string | null;
|
||||
manager: string | null;
|
||||
manager_id: string | null;
|
||||
rent_status: string | null;
|
||||
entity: string | null;
|
||||
project: string | null;
|
||||
|
||||
@@ -7,6 +7,7 @@ export const VEHICLE_INFO_SQL = `SELECT
|
||||
cus.customer_name AS customer,
|
||||
dep.dep_name AS department,
|
||||
u.user_name AS manager,
|
||||
CAST(c.bd AS CHAR) AS manager_id,
|
||||
dic_status.dic_name AS rent_status,
|
||||
org_truck.org_name AS entity,
|
||||
c.project_name AS project
|
||||
|
||||
@@ -10,6 +10,9 @@ import type {
|
||||
BatchGroup,
|
||||
InventoryTypeSummary,
|
||||
} from '../types.js';
|
||||
import { filterByPermission } from '../auth/permissions.js';
|
||||
import type { AuthUser } from '../auth/types.js';
|
||||
import type { Context } from 'hono';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -39,7 +42,8 @@ const MAIN_SQL = `SELECT
|
||||
dep.dep_name AS 合同归属部门,
|
||||
org_truck.org_name AS 主体,
|
||||
c.project_name AS 项目名称,
|
||||
u.user_name AS 客户经理
|
||||
u.user_name AS 客户经理,
|
||||
CAST(c.bd AS CHAR) AS 经理ID
|
||||
FROM tab_truck truck
|
||||
LEFT JOIN tab_truck_remote_sync_realtime_info info
|
||||
ON info.id = truck.id
|
||||
@@ -285,6 +289,7 @@ function transformRow(row: VehicleRow): Vehicle {
|
||||
subjectOrg: row.主体,
|
||||
projectName: row.项目名称,
|
||||
customerManager: row.客户经理,
|
||||
managerId: row.经理ID || null,
|
||||
brandLabel: row.车辆品牌Label,
|
||||
};
|
||||
}
|
||||
@@ -305,6 +310,12 @@ async function getVehicles(): Promise<Vehicle[]> {
|
||||
return cachedVehicles;
|
||||
}
|
||||
|
||||
async function getVehiclesForUser(c: Context): Promise<Vehicle[]> {
|
||||
const all = await getVehicles();
|
||||
const user = c.get('user') as AuthUser | undefined;
|
||||
return user ? filterByPermission(all, user) : all;
|
||||
}
|
||||
|
||||
function getRegionCounts(vehicles: Vehicle[], regions: readonly string[]): Record<string, number> {
|
||||
return regions.reduce((acc, reg) => {
|
||||
acc[reg] = vehicles.filter((v) => v.location === reg).length;
|
||||
@@ -566,7 +577,7 @@ app.get('/by-type', async (c) => {
|
||||
|
||||
// GET /api/vehicles/by-batch
|
||||
app.get('/by-batch', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const batches = Array.from(new Set(vehicles.map((v) => v.contractNo || '未知')))
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
@@ -595,7 +606,7 @@ app.get('/by-batch', async (c) => {
|
||||
|
||||
// GET /api/vehicles/inventory-analysis
|
||||
app.get('/inventory-analysis', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
|
||||
const typeFilters = [
|
||||
{ name: '4.5T普货', filter: (v: Vehicle) => v.type === '4.5T' && !v.model.includes('冷链') },
|
||||
@@ -649,7 +660,7 @@ app.get('/inventory-analysis', async (c) => {
|
||||
|
||||
// GET /api/vehicles/dept-stats — department & manager breakdown with mileage/attendance
|
||||
app.get('/dept-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const withManager = vehicles.filter((v) => v.status === 'Operating');
|
||||
|
||||
// Query realtime day_mileage from tab_truck_remote_sync_realtime_info
|
||||
@@ -723,7 +734,7 @@ app.get('/dept-stats', async (c) => {
|
||||
|
||||
// GET /api/vehicles/region-stats — macro-region with city drill-down
|
||||
app.get('/region-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const { customer, city: filterCity, region: filterRegion } = c.req.query();
|
||||
let operating = vehicles.filter((v) => v.status === 'Operating' || v.status === 'Pending');
|
||||
if (customer) operating = operating.filter((v) => v.customerName === customer);
|
||||
@@ -799,7 +810,7 @@ app.get('/region-stats', async (c) => {
|
||||
|
||||
// GET /api/vehicles/customer-stats — per-customer breakdown for operating vehicles
|
||||
app.get('/customer-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const operating = vehicles.filter((v) => v.status === 'Operating');
|
||||
|
||||
const custMap = new Map<string, Vehicle[]>();
|
||||
@@ -839,7 +850,7 @@ const VEHICLE_TYPE_FILTERS: Record<string, (v: Vehicle) => boolean> = {
|
||||
|
||||
// GET /api/vehicles/list — flat list with optional filters
|
||||
app.get('/list', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const { batch, model, location, status, category, vehicleType, manager, customer, isColdChain, isTrailer, department, attendance } = c.req.query();
|
||||
|
||||
let filtered = vehicles;
|
||||
@@ -935,7 +946,7 @@ app.get('/list', async (c) => {
|
||||
|
||||
// GET /api/vehicles/inventory-stats — grouped inventory stats for the inventory statistics section
|
||||
app.get('/inventory-stats', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const inventory = vehicles.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal');
|
||||
|
||||
const TYPE_NAME_MAP: Record<string, string> = {
|
||||
@@ -997,7 +1008,7 @@ app.get('/weekly-detail', async (c) => {
|
||||
app.get('/refresh', async (c) => {
|
||||
lastFetchTime = 0;
|
||||
weeklyStatsLastFetch = 0;
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
return c.json({ message: 'Cache refreshed', count: vehicles.length });
|
||||
});
|
||||
|
||||
@@ -1032,7 +1043,7 @@ app.get('/debug', async (c) => {
|
||||
|
||||
// GET /api/vehicles/region-chart — aggregated chart data with top N + "其他"
|
||||
app.get('/region-chart', async (c) => {
|
||||
const vehicles = await getVehicles();
|
||||
const vehicles = await getVehiclesForUser(c);
|
||||
const operating = vehicles.filter((v) => v.status === 'Operating');
|
||||
const groupBy = c.req.query('groupBy') || 'region'; // 'region' | 'province'
|
||||
const source = c.req.query('source') || 'realtime'; // 'realtime' | 'vehicle'
|
||||
|
||||
Reference in New Issue
Block a user