feat: jwt auth. #7
23
package-lock.json
generated
23
package-lock.json
generated
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "fennec-be",
|
"name": "fennec-be",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
||||||
|
"@nestjs-lib/auth": "^0.1.1",
|
||||||
"@nestjs/common": "^7.5.1",
|
"@nestjs/common": "^7.5.1",
|
||||||
"@nestjs/config": "^0.6.2",
|
"@nestjs/config": "^0.6.2",
|
||||||
"@nestjs/core": "^7.5.1",
|
"@nestjs/core": "^7.5.1",
|
||||||
@ -2892,6 +2893,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
||||||
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs-lib/auth": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://npm.ivanli.cc/@nestjs-lib%2fauth/-/auth-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-JXKvDsJudBlEXBiGyoODFpbbJabcoSaUqJY0bQHX0imidmhovx3VuGZwudALrlw1BT2NOJEO7ElFoITCfTDfGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^7.0.0",
|
||||||
|
"@nestjs/graphql": "^7.10.3",
|
||||||
|
"jose": "^3.14.0",
|
||||||
|
"nestjs-etcd": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/cli": {
|
"node_modules/@nestjs/cli": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz",
|
||||||
@ -18822,6 +18835,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
|
||||||
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
"integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="
|
||||||
},
|
},
|
||||||
|
"@nestjs-lib/auth": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://npm.ivanli.cc/@nestjs-lib%2fauth/-/auth-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-JXKvDsJudBlEXBiGyoODFpbbJabcoSaUqJY0bQHX0imidmhovx3VuGZwudALrlw1BT2NOJEO7ElFoITCfTDfGw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"@nestjs/cli": {
|
"@nestjs/cli": {
|
||||||
"version": "7.6.0",
|
"version": "7.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-7.6.0.tgz",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "fennec-be",
|
"name": "fennec-be",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "",
|
"description": "a ci/cd project.",
|
||||||
"author": "",
|
"author": "Ivan Li\b<ivanli2048@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -22,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
||||||
|
"@nestjs-lib/auth": "^0.1.1",
|
||||||
"@nestjs/common": "^7.5.1",
|
"@nestjs/common": "^7.5.1",
|
||||||
"@nestjs/config": "^0.6.2",
|
"@nestjs/config": "^0.6.2",
|
||||||
"@nestjs/core": "^7.5.1",
|
"@nestjs/core": "^7.5.1",
|
||||||
|
@ -19,7 +19,7 @@ import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware'
|
|||||||
import { LoggerModule } from 'nestjs-pino';
|
import { LoggerModule } from 'nestjs-pino';
|
||||||
import { EtcdModule } from 'nestjs-etcd';
|
import { EtcdModule } from 'nestjs-etcd';
|
||||||
import pinoPretty from 'pino-pretty';
|
import pinoPretty from 'pino-pretty';
|
||||||
import { AccountMiddleware } from './commons/middleware/account.middleware';
|
import { fromPairs, map, pipe, toPairs } from 'ramda';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -64,6 +64,22 @@ import { AccountMiddleware } from './commons/middleware/account.middleware';
|
|||||||
playground: true,
|
playground: true,
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
installSubscriptionHandlers: true,
|
installSubscriptionHandlers: true,
|
||||||
|
context: ({ req, connection, ...args }) => {
|
||||||
|
return connection ? { req: connection.context } : { req };
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
onConnect: (connectionParams: Record<string, string>) => {
|
||||||
|
const connectionParamsWithLowerKeys = pipe(
|
||||||
|
toPairs,
|
||||||
|
map(([key, value]) => [key.toLowerCase(), value]),
|
||||||
|
fromPairs,
|
||||||
|
)(connectionParams);
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: connectionParamsWithLowerKeys,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
@ -100,8 +116,6 @@ export class AppModule implements NestModule {
|
|||||||
.apply(RawBodyMiddleware)
|
.apply(RawBodyMiddleware)
|
||||||
.forRoutes(GiteaWebhooksController)
|
.forRoutes(GiteaWebhooksController)
|
||||||
.apply(ParseBodyMiddleware)
|
.apply(ParseBodyMiddleware)
|
||||||
.forRoutes('*')
|
|
||||||
.apply(AccountMiddleware)
|
|
||||||
.forRoutes('*');
|
.forRoutes('*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PasswordConverter } from './services/password-converter';
|
import { PasswordConverter } from './services/password-converter';
|
||||||
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
|
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
|
||||||
import { JwtService } from './services/jwt.service';
|
import { AuthModule } from '@nestjs-lib/auth';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PasswordConverter, JwtService],
|
imports: [RedisMutexModule, AuthModule],
|
||||||
exports: [PasswordConverter, RedisMutexModule, JwtService],
|
providers: [PasswordConverter],
|
||||||
imports: [RedisMutexModule],
|
exports: [PasswordConverter, RedisMutexModule],
|
||||||
})
|
})
|
||||||
export class CommonsModule {}
|
export class CommonsModule {}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { JwtService } from '../services/jwt.service';
|
|
||||||
import { AccountMiddleware } from './account.middleware';
|
|
||||||
|
|
||||||
describe('AccountMiddleware', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(new AccountMiddleware({} as JwtService)).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,31 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
NestMiddleware,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { JwtService } from '../services/jwt.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AccountMiddleware implements NestMiddleware {
|
|
||||||
constructor(private readonly jwtService: JwtService) {}
|
|
||||||
async use(req: any, res: any, next: () => void) {
|
|
||||||
const authPayload = req.header('authorization') ?? '';
|
|
||||||
if (!authPayload) {
|
|
||||||
req.user = req.session?.user;
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = authPayload.replace('Bearer ', '');
|
|
||||||
if (!token) {
|
|
||||||
throw new UnauthorizedException('授权凭据不合法!');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { payload } = await this.jwtService.verify(token);
|
|
||||||
req.user = payload;
|
|
||||||
next();
|
|
||||||
} catch (err) {
|
|
||||||
throw new UnauthorizedException('登录凭据失效或不合法!');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { generateKeyPair, KeyObject } from 'crypto';
|
|
||||||
import { getClientToken } from 'nestjs-etcd';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { JwtService } from './jwt.service';
|
|
||||||
import { SignJWT } from 'jose/jwt/sign';
|
|
||||||
|
|
||||||
describe('JwtService', () => {
|
|
||||||
let service: JwtService;
|
|
||||||
let privateKey: KeyObject;
|
|
||||||
let publicKey: KeyObject;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const pair = await promisify(generateKeyPair)('ec', {
|
|
||||||
namedCurve: 'prime256v1',
|
|
||||||
});
|
|
||||||
privateKey = pair.privateKey;
|
|
||||||
publicKey = pair.publicKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
JwtService,
|
|
||||||
{
|
|
||||||
provide: getClientToken(),
|
|
||||||
useValue: {
|
|
||||||
get: () => ({
|
|
||||||
buffer: () =>
|
|
||||||
Promise.resolve(
|
|
||||||
publicKey.export({ format: 'pem', type: 'spki' }),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<JwtService>(JwtService);
|
|
||||||
await service.onModuleInit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('verify', () => {
|
|
||||||
it('normal', async () => {
|
|
||||||
const token = await new SignJWT({ userId: 'test' })
|
|
||||||
.setProtectedHeader({ alg: 'ES256' })
|
|
||||||
.setIssuedAt()
|
|
||||||
.setIssuer('urn:example:issuer')
|
|
||||||
.setAudience('urn:example:audience')
|
|
||||||
.setExpirationTime('1h')
|
|
||||||
.sign(privateKey);
|
|
||||||
await expect(service.verify(token)).resolves.toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,23 +0,0 @@
|
|||||||
import { OnModuleInit } from '@nestjs/common';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { KeyObject, createPublicKey } from 'crypto';
|
|
||||||
import { jwtVerify } from 'jose/jwt/verify';
|
|
||||||
import { Etcd3, InjectClient } from 'nestjs-etcd';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JwtService implements OnModuleInit {
|
|
||||||
publicKey: KeyObject;
|
|
||||||
constructor(@InjectClient() private readonly etcd: Etcd3) {}
|
|
||||||
async onModuleInit() {
|
|
||||||
const buff = await this.etcd
|
|
||||||
.get('commons/auth-jwt-public-key/index')
|
|
||||||
.buffer();
|
|
||||||
this.publicKey = createPublicKey(buff);
|
|
||||||
}
|
|
||||||
|
|
||||||
async verify(token: string) {
|
|
||||||
return await jwtVerify(token, this.publicKey, {
|
|
||||||
algorithms: ['PS256', 'ES256'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,9 @@ import { plainToClass } from 'class-transformer';
|
|||||||
import { PipelineTaskLogger } from './pipeline-task.logger';
|
import { PipelineTaskLogger } from './pipeline-task.logger';
|
||||||
import { observableToAsyncIterable } from '@graphql-tools/utils';
|
import { observableToAsyncIterable } from '@graphql-tools/utils';
|
||||||
import { PipelineTaskEvent } from './models/pipeline-task-event';
|
import { PipelineTaskEvent } from './models/pipeline-task-event';
|
||||||
|
import { Roles, AccountRole } from '@nestjs-lib/auth';
|
||||||
|
|
||||||
|
@Roles(AccountRole.admin, AccountRole.super)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class PipelineTasksResolver {
|
export class PipelineTasksResolver {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Roles, AccountRole } from '@nestjs-lib/auth';
|
||||||
import { Query } from '@nestjs/graphql';
|
import { Query } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
@ -10,6 +11,7 @@ import { PipelineTasksService } from '../pipeline-tasks/pipeline-tasks.service';
|
|||||||
import { Commit, LogFields } from '../repos/dtos/log-list.model';
|
import { Commit, LogFields } from '../repos/dtos/log-list.model';
|
||||||
import { PipelinesService } from './pipelines.service';
|
import { PipelinesService } from './pipelines.service';
|
||||||
|
|
||||||
|
@Roles(AccountRole.admin, AccountRole.super)
|
||||||
@Resolver(() => Commit)
|
@Resolver(() => Commit)
|
||||||
export class CommitLogsResolver {
|
export class CommitLogsResolver {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -4,7 +4,9 @@ import { UpdatePipelineInput } from './dtos/update-pipeline.input';
|
|||||||
import { Pipeline } from './pipeline.entity';
|
import { Pipeline } from './pipeline.entity';
|
||||||
import { PipelinesService } from './pipelines.service';
|
import { PipelinesService } from './pipelines.service';
|
||||||
import { ListPipelineArgs } from './dtos/list-pipelines.args';
|
import { ListPipelineArgs } from './dtos/list-pipelines.args';
|
||||||
|
import { Roles, AccountRole } from '@nestjs-lib/auth';
|
||||||
|
|
||||||
|
@Roles(AccountRole.admin, AccountRole.super)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class PipelinesResolver {
|
export class PipelinesResolver {
|
||||||
constructor(private readonly service: PipelinesService) {}
|
constructor(private readonly service: PipelinesService) {}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { AccountRole, Roles } from '@nestjs-lib/auth';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
import { CreateProjectInput } from './dtos/create-project.input';
|
import { CreateProjectInput } from './dtos/create-project.input';
|
||||||
import { UpdateProjectInput } from './dtos/update-project.input';
|
import { UpdateProjectInput } from './dtos/update-project.input';
|
||||||
import { Project } from './project.entity';
|
import { Project } from './project.entity';
|
||||||
import { ProjectsService } from './projects.service';
|
import { ProjectsService } from './projects.service';
|
||||||
|
|
||||||
|
@Roles(AccountRole.admin, AccountRole.super)
|
||||||
@Resolver(() => Project)
|
@Resolver(() => Project)
|
||||||
export class ProjectsResolver {
|
export class ProjectsResolver {
|
||||||
constructor(private readonly service: ProjectsService) {}
|
constructor(private readonly service: ProjectsService) {}
|
||||||
|
Loading…
Reference in New Issue
Block a user