feat(web,server): api keys (#1244)

* feat(server): api keys

* chore: open-api

* feat(web): api keys

* fix: remove keys when deleting a user
This commit is contained in:
Jason Rasmussen
2023-01-02 15:22:33 -05:00
committed by GitHub
parent 9edbff0ec0
commit 9e6d6b2532
51 changed files with 2586 additions and 35 deletions
@@ -0,0 +1,48 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, ValidationPipe } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { APIKeyService } from './api-key.service';
import { APIKeyCreateDto } from './dto/api-key-create.dto';
import { APIKeyUpdateDto } from './dto/api-key-update.dto';
import { APIKeyCreateResponseDto } from './repsonse-dto/api-key-create-response.dto';
import { APIKeyResponseDto } from './repsonse-dto/api-key-response.dto';
@ApiTags('API Key')
@Controller('api-key')
@Authenticated()
export class APIKeyController {
constructor(private service: APIKeyService) {}
@Post()
createKey(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: APIKeyCreateDto,
): Promise<APIKeyCreateResponseDto> {
return this.service.create(authUser, dto);
}
@Get()
getKeys(@GetAuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(authUser);
}
@Get(':id')
getKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id);
}
@Put(':id')
updateKey(
@GetAuthUser() authUser: AuthUserDto,
@Param('id', ParseIntPipe) id: number,
@Body(ValidationPipe) dto: APIKeyUpdateDto,
): Promise<APIKeyResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
deleteKey(@GetAuthUser() authUser: AuthUserDto, @Param('id', ParseIntPipe) id: number): Promise<void> {
return this.service.delete(authUser, id);
}
}
@@ -0,0 +1,16 @@
import { APIKeyEntity } from '@app/database';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APIKeyController } from './api-key.controller';
import { APIKeyRepository, IKeyRepository } from './api-key.repository';
import { APIKeyService } from './api-key.service';
const KEY_REPOSITORY = { provide: IKeyRepository, useClass: APIKeyRepository };
@Module({
imports: [TypeOrmModule.forFeature([APIKeyEntity])],
controllers: [APIKeyController],
providers: [APIKeyService, KEY_REPOSITORY],
exports: [APIKeyService, KEY_REPOSITORY],
})
export class APIKeyModule {}
@@ -0,0 +1,59 @@
import { APIKeyEntity } from '@app/database';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
export const IKeyRepository = 'IKeyRepository';
export interface IKeyRepository {
create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
delete(userId: string, id: number): Promise<void>;
/**
* Includes the hashed `key` for verification
* @param id
*/
getKey(id: number): Promise<APIKeyEntity | null>;
getById(userId: string, id: number): Promise<APIKeyEntity | null>;
getByUserId(userId: string): Promise<APIKeyEntity[]>;
}
@Injectable()
export class APIKeyRepository implements IKeyRepository {
constructor(@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>) {}
async create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> {
return this.repository.save(dto);
}
async update(userId: string, id: number, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity> {
await this.repository.update({ userId, id }, dto);
return this.repository.findOneOrFail({ where: { id: dto.id } });
}
async delete(userId: string, id: number): Promise<void> {
await this.repository.delete({ userId, id });
}
getKey(id: number): Promise<APIKeyEntity | null> {
return this.repository.findOne({
select: {
id: true,
key: true,
userId: true,
},
where: { id },
relations: {
user: true,
},
});
}
getById(userId: string, id: number): Promise<APIKeyEntity | null> {
return this.repository.findOne({ where: { userId, id } });
}
getByUserId(userId: string): Promise<APIKeyEntity[]> {
return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } });
}
}
@@ -0,0 +1,74 @@
import { UserEntity } from '@app/database';
import { BadRequestException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { compareSync, hash } from 'bcrypt';
import { randomBytes } from 'node:crypto';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { IKeyRepository } from './api-key.repository';
import { APIKeyCreateDto } from './dto/api-key-create.dto';
import { APIKeyCreateResponseDto } from './repsonse-dto/api-key-create-response.dto';
import { APIKeyResponseDto, mapKey } from './repsonse-dto/api-key-response.dto';
@Injectable()
export class APIKeyService {
constructor(@Inject(IKeyRepository) private repository: IKeyRepository) {}
async create(authUser: AuthUserDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
const key = randomBytes(24).toString('base64').replace(/\W/g, '');
const entity = await this.repository.create({
key: await hash(key, 10),
name: dto.name || 'API Key',
userId: authUser.id,
});
const secret = Buffer.from(`${entity.id}:${key}`, 'utf8').toString('base64');
return { secret, apiKey: mapKey(entity) };
}
async update(authUser: AuthUserDto, id: number, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
const exists = await this.repository.getById(authUser.id, id);
if (!exists) {
throw new BadRequestException('API Key not found');
}
return this.repository.update(authUser.id, id, {
name: dto.name,
});
}
async delete(authUser: AuthUserDto, id: number): Promise<void> {
const exists = await this.repository.getById(authUser.id, id);
if (!exists) {
throw new BadRequestException('API Key not found');
}
await this.repository.delete(authUser.id, id);
}
async getById(authUser: AuthUserDto, id: number): Promise<APIKeyResponseDto> {
const key = await this.repository.getById(authUser.id, id);
if (!key) {
throw new BadRequestException('API Key not found');
}
return mapKey(key);
}
async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
const keys = await this.repository.getByUserId(authUser.id);
return keys.map(mapKey);
}
async validate(token: string): Promise<UserEntity> {
const [_id, key] = Buffer.from(token, 'base64').toString('utf8').split(':');
const id = Number(_id);
if (id && key) {
const entity = await this.repository.getKey(id);
if (entity?.user && entity?.key && compareSync(key, entity.key)) {
return entity.user as UserEntity;
}
}
throw new UnauthorizedException('Invalid API Key');
}
}
@@ -0,0 +1,8 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class APIKeyCreateDto {
@IsString()
@IsNotEmpty()
@IsOptional()
name?: string;
}
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class APIKeyUpdateDto {
@IsString()
@IsNotEmpty()
name!: string;
}
@@ -0,0 +1,6 @@
import { APIKeyResponseDto } from './api-key-response.dto';
export class APIKeyCreateResponseDto {
secret!: string;
apiKey!: APIKeyResponseDto;
}
@@ -0,0 +1,17 @@
import { APIKeyEntity } from '@app/database';
export class APIKeyResponseDto {
id!: number;
name!: string;
createdAt!: string;
updatedAt!: string;
}
export function mapKey(entity: APIKeyEntity): APIKeyResponseDto {
return {
id: entity.id,
name: entity.name,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
};
}
+3
View File
@@ -19,6 +19,7 @@ import { JobModule } from './api-v1/job/job.module';
import { SystemConfigModule } from './api-v1/system-config/system-config.module';
import { OAuthModule } from './api-v1/oauth/oauth.module';
import { TagModule } from './api-v1/tag/tag.module';
import { APIKeyModule } from './api-v1/api-key/api-key.module';
@Module({
imports: [
@@ -27,6 +28,8 @@ import { TagModule } from './api-v1/tag/tag.module';
DatabaseModule,
UserModule,
APIKeyModule,
AssetModule,
AuthModule,
@@ -1,13 +1,13 @@
import { UseGuards } from '@nestjs/common';
import { AdminRolesGuard } from '../middlewares/admin-role-guard.middleware';
import { JwtAuthGuard } from '../modules/immich-jwt/guards/jwt-auth.guard';
import { AuthGuard } from '../modules/immich-jwt/guards/auth.guard';
interface AuthenticatedOptions {
admin?: boolean;
}
export const Authenticated = (options?: AuthenticatedOptions) => {
const guards: Parameters<typeof UseGuards> = [JwtAuthGuard];
const guards: Parameters<typeof UseGuards> = [AuthGuard];
options = options || {};
if (options.admin) {
guards.push(AdminRolesGuard);
@@ -1,12 +1,17 @@
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { Request } from 'express';
import { UserResponseDto } from '../api-v1/user/response-dto/user-response.dto';
interface UserRequest extends Request {
user: UserResponseDto;
}
@Injectable()
export class AdminRolesGuard implements CanActivate {
logger = new Logger(AdminRolesGuard.name);
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const request = context.switchToHttp().getRequest<UserRequest>();
const isAdmin = request.user?.isAdmin || false;
if (!isAdmin) {
this.logger.log(`Denied access to admin only route: ${request.path}`);
@@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
import { API_KEY_STRATEGY } from '../strategies/api-key.strategy';
import { JWT_STRATEGY } from '../strategies/jwt.strategy';
@Injectable()
export class AuthGuard extends PassportAuthGuard([JWT_STRATEGY, API_KEY_STRATEGY]) {}
@@ -1,5 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
@@ -5,10 +5,12 @@ import { jwtConfig } from '../../config/jwt.config';
import { JwtStrategy } from './strategies/jwt.strategy';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '@app/database';
import { APIKeyModule } from '../../api-v1/api-key/api-key.module';
import { APIKeyStrategy } from './strategies/api-key.strategy';
@Module({
imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity])],
providers: [ImmichJwtService, JwtStrategy],
imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity]), APIKeyModule],
providers: [ImmichJwtService, JwtStrategy, APIKeyStrategy],
exports: [ImmichJwtService],
})
export class ImmichJwtModule {}
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
import { APIKeyService } from '../../../api-v1/api-key/api-key.service';
export const API_KEY_STRATEGY = 'api-key';
const options: IStrategyOptions = {
header: 'x-api-key',
};
@Injectable()
export class APIKeyStrategy extends PassportStrategy(Strategy, API_KEY_STRATEGY) {
constructor(private apiKeyService: APIKeyService) {
super(options);
}
async validate(token: string) {
return this.apiKeyService.validate(token);
}
}
@@ -1,15 +1,17 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt';
import { Repository } from 'typeorm';
import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto';
import { UserEntity } from '@app/database';
import { jwtSecret } from '../../../constants/jwt.constant';
import { ImmichJwtService } from '../immich-jwt.service';
export const JWT_STRATEGY = 'jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) {
constructor(
@InjectRepository(UserEntity)
private usersRepository: Repository<UserEntity>,
@@ -22,7 +24,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
]),
ignoreExpiration: false,
secretOrKey: jwtSecret,
});
} as StrategyOptions);
}
async validate(payload: JwtPayloadDto) {
+2 -2
View File
@@ -3,7 +3,7 @@ import { TestingModuleBuilder } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { IUserRepository } from '../src/api-v1/user/user-repository';
import { AuthUserDto } from '../src/decorators/auth-user.decorator';
import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard';
import { AuthGuard } from '../src/modules/immich-jwt/guards/auth.guard';
type CustomAuthCallback = () => AuthUserDto;
@@ -49,5 +49,5 @@ export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCa
return true;
},
};
return builder.overrideGuard(JwtAuthGuard).useValue(canActivate);
return builder.overrideGuard(AuthGuard).useValue(canActivate);
}
@@ -1,5 +1,5 @@
import { immichAppConfig, immichBullAsyncConfig } from '@app/common/config';
import { DatabaseModule, AssetEntity, ExifEntity, SmartInfoEntity, UserEntity } from '@app/database';
import { DatabaseModule, AssetEntity, ExifEntity, SmartInfoEntity, UserEntity, APIKeyEntity } from '@app/database';
import { StorageModule } from '@app/storage';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
@@ -23,7 +23,7 @@ import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.c
ConfigModule.forRoot(immichAppConfig),
DatabaseModule,
ImmichConfigModule,
TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity]),
TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity, APIKeyEntity]),
StorageModule,
BullModule.forRootAsync(immichBullAsyncConfig),
BullModule.registerQueue(...immichSharedQueues),
@@ -1,5 +1,5 @@
import { APP_UPLOAD_LOCATION, userUtils } from '@app/common';
import { AssetEntity, UserEntity } from '@app/database';
import { APIKeyEntity, AssetEntity, UserEntity } from '@app/database';
import { QueueNameEnum, userDeletionProcessorName } from '@app/job';
import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface';
import { Process, Processor } from '@nestjs/bull';
@@ -17,6 +17,9 @@ export class UserDeletionProcessor {
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@InjectRepository(APIKeyEntity)
private apiKeyRepository: Repository<APIKeyEntity>,
) {}
@Process(userDeletionProcessorName)
@@ -27,6 +30,7 @@ export class UserDeletionProcessor {
const basePath = APP_UPLOAD_LOCATION;
const userAssetDir = join(basePath, user.id);
fs.rmSync(userAssetDir, { recursive: true, force: true });
await this.apiKeyRepository.delete({ userId: user.id });
await this.assetRepository.delete({ userId: user.id });
await this.userRepository.remove(user);
}
+199
View File
@@ -331,6 +331,148 @@
]
}
},
"/api-key": {
"post": {
"operationId": "createKey",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIKeyCreateDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIKeyCreateResponseDto"
}
}
}
}
},
"tags": [
"API Key"
]
},
"get": {
"operationId": "getKeys",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/APIKeyResponseDto"
}
}
}
}
}
},
"tags": [
"API Key"
]
}
},
"/api-key/{id}": {
"get": {
"operationId": "getKey",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIKeyResponseDto"
}
}
}
}
},
"tags": [
"API Key"
]
},
"put": {
"operationId": "updateKey",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIKeyUpdateDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIKeyResponseDto"
}
}
}
}
},
"tags": [
"API Key"
]
},
"delete": {
"operationId": "deleteKey",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"API Key"
]
}
},
"/asset/upload": {
"post": {
"operationId": "uploadFile",
@@ -2467,6 +2609,63 @@
"profileImagePath"
]
},
"APIKeyCreateDto": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"APIKeyResponseDto": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
},
"required": [
"id",
"name",
"createdAt",
"updatedAt"
]
},
"APIKeyCreateResponseDto": {
"type": "object",
"properties": {
"secret": {
"type": "string"
},
"apiKey": {
"$ref": "#/components/schemas/APIKeyResponseDto"
}
},
"required": [
"secret",
"apiKey"
]
},
"APIKeyUpdateDto": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
]
},
"AssetFileUploadDto": {
"type": "object",
"properties": {
@@ -0,0 +1,26 @@
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { UserEntity } from './user.entity';
@Entity('api_keys')
export class APIKeyEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column({ select: false })
key?: string;
@Column()
userId!: string;
@ManyToOne(() => UserEntity)
user?: UserEntity;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: string;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
}
@@ -1,4 +1,5 @@
export * from './album.entity';
export * from './api-key.entity';
export * from './asset-album.entity';
export * from './asset.entity';
export * from './device-info.entity';
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAPIKeys1672502270115 implements MigrationInterface {
name = 'AddAPIKeys1672502270115'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "api_keys" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`);
await queryRunner.query(`DROP TABLE "api_keys"`);
}
}
+24 -6
View File
@@ -47,6 +47,7 @@
"nest-commander": "^3.3.0",
"openid-client": "^5.2.1",
"passport": "^0.6.0",
"passport-http-header-strategy": "^1.1.0",
"passport-jwt": "^4.0.0",
"pg": "^8.7.1",
"redis": "^3.1.2",
@@ -2377,9 +2378,9 @@
}
},
"node_modules/@types/inquirer": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.4.tgz",
"integrity": "sha512-Pxxx3i3AyK7vKAj3LRM/vF7ETcHKiLJ/u5CnNgbz/eYj/vB3xGAYtRxI5IKtq0hpe5iFHD22BKV3n6WHUu0k4Q==",
"version": "8.2.5",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz",
"integrity": "sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg==",
"peer": true,
"dependencies": {
"@types/through": "*"
@@ -8618,6 +8619,14 @@
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-http-header-strategy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz",
"integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==",
"dependencies": {
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-jwt": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
@@ -9848,6 +9857,7 @@
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"dev": true
},
"node_modules/spawn-command": {
@@ -13079,9 +13089,9 @@
}
},
"@types/inquirer": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.4.tgz",
"integrity": "sha512-Pxxx3i3AyK7vKAj3LRM/vF7ETcHKiLJ/u5CnNgbz/eYj/vB3xGAYtRxI5IKtq0hpe5iFHD22BKV3n6WHUu0k4Q==",
"version": "8.2.5",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.5.tgz",
"integrity": "sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg==",
"peer": true,
"requires": {
"@types/through": "*"
@@ -17917,6 +17927,14 @@
"utils-merge": "^1.0.1"
}
},
"passport-http-header-strategy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz",
"integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==",
"requires": {
"passport-strategy": "^1.0.0"
}
},
"passport-jwt": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
+1
View File
@@ -70,6 +70,7 @@
"nest-commander": "^3.3.0",
"openid-client": "^5.2.1",
"passport": "^0.6.0",
"passport-http-header-strategy": "^1.1.0",
"passport-jwt": "^4.0.0",
"pg": "^8.7.1",
"redis": "^3.1.2",