mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-08 04:09:07 +00:00
refactor(server): api keys (#1339)
* refactor: api keys * refactor: test module * chore: tests * chore: fix provider * refactor: test mock repos
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import { APIKeyEntity } from '@app/infra';
|
||||
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 {}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { APIKeyEntity } from '@app/infra';
|
||||
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' } });
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { UserEntity } from '@app/infra';
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class APIKeyCreateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class APIKeyUpdateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { APIKeyResponseDto } from './api-key-response.dto';
|
||||
|
||||
export class APIKeyCreateResponseDto {
|
||||
secret!: string;
|
||||
apiKey!: APIKeyResponseDto;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { APIKeyEntity } from '@app/infra';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class APIKeyResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { immichAppConfig, immichBullAsyncConfig } from '@app/common/config';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { AssetModule } from './api-v1/asset/asset.module';
|
||||
import { AuthModule } from './api-v1/auth/auth.module';
|
||||
import { APIKeyModule } from './api-v1/api-key/api-key.module';
|
||||
import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
|
||||
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
@@ -22,7 +21,7 @@ import { ImmichConfigModule } from '@app/immich-config';
|
||||
import { ShareModule } from './api-v1/share/share.module';
|
||||
import { DomainModule } from '@app/domain';
|
||||
import { InfraModule } from '@app/infra';
|
||||
import { UserController } from './controllers';
|
||||
import { APIKeyController, UserController } from './controllers';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,8 +31,6 @@ import { UserController } from './controllers';
|
||||
imports: [InfraModule],
|
||||
}),
|
||||
|
||||
APIKeyModule,
|
||||
|
||||
AssetModule,
|
||||
|
||||
AuthModule,
|
||||
@@ -69,6 +66,7 @@ import { UserController } from './controllers';
|
||||
controllers: [
|
||||
//
|
||||
AppController,
|
||||
APIKeyController,
|
||||
UserController,
|
||||
],
|
||||
providers: [],
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
APIKeyCreateDto,
|
||||
APIKeyCreateResponseDto,
|
||||
APIKeyResponseDto,
|
||||
APIKeyService,
|
||||
APIKeyUpdateDto,
|
||||
AuthUserDto,
|
||||
} from '@app/domain';
|
||||
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';
|
||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
|
||||
@ApiTags('API Key')
|
||||
@Controller('api-key')
|
||||
@@ -1 +1,2 @@
|
||||
export * from './api-key.controller';
|
||||
export * from './user.controller';
|
||||
|
||||
@@ -3,13 +3,12 @@ import { ImmichJwtService } from './immich-jwt.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { jwtConfig } from '../../config/jwt.config';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { APIKeyModule } from '../../api-v1/api-key/api-key.module';
|
||||
import { APIKeyStrategy } from './strategies/api-key.strategy';
|
||||
import { ShareModule } from '../../api-v1/share/share.module';
|
||||
import { PublicShareStrategy } from './strategies/public-share.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [JwtModule.register(jwtConfig), APIKeyModule, ShareModule],
|
||||
imports: [JwtModule.register(jwtConfig), ShareModule],
|
||||
providers: [ImmichJwtService, JwtStrategy, APIKeyStrategy, PublicShareStrategy],
|
||||
exports: [ImmichJwtService],
|
||||
})
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { APIKeyService, AuthUserDto } from '@app/domain';
|
||||
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';
|
||||
import { AuthUserDto } from '../../../decorators/auth-user.decorator';
|
||||
|
||||
export const API_KEY_STRATEGY = 'api-key';
|
||||
|
||||
@@ -16,16 +15,7 @@ export class APIKeyStrategy extends PassportStrategy(Strategy, API_KEY_STRATEGY)
|
||||
super(options);
|
||||
}
|
||||
|
||||
async validate(token: string): Promise<AuthUserDto> {
|
||||
const user = await this.apiKeyService.validate(token);
|
||||
|
||||
const authUser = new AuthUserDto();
|
||||
authUser.id = user.id;
|
||||
authUser.email = user.email;
|
||||
authUser.isAdmin = user.isAdmin;
|
||||
authUser.isPublicUser = false;
|
||||
authUser.isAllowUpload = true;
|
||||
|
||||
return authUser;
|
||||
validate(token: string): Promise<AuthUserDto> {
|
||||
return this.apiKeyService.validate(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,6 @@
|
||||
"declaration": false,
|
||||
"outDir": "../../dist/apps/immich"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../libs/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"test",
|
||||
"**/*spec.ts"
|
||||
]
|
||||
}
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user