mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-05-09 06:55:47 +00:00
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:
48
server/apps/immich/src/api-v1/api-key/api-key.controller.ts
Normal file
48
server/apps/immich/src/api-v1/api-key/api-key.controller.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
16
server/apps/immich/src/api-v1/api-key/api-key.module.ts
Normal file
16
server/apps/immich/src/api-v1/api-key/api-key.module.ts
Normal file
@@ -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 {}
|
||||
59
server/apps/immich/src/api-v1/api-key/api-key.repository.ts
Normal file
59
server/apps/immich/src/api-v1/api-key/api-key.repository.ts
Normal file
@@ -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' } });
|
||||
}
|
||||
}
|
||||
74
server/apps/immich/src/api-v1/api-key/api-key.service.ts
Normal file
74
server/apps/immich/src/api-v1/api-key/api-key.service.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
26
server/libs/database/src/entities/api-key.entity.ts
Normal file
26
server/libs/database/src/entities/api-key.entity.ts
Normal file
@@ -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"`);
|
||||
}
|
||||
|
||||
}
|
||||
30
server/package-lock.json
generated
30
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user