feat(date): enhance date formatting and timezone handling
- Update formatDateTime function to handle timezone-aware date strings directly. - Introduce utcToBeijingISOString function for consistent Beijing time formatting in ISO string. - Modify BidsService to utilize the new utcToBeijingISOString for date conversions. - Add unit tests for AuthGuard to validate API key authentication and access control. - Create end-to-end tests for API key handling in various scenarios. - Update .gitignore to exclude 'qingyun' directory. - Add environment configuration for production settings in .env.production. - Include Tailwind CSS setup in the uni-app version for styling.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
setStartOfDay,
|
||||
setEndOfDay,
|
||||
utcToBeijing,
|
||||
utcToBeijingISOString,
|
||||
} from '../../common/utils/timezone.util';
|
||||
|
||||
interface FindAllQuery {
|
||||
@@ -179,12 +180,13 @@ export class BidsService {
|
||||
await this.crawlInfoRepository.query<CrawlInfoAddRawResult[]>(query);
|
||||
|
||||
return results.map((item) => {
|
||||
// 将UTC时间转换为北京时间显示
|
||||
// 将UTC时间转换为北京时间的ISO字符串格式
|
||||
// 这样前端接收到的时间字符串已经是正确的北京时间,不需要再次转换
|
||||
const latestUpdateBeijing = item.latestUpdate
|
||||
? utcToBeijing(new Date(item.latestUpdate))
|
||||
? utcToBeijingISOString(new Date(item.latestUpdate))
|
||||
: null;
|
||||
const latestPublishDateBeijing = item.latestPublishDate
|
||||
? utcToBeijing(new Date(item.latestPublishDate))
|
||||
? utcToBeijingISOString(new Date(item.latestPublishDate))
|
||||
: null;
|
||||
|
||||
return {
|
||||
|
||||
329
src/common/auth/auth.guard.spec.ts
Normal file
329
src/common/auth/auth.guard.spec.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
|
||||
describe('AuthGuard', () => {
|
||||
let guard: AuthGuard;
|
||||
let configService: ConfigService;
|
||||
let mockExecutionContext: ExecutionContext;
|
||||
let mockRequest: Partial<Request>;
|
||||
|
||||
const createMockExecutionContext = (request: Partial<Request>): ExecutionContext => {
|
||||
return {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => request as Request,
|
||||
getResponse: () => ({}),
|
||||
getNext: () => ({}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AuthGuard,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<AuthGuard>(AuthGuard);
|
||||
configService = module.get<ConfigService>(ConfigService);
|
||||
});
|
||||
|
||||
describe('本地IP访问', () => {
|
||||
it('应该允许 127.0.0.1 访问', () => {
|
||||
mockRequest = {
|
||||
ip: '127.0.0.1',
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该允许 ::1 (IPv6本地地址) 访问', () => {
|
||||
mockRequest = {
|
||||
ip: '::1',
|
||||
socket: { remoteAddress: '::1' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该允许 localhost 访问', () => {
|
||||
mockRequest = {
|
||||
ip: 'localhost',
|
||||
socket: { remoteAddress: 'localhost' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该允许通过 X-Forwarded-For 传递的本地IP访问', () => {
|
||||
mockRequest = {
|
||||
ip: '192.168.1.1',
|
||||
socket: { remoteAddress: '192.168.1.1' },
|
||||
headers: {
|
||||
'x-forwarded-for': '127.0.0.1',
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('公网IP访问 - 已配置API_KEY', () => {
|
||||
const validApiKey = 'test-api-key-12345';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(configService, 'get').mockReturnValue(validApiKey);
|
||||
});
|
||||
|
||||
it('应该允许提供正确API Key的公网访问', () => {
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {
|
||||
'x-api-key': validApiKey,
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝未提供API Key的公网访问', () => {
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
'Invalid or missing API Key',
|
||||
);
|
||||
});
|
||||
|
||||
it('应该拒绝提供错误API Key的公网访问', () => {
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {
|
||||
'x-api-key': 'wrong-api-key',
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
'Invalid or missing API Key',
|
||||
);
|
||||
});
|
||||
|
||||
it('应该拒绝提供空字符串API Key的公网访问', () => {
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {
|
||||
'x-api-key': '',
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确处理 X-Forwarded-For 中的公网IP', () => {
|
||||
mockRequest = {
|
||||
ip: '127.0.0.1',
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: {
|
||||
'x-forwarded-for': '8.8.8.8',
|
||||
'x-api-key': validApiKey,
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确处理多个IP的 X-Forwarded-For 头(取第一个)', () => {
|
||||
mockRequest = {
|
||||
ip: '127.0.0.1',
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: {
|
||||
'x-forwarded-for': '8.8.8.8, 192.168.1.1',
|
||||
'x-api-key': validApiKey,
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('公网IP访问 - 未配置API_KEY', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(configService, 'get').mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it('应该允许所有公网访问(开发环境)', () => {
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该允许未提供API Key的公网访问', () => {
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('内网IP访问', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(configService, 'get').mockReturnValue('test-api-key');
|
||||
});
|
||||
|
||||
it('应该要求内网IP提供API Key', () => {
|
||||
mockRequest = {
|
||||
ip: '192.168.1.100',
|
||||
socket: { remoteAddress: '192.168.1.100' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该允许提供正确API Key的内网访问', () => {
|
||||
mockRequest = {
|
||||
ip: '192.168.1.100',
|
||||
socket: { remoteAddress: '192.168.1.100' },
|
||||
headers: {
|
||||
'x-api-key': 'test-api-key',
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
const result = guard.canActivate(mockExecutionContext);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('应该要求 10.x.x.x 网段提供API Key', () => {
|
||||
mockRequest = {
|
||||
ip: '10.0.0.1',
|
||||
socket: { remoteAddress: '10.0.0.1' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该要求 172.16-31.x.x 网段提供API Key', () => {
|
||||
mockRequest = {
|
||||
ip: '172.16.0.1',
|
||||
socket: { remoteAddress: '172.16.0.1' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(configService, 'get').mockReturnValue('test-api-key');
|
||||
});
|
||||
|
||||
it('应该处理 unknown IP 地址', () => {
|
||||
mockRequest = {
|
||||
ip: 'unknown',
|
||||
socket: { remoteAddress: 'unknown' },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该处理缺少 IP 信息的请求', () => {
|
||||
mockRequest = {
|
||||
ip: undefined,
|
||||
socket: { remoteAddress: undefined },
|
||||
headers: {},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该处理 API Key 大小写敏感', () => {
|
||||
jest.spyOn(configService, 'get').mockReturnValue('Test-API-Key');
|
||||
mockRequest = {
|
||||
ip: '8.8.8.8',
|
||||
socket: { remoteAddress: '8.8.8.8' },
|
||||
headers: {
|
||||
'x-api-key': 'test-api-key', // 小写,应该被拒绝
|
||||
},
|
||||
};
|
||||
mockExecutionContext = createMockExecutionContext(mockRequest);
|
||||
|
||||
expect(() => guard.canActivate(mockExecutionContext)).toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -116,3 +116,28 @@ export function parseDateString(dateStr: string): Date {
|
||||
const date = new Date(dateStr);
|
||||
return convertToTimezone(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将UTC时间转换为北京时间的ISO字符串格式
|
||||
* 用于API返回,确保前端接收到的时间字符串已经是北京时间
|
||||
* @param date UTC时间的Date对象
|
||||
* @returns 北京时间的ISO字符串 (格式: YYYY-MM-DDTHH:mm:ss+08:00)
|
||||
*/
|
||||
export function utcToBeijingISOString(date: Date): string {
|
||||
// 获取UTC时间戳(毫秒)
|
||||
const utcTimestamp = date.getTime();
|
||||
// 计算北京时间戳(UTC + 8小时)
|
||||
const beijingTimestamp = utcTimestamp + TIMEZONE_OFFSET;
|
||||
// 创建UTC Date对象来格式化(避免本地时区影响)
|
||||
const beijingDate = new Date(beijingTimestamp);
|
||||
|
||||
// 使用UTC方法获取时间组件,确保不受本地时区影响
|
||||
const year = beijingDate.getUTCFullYear();
|
||||
const month = String(beijingDate.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(beijingDate.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(beijingDate.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(beijingDate.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(beijingDate.getUTCSeconds()).padStart(2, '0');
|
||||
const milliseconds = String(beijingDate.getUTCMilliseconds()).padStart(3, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}+08:00`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user