diff --git a/.env b/.env index 62b520f..c91b64a 100644 --- a/.env +++ b/.env @@ -42,4 +42,7 @@ LOG_LEVEL=debug # OpenAI API Key (用于 AI 推荐) ARK_API_KEY=a63d58b6-cf56-434b-8a42-5c781ba0822a -SSH_PASSPHRASE=x \ No newline at end of file +SSH_PASSPHRASE=x + +API_KEY=22c64b60-6e60-433c-991d-f6d658024b9e + diff --git a/.env.example b/.env.example index f04c85d..4af1f09 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,6 @@ PROXY_PORT=6000 LOG_LEVEL=info # OpenAI API Key (用于 AI 推荐) -ARK_API_KEY=your_openai_api_key_here \ No newline at end of file +ARK_API_KEY=your_openai_api_key_here + +API_KEY=your_secure_api_key_here \ No newline at end of file diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 17a31bc..86d4bb3 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,34 +1,52 @@ -import axios from 'axios' +import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; /** * API配置 * 配置axios实例,设置baseURL和请求拦截器 */ const api = axios.create({ - baseURL: 'http://localhost:3000', // 设置后端服务地址 + baseURL: + (import.meta.env.VITE_API_BASE_URL as string) || 'http://localhost:3000', // 设置后端服务地址 timeout: 120000, // 请求超时时间(120秒) -}) +}); // 请求拦截器 api.interceptors.request.use( - (config) => { - // 可以在这里添加认证信息等 - return config + (config: AxiosRequestConfig) => { + // 如果 baseURL 不是 localhost,自动添加 API Key + const baseURL = + (config.baseURL as string) || + (api.defaults.baseURL as string) || + ''; + const isLocalhost = + baseURL.includes('localhost') || baseURL.includes('127.0.0.1'); + + if (!isLocalhost) { + // 从环境变量或 localStorage 获取 API Key + const apiKey = + (import.meta.env.VITE_API_KEY as string) || + localStorage.getItem('apiKey'); + if (apiKey && config.headers) { + config.headers['X-API-Key'] = apiKey; + } + } + + return config; }, - (error) => { - return Promise.reject(error) - } -) + (error: AxiosError) => { + return Promise.reject(error); + }, +); // 响应拦截器 api.interceptors.response.use( (response) => { - return response + return response; }, - (error) => { - console.error('API请求错误:', error) - return Promise.reject(error) - } -) + (error: AxiosError) => { + console.error('API请求错误:', error); + return Promise.reject(error); + }, +); -export default api \ No newline at end of file +export default api; \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 7897e8e..f8168e6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { TasksModule } from './schedule/schedule.module'; import { LoggerModule } from './common/logger/logger.module'; import { LoggingMiddleware } from './common/logger/logging.middleware'; import { AiModule } from './ai/ai.module'; +import { AuthModule } from './common/auth/auth.module'; @Module({ imports: [ @@ -21,6 +22,7 @@ import { AiModule } from './ai/ai.module'; exclude: ['/api'], }), LoggerModule, + AuthModule, DatabaseModule, BidsModule, KeywordsModule, diff --git a/src/common/auth/auth.guard.ts b/src/common/auth/auth.guard.ts new file mode 100644 index 0000000..c3d3418 --- /dev/null +++ b/src/common/auth/auth.guard.ts @@ -0,0 +1,83 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const clientIp = this.getClientIp(request); + + // 检查是否为本地 IP + if (this.isLocalIp(clientIp)) { + return true; // 本地访问直接放行 + } + + // 公网访问需要验证 API Key + const apiKey = request.headers['x-api-key'] as string; + const expectedApiKey = this.configService.get('API_KEY'); + + if (!expectedApiKey) { + // 如果未配置 API_KEY,允许所有访问(开发环境) + return true; + } + + if (!apiKey || apiKey !== expectedApiKey) { + throw new UnauthorizedException('Invalid or missing API Key'); + } + + return true; + } + + /** + * 获取客户端真实 IP + * 支持反向代理场景(X-Forwarded-For) + */ + private getClientIp(request: Request): string { + const forwardedFor = request.headers['x-forwarded-for']; + if (forwardedFor) { + // X-Forwarded-For 可能包含多个 IP,取第一个 + const ips = (forwardedFor as string).split(','); + return ips[0].trim(); + } + return request.ip || request.socket.remoteAddress || 'unknown'; + } + + /** + * 判断是否为本地 IP + */ + private isLocalIp(ip: string): boolean { + if (!ip || ip === 'unknown') { + return false; + } + + // IPv4 本地地址 + if (ip === '127.0.0.1' || ip === 'localhost' || ip.startsWith('127.')) { + return true; + } + + // IPv6 本地地址 + if (ip === '::1' || ip === '::ffff:127.0.0.1') { + return true; + } + + // 内网地址(可选,根据需求决定是否允许) + // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + const privateIpRegex = + /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/; + if (privateIpRegex.test(ip)) { + // 如果需要内网也免鉴权,可以返回 true + // 这里默认返回 false,内网也需要鉴权 + return false; + } + + return false; + } +} diff --git a/src/common/auth/auth.module.ts b/src/common/auth/auth.module.ts new file mode 100644 index 0000000..0609fd0 --- /dev/null +++ b/src/common/auth/auth.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthGuard } from './auth.guard'; + +@Module({ + providers: [ + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + ], +}) +export class AuthModule {} diff --git a/src/main.ts b/src/main.ts index 725bc57..0ecbab2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,10 @@ async function bootstrap() { // 启用 CORS app.enableCors(); + // 信任代理(用于获取真实客户端 IP) + const httpAdapter = app.getHttpAdapter(); + httpAdapter.getInstance().set('trust proxy', true); + await app.listen(process.env.PORT ?? 3000); } void bootstrap();