From 6937049ece54aaecbb86cb885203e0ca375c3745 Mon Sep 17 00:00:00 2001 From: magiccaptain Date: Sun, 19 Jan 2025 21:45:09 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20header=20api?= =?UTF-8?q?=20key=20=E7=9A=84=E9=AA=8C=E8=AF=81=20#49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 3 +++ src/app.module.ts | 10 ++++++++-- src/auth/api-key-auth.guard.ts | 31 +++++++++++++++++++++++++++++++ src/auth/index.ts | 1 + src/constants/config.ts | 1 + src/hello.controller.ts | 3 +++ 6 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/auth/api-key-auth.guard.ts diff --git a/.env b/.env index f7f38a1..da2df51 100644 --- a/.env +++ b/.env @@ -30,3 +30,6 @@ LOGIN_LOCK_IN_S=60 ## JWT JWT_SECRET_KEY=cksfkewkfoptyhiop534o239dkja23as + +# API KEY +API_KEY=YHImpSchS4iEwVD1IxXp4012 diff --git a/src/app.module.ts b/src/app.module.ts index 6cbe9e0..67f7bad 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,7 @@ import { BullModule } from '@nestjs/bull'; import { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager'; import { Inject, MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { MongooseModule } from '@nestjs/mongoose/dist/mongoose.module'; import { redisClusterInsStore, redisInsStore } from 'cache-manager-redis-yet'; @@ -8,7 +9,7 @@ import { redisClusterInsStore, redisInsStore } from 'cache-manager-redis-yet'; import * as config from 'src/constants'; import { AppController } from './app.controller'; -import { AuthModule } from './auth'; +import { ApiKeyAuthGuard, AuthModule } from './auth'; import { CaptchaModule } from './captcha'; import { RouteLoggerMiddleware } from './common/route-logger.middleware'; import { EmailModule } from './email'; @@ -61,7 +62,12 @@ import { UserModule } from './user'; RoleModule, ], controllers: [HelloController, AppController], - providers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ApiKeyAuthGuard, + }, + ], }) export class AppModule implements NestModule { constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: any) {} diff --git a/src/auth/api-key-auth.guard.ts b/src/auth/api-key-auth.guard.ts new file mode 100644 index 0000000..6b80d3e --- /dev/null +++ b/src/auth/api-key-auth.guard.ts @@ -0,0 +1,31 @@ +import { CanActivate, ExecutionContext, Injectable, SetMetadata } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +import { auth } from 'src/constants'; + +export const IS_PUBLIC_KEY = 'isPublic'; + +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +@Injectable() +export class ApiKeyAuthGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): any { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const apiKey = request.headers['x-api-key']; + if (apiKey === auth.apiKey) { + return true; + } + + return false; + } +} diff --git a/src/auth/index.ts b/src/auth/index.ts index 1aea172..4427f40 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,3 +1,4 @@ export * from './auth.module'; export * from './entities/jwt.entity'; export * from './entities/session-with-token.entity'; +export * from './api-key-auth.guard'; diff --git a/src/constants/config.ts b/src/constants/config.ts index c442c21..e822853 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -12,6 +12,7 @@ export const auth = { maxLoginAttempts: toInteger(loadEnv('MAX_LOGIN_ATTEMPTS')), // 过期时间 loginLockInS: toInteger(loadEnv('LOGIN_LOCK_IN_S')), // 验证码长度 jwtSecretKey: loadEnv('JWT_SECRET_KEY'), // jwt secret key + apiKey: loadEnv('API_KEY'), // api key }; export const captcha = { diff --git a/src/hello.controller.ts b/src/hello.controller.ts index 986a2e5..080175b 100644 --- a/src/hello.controller.ts +++ b/src/hello.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; +import { Public } from './auth'; + class HealthCheckResult { @ApiProperty({ name: 'message', @@ -15,6 +17,7 @@ export class HelloController { /** * health check */ + @Public() @ApiOperation({ operationId: 'hello' }) @ApiOkResponse({ description: 'Hello!', From 0608a40efc5c2dbde41da9725b2290cde5e09a8d Mon Sep 17 00:00:00 2001 From: magiccaptain Date: Mon, 20 Jan 2025 08:57:36 +0800 Subject: [PATCH 2/2] fix: e2e test --- test/auth-login-logout.e2e-spec.ts | 9 +++++++++ test/captcha.e2e-spec.ts | 15 +++++++++++++++ test/namespace.e2e-spec.ts | 10 ++++++++++ test/user.e2e-spec.ts | 12 ++++++++++++ 4 files changed, 46 insertions(+) diff --git a/test/auth-login-logout.e2e-spec.ts b/test/auth-login-logout.e2e-spec.ts index 8edde83..8293dcd 100644 --- a/test/auth-login-logout.e2e-spec.ts +++ b/test/auth-login-logout.e2e-spec.ts @@ -6,6 +6,7 @@ import { Connection } from 'mongoose'; import request from 'supertest'; import { SessionWithToken } from 'src/auth'; +import { auth } from 'src/constants'; import { NamespaceService } from 'src/namespace'; import { UserService } from 'src/user'; @@ -65,6 +66,7 @@ describe('Web auth (e2e)', () => { .send(userDoc) .set('Content-Type', 'application/json') .set('Accept', 'application/json') + .set('x-api-key', auth.apiKey) .expect(200); const user = registerResp.body; @@ -80,6 +82,7 @@ describe('Web auth (e2e)', () => { }) .set('Content-Type', 'application/json') .set('Accept', 'application/json') + .set('x-api-key', auth.apiKey) .expect(401); }); @@ -95,6 +98,7 @@ describe('Web auth (e2e)', () => { password: userDoc.password, }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json'); const session: SessionWithToken = sessionResp.body; @@ -107,6 +111,7 @@ describe('Web auth (e2e)', () => { .post('/auth/@refresh') .send({ refreshToken: session.refreshToken }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); const sessionWithToken: SessionWithToken = refreshTokenResp.body; @@ -119,6 +124,7 @@ describe('Web auth (e2e)', () => { .post('/auth/@refresh') .send({ refreshToken: session.refreshToken }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json'); expect(shouldRotateRes.statusCode).toBe(200); const rotateSession: SessionWithToken = shouldRotateRes.body; @@ -132,6 +138,7 @@ describe('Web auth (e2e)', () => { .post('/auth/@refresh') .send({ refreshToken: session.refreshToken }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json'); expect(expiredRes.statusCode).toBe(401); global.Date.now = RealDate; @@ -140,6 +147,7 @@ describe('Web auth (e2e)', () => { await request(app.getHttpServer()) .delete(`/sessions/${session.id}`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(204); @@ -148,6 +156,7 @@ describe('Web auth (e2e)', () => { .post('/auth/@refresh') .send({ refreshToken: session.refreshToken }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(401); }); diff --git a/test/captcha.e2e-spec.ts b/test/captcha.e2e-spec.ts index 550fea2..f5ca202 100644 --- a/test/captcha.e2e-spec.ts +++ b/test/captcha.e2e-spec.ts @@ -7,6 +7,7 @@ import request from 'supertest'; import { SessionWithToken } from 'src/auth'; import { CaptchaService, CreateCaptchaDto } from 'src/captcha'; +import { auth } from 'src/constants'; import { MongoErrorsInterceptor } from 'src/mongo'; import { AppModule } from '../src/app.module'; @@ -55,6 +56,7 @@ describe('Captcha workflow (e2e)', () => { .post('/captchas') .send(captchaDoc) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(201); @@ -64,6 +66,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@registerByPhone') .send({ phone, code: '000000', key: '0000' }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -72,6 +75,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@registerByPhone') .send({ phone, code: captchaDoc.code, key: '0000' }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -79,6 +83,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@registerByPhone') .send({ phone, ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); @@ -100,6 +105,7 @@ describe('Captcha workflow (e2e)', () => { .post('/captchas') .send({ phone, ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(201); @@ -108,6 +114,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@loginByPhone') .send({ phone: '11111111111', ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(401); @@ -116,6 +123,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@loginByPhone') .send({ phone, ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); @@ -135,6 +143,7 @@ describe('Captcha workflow (e2e)', () => { .post('/captchas') .send(captchaDoc) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(201); @@ -143,6 +152,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@registerByEmail') .send({ email, code: '000000', key: '0000' }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -151,6 +161,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@registerByEmail') .send({ email, code: captchaDoc.code, key: '0000' }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -158,6 +169,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@registerByEmail') .send({ email, ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); @@ -179,6 +191,7 @@ describe('Captcha workflow (e2e)', () => { .post('/captchas') .send({ email, ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(201); @@ -187,6 +200,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@loginByEmail') .send({ email: 'aa@36node.com', ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(401); @@ -195,6 +209,7 @@ describe('Captcha workflow (e2e)', () => { .post('/auth/@loginByEmail') .send({ email, ...captchaDoc }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); diff --git a/test/namespace.e2e-spec.ts b/test/namespace.e2e-spec.ts index 1ce41d4..a6970c4 100644 --- a/test/namespace.e2e-spec.ts +++ b/test/namespace.e2e-spec.ts @@ -5,6 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Connection, Model } from 'mongoose'; import request from 'supertest'; +import { auth } from 'src/constants'; import { MongoErrorsInterceptor } from 'src/mongo'; import { Namespace, NamespaceDocument, NamespaceService } from 'src/namespace'; @@ -55,6 +56,7 @@ describe('Namespace crud (e2e)', () => { ns: faker.helpers.arrayElement(invalidNs), }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -67,6 +69,7 @@ describe('Namespace crud (e2e)', () => { ns: 'haivivi.com2/pal1', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(404); }); @@ -82,6 +85,7 @@ describe('Namespace crud (e2e)', () => { const resp1 = await request(app.getHttpServer()) .get(`/namespaces`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); expect(resp1.body.length).toBeGreaterThanOrEqual(5); // 包含初始化的 namespace @@ -90,6 +94,7 @@ describe('Namespace crud (e2e)', () => { const resp2 = await request(app.getHttpServer()) .get(`/namespaces?ns=n1`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); expect(resp2.body).toHaveLength(2); @@ -98,6 +103,7 @@ describe('Namespace crud (e2e)', () => { const resp3 = await request(app.getHttpServer()) .get(`/namespaces?ns_start=n`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); expect(resp3.body).toHaveLength(3); @@ -114,6 +120,7 @@ describe('Namespace crud (e2e)', () => { const resp1 = await request(app.getHttpServer()) .get(`/namespaces/${encodeURIComponent(ns1.id)}`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); const founded1 = resp1.body; @@ -123,6 +130,7 @@ describe('Namespace crud (e2e)', () => { const resp2 = await request(app.getHttpServer()) .get(`/namespaces/${encodeURIComponent('aaa/bbb')}`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); const founded = resp2.body; @@ -143,6 +151,7 @@ describe('Namespace crud (e2e)', () => { name: nameToBeUpdated, }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); const updated = resp.body; @@ -160,6 +169,7 @@ describe('Namespace crud (e2e)', () => { await request(app.getHttpServer()) .delete(`/namespaces/${ns.id}`) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(204); expect(await namespaceService.get(ns.id)).toBeNull(); diff --git a/test/user.e2e-spec.ts b/test/user.e2e-spec.ts index 6034c32..0f59571 100644 --- a/test/user.e2e-spec.ts +++ b/test/user.e2e-spec.ts @@ -5,6 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Connection } from 'mongoose'; import request from 'supertest'; +import { auth } from 'src/constants'; import { MongoErrorsInterceptor } from 'src/mongo'; import { NamespaceService } from 'src/namespace'; import { UserService } from 'src/user'; @@ -62,6 +63,7 @@ describe('User crud (e2e)', () => { .post('/users') .send(userDoc) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(404); @@ -75,6 +77,7 @@ describe('User crud (e2e)', () => { .post('/users') .send({ ...userDoc, password: '1234567' }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -83,6 +86,7 @@ describe('User crud (e2e)', () => { .post('/users') .send(userDoc) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(201); const user = userResp.body; @@ -97,6 +101,7 @@ describe('User crud (e2e)', () => { ns: 'a/b', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -109,6 +114,7 @@ describe('User crud (e2e)', () => { ns: 'a/b', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); }); @@ -128,6 +134,7 @@ describe('User crud (e2e)', () => { password: '^tR123456', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(404); @@ -139,6 +146,7 @@ describe('User crud (e2e)', () => { newPassword: '^123456', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -150,6 +158,7 @@ describe('User crud (e2e)', () => { newPassword: '^tR123456', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(204); @@ -161,6 +170,7 @@ describe('User crud (e2e)', () => { password: '^tR123456', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(200); @@ -171,6 +181,7 @@ describe('User crud (e2e)', () => { username: '1a@22', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); @@ -181,6 +192,7 @@ describe('User crud (e2e)', () => { email: '1a@22', }) .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) .set('Accept', 'application/json') .expect(400); });