mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02: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:
parent
9edbff0ec0
commit
9e6d6b2532
15
mobile/openapi/.openapi-generator/FILES
generated
15
mobile/openapi/.openapi-generator/FILES
generated
@ -3,6 +3,11 @@
|
||||
.travis.yml
|
||||
README.md
|
||||
analysis_options.yaml
|
||||
doc/APIKeyApi.md
|
||||
doc/APIKeyCreateDto.md
|
||||
doc/APIKeyCreateResponseDto.md
|
||||
doc/APIKeyResponseDto.md
|
||||
doc/APIKeyUpdateDto.md
|
||||
doc/AddAssetsDto.md
|
||||
doc/AddAssetsResponseDto.md
|
||||
doc/AddUsersDto.md
|
||||
@ -85,6 +90,7 @@ doc/ValidateAccessTokenResponseDto.md
|
||||
git_push.sh
|
||||
lib/api.dart
|
||||
lib/api/album_api.dart
|
||||
lib/api/api_key_api.dart
|
||||
lib/api/asset_api.dart
|
||||
lib/api/authentication_api.dart
|
||||
lib/api/device_info_api.dart
|
||||
@ -109,6 +115,10 @@ lib/model/admin_signup_response_dto.dart
|
||||
lib/model/album_count_response_dto.dart
|
||||
lib/model/album_response_dto.dart
|
||||
lib/model/all_job_status_response_dto.dart
|
||||
lib/model/api_key_create_dto.dart
|
||||
lib/model/api_key_create_response_dto.dart
|
||||
lib/model/api_key_response_dto.dart
|
||||
lib/model/api_key_update_dto.dart
|
||||
lib/model/asset_count_by_time_bucket.dart
|
||||
lib/model/asset_count_by_time_bucket_response_dto.dart
|
||||
lib/model/asset_count_by_user_id_response_dto.dart
|
||||
@ -180,6 +190,11 @@ test/album_api_test.dart
|
||||
test/album_count_response_dto_test.dart
|
||||
test/album_response_dto_test.dart
|
||||
test/all_job_status_response_dto_test.dart
|
||||
test/api_key_api_test.dart
|
||||
test/api_key_create_dto_test.dart
|
||||
test/api_key_create_response_dto_test.dart
|
||||
test/api_key_response_dto_test.dart
|
||||
test/api_key_update_dto_test.dart
|
||||
test/asset_api_test.dart
|
||||
test/asset_count_by_time_bucket_response_dto_test.dart
|
||||
test/asset_count_by_time_bucket_test.dart
|
||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/APIKeyApi.md
generated
Normal file
BIN
mobile/openapi/doc/APIKeyApi.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/APIKeyCreateDto.md
generated
Normal file
BIN
mobile/openapi/doc/APIKeyCreateDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/APIKeyCreateResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/APIKeyCreateResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/APIKeyResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/APIKeyResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/APIKeyUpdateDto.md
generated
Normal file
BIN
mobile/openapi/doc/APIKeyUpdateDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/api_key_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/api_key_api.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/api_key_create_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/api_key_create_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/api_key_create_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/api_key_create_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/api_key_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/api_key_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/api_key_update_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/api_key_update_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/api_key_api_test.dart
generated
Normal file
BIN
mobile/openapi/test/api_key_api_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/api_key_create_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/api_key_create_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/api_key_create_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/api_key_create_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/api_key_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/api_key_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/api_key_update_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/api_key_update_dto_test.dart
generated
Normal file
Binary file not shown.
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",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { env } from '$env/dynamic/public';
|
||||
import {
|
||||
AlbumApi,
|
||||
APIKeyApi,
|
||||
AssetApi,
|
||||
AuthenticationApi,
|
||||
Configuration,
|
||||
@ -21,6 +22,7 @@ class ImmichApi {
|
||||
public deviceInfoApi: DeviceInfoApi;
|
||||
public serverInfoApi: ServerInfoApi;
|
||||
public jobApi: JobApi;
|
||||
public keyApi: APIKeyApi;
|
||||
public systemConfigApi: SystemConfigApi;
|
||||
|
||||
private config = new Configuration({ basePath: '/api' });
|
||||
@ -34,6 +36,7 @@ class ImmichApi {
|
||||
this.deviceInfoApi = new DeviceInfoApi(this.config);
|
||||
this.serverInfoApi = new ServerInfoApi(this.config);
|
||||
this.jobApi = new JobApi(this.config);
|
||||
this.keyApi = new APIKeyApi(this.config);
|
||||
this.systemConfigApi = new SystemConfigApi(this.config);
|
||||
}
|
||||
|
||||
|
433
web/src/api/open-api/api.ts
generated
433
web/src/api/open-api/api.ts
generated
@ -21,6 +21,82 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface APIKeyCreateDto
|
||||
*/
|
||||
export interface APIKeyCreateDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof APIKeyCreateDto
|
||||
*/
|
||||
'name'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface APIKeyCreateResponseDto
|
||||
*/
|
||||
export interface APIKeyCreateResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof APIKeyCreateResponseDto
|
||||
*/
|
||||
'secret': string;
|
||||
/**
|
||||
*
|
||||
* @type {APIKeyResponseDto}
|
||||
* @memberof APIKeyCreateResponseDto
|
||||
*/
|
||||
'apiKey': APIKeyResponseDto;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface APIKeyResponseDto
|
||||
*/
|
||||
export interface APIKeyResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof APIKeyResponseDto
|
||||
*/
|
||||
'id': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof APIKeyResponseDto
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof APIKeyResponseDto
|
||||
*/
|
||||
'createdAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof APIKeyResponseDto
|
||||
*/
|
||||
'updatedAt': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface APIKeyUpdateDto
|
||||
*/
|
||||
export interface APIKeyUpdateDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof APIKeyUpdateDto
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -1990,6 +2066,363 @@ export interface ValidateAccessTokenResponseDto {
|
||||
'authStatus': boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* APIKeyApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const APIKeyApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {APIKeyCreateDto} aPIKeyCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createKey: async (aPIKeyCreateDto: APIKeyCreateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'aPIKeyCreateDto' is not null or undefined
|
||||
assertParamExists('createKey', 'aPIKeyCreateDto', aPIKeyCreateDto)
|
||||
const localVarPath = `/api-key`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(aPIKeyCreateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteKey: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('deleteKey', 'id', id)
|
||||
const localVarPath = `/api-key/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getKey: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getKey', 'id', id)
|
||||
const localVarPath = `/api-key/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getKeys: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api-key`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updateKey: async (id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('updateKey', 'id', id)
|
||||
// verify required parameter 'aPIKeyUpdateDto' is not null or undefined
|
||||
assertParamExists('updateKey', 'aPIKeyUpdateDto', aPIKeyUpdateDto)
|
||||
const localVarPath = `/api-key/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(aPIKeyUpdateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* APIKeyApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const APIKeyApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = APIKeyApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {APIKeyCreateDto} aPIKeyCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createKey(aPIKeyCreateDto: APIKeyCreateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyCreateResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createKey(aPIKeyCreateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteKey(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getKey(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getKey(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getKeys(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<APIKeyResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getKeys(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<APIKeyResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updateKey(id, aPIKeyUpdateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* APIKeyApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const APIKeyApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = APIKeyApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {APIKeyCreateDto} aPIKeyCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createKey(aPIKeyCreateDto: APIKeyCreateDto, options?: any): AxiosPromise<APIKeyCreateResponseDto> {
|
||||
return localVarFp.createKey(aPIKeyCreateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteKey(id: number, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.deleteKey(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getKey(id: number, options?: any): AxiosPromise<APIKeyResponseDto> {
|
||||
return localVarFp.getKey(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getKeys(options?: any): AxiosPromise<Array<APIKeyResponseDto>> {
|
||||
return localVarFp.getKeys(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: any): AxiosPromise<APIKeyResponseDto> {
|
||||
return localVarFp.updateKey(id, aPIKeyUpdateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* APIKeyApi - object-oriented interface
|
||||
* @export
|
||||
* @class APIKeyApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class APIKeyApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {APIKeyCreateDto} aPIKeyCreateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof APIKeyApi
|
||||
*/
|
||||
public createKey(aPIKeyCreateDto: APIKeyCreateDto, options?: AxiosRequestConfig) {
|
||||
return APIKeyApiFp(this.configuration).createKey(aPIKeyCreateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof APIKeyApi
|
||||
*/
|
||||
public deleteKey(id: number, options?: AxiosRequestConfig) {
|
||||
return APIKeyApiFp(this.configuration).deleteKey(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof APIKeyApi
|
||||
*/
|
||||
public getKey(id: number, options?: AxiosRequestConfig) {
|
||||
return APIKeyApiFp(this.configuration).getKey(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof APIKeyApi
|
||||
*/
|
||||
public getKeys(options?: AxiosRequestConfig) {
|
||||
return APIKeyApiFp(this.configuration).getKeys(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} id
|
||||
* @param {APIKeyUpdateDto} aPIKeyUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof APIKeyApi
|
||||
*/
|
||||
public updateKey(id: number, aPIKeyUpdateDto: APIKeyUpdateDto, options?: AxiosRequestConfig) {
|
||||
return APIKeyApiFp(this.configuration).updateKey(id, aPIKeyUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AlbumApi - axios parameter creator
|
||||
* @export
|
||||
|
57
web/src/lib/components/forms/api-key-form.svelte
Normal file
57
web/src/lib/components/forms/api-key-form.svelte
Normal file
@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { APIKeyResponseDto } from '@api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
|
||||
export let apiKey: Partial<APIKeyResponseDto>;
|
||||
export let title = 'API Key';
|
||||
export let cancelText = 'Cancel';
|
||||
export let submitText = 'Save';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const handleCancel = () => dispatch('cancel');
|
||||
const handleSubmit = () => dispatch('submit', { ...apiKey, name: apiKey.name });
|
||||
</script>
|
||||
|
||||
<FullScreenModal on:clickOutside={() => handleCancel()}>
|
||||
<div
|
||||
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<KeyVariant size="4em" />
|
||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="email">Name</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={apiKey.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full px-4 gap-4 mt-8">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => handleCancel()}
|
||||
class="flex-1 transition-colors bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-6 py-3 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium"
|
||||
>{cancelText}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||
>{submitText}</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</FullScreenModal>
|
69
web/src/lib/components/forms/api-key-secret.svelte
Normal file
69
web/src/lib/components/forms/api-key-secret.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import KeyVariant from 'svelte-material-icons/KeyVariant.svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let secret = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const handleDone = () => dispatch('done');
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(secret);
|
||||
notificationController.show({
|
||||
message: 'Copied to clipboard!',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to copy to clipboard');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal>
|
||||
<div
|
||||
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<KeyVariant size="4em" />
|
||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||
API Key
|
||||
</h1>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
This value will only be shown once. Please be sure to copy it before closing the window.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<!-- <label class="immich-form-label" for="email">API Key</label> -->
|
||||
<textarea
|
||||
class="immich-form-input"
|
||||
id="secret"
|
||||
name="secret"
|
||||
readonly={true}
|
||||
value={secret}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full px-4 gap-4 mt-8">
|
||||
<button
|
||||
on:click={() => handleCopy()}
|
||||
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||
>Copy to Clipboard</button
|
||||
>
|
||||
<button
|
||||
on:click={() => handleDone()}
|
||||
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||
>Done</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
|
||||
export let title = 'Confirm Delete';
|
||||
export let prompt = 'Are you sure you want to delete this item?';
|
||||
export let confirmText = 'Confirm';
|
||||
export let cancelText = 'Cancel';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const handleCancel = () => dispatch('cancel');
|
||||
const handleConfirm = () => dispatch('confirm');
|
||||
</script>
|
||||
|
||||
<FullScreenModal on:clickOutside={() => handleCancel()}>
|
||||
<div
|
||||
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] rounded-3xl py-8 dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<p class="ml-4 text-md py-5 text-center">{prompt}</p>
|
||||
|
||||
<div class="flex w-full px-4 gap-4 mt-4">
|
||||
<button
|
||||
on:click={() => handleCancel()}
|
||||
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
on:click={() => handleConfirm()}
|
||||
class="flex-1 transition-colors bg-red-500 hover:bg-red-400 px-6 py-3 text-white rounded-full w-full font-medium"
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { api, APIKeyResponseDto } from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import APIKeyForm from '../forms/api-key-form.svelte';
|
||||
import APIKeySecret from '../forms/api-key-secret.svelte';
|
||||
import DeleteConfirmDialogue from '../shared-components/delete-confirm-dialogue.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
let keys: APIKeyResponseDto[] = [];
|
||||
|
||||
let newKey: Partial<APIKeyResponseDto> | null = null;
|
||||
let editKey: APIKeyResponseDto | null = null;
|
||||
let deleteKey: APIKeyResponseDto | null = null;
|
||||
let secret = '';
|
||||
|
||||
const locale = navigator.language;
|
||||
const format: Intl.DateTimeFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
refreshKeys();
|
||||
});
|
||||
|
||||
async function refreshKeys() {
|
||||
const { data } = await api.keyApi.getKeys();
|
||||
keys = data;
|
||||
}
|
||||
|
||||
const handleCreate = async (event: CustomEvent<APIKeyResponseDto>) => {
|
||||
try {
|
||||
const dto = event.detail;
|
||||
const { data } = await api.keyApi.createKey(dto);
|
||||
secret = data.secret;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to create a new API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
newKey = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (event: CustomEvent<APIKeyResponseDto>) => {
|
||||
if (!editKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dto = event.detail;
|
||||
|
||||
try {
|
||||
await api.keyApi.updateKey(editKey.id, { name: dto.name });
|
||||
notificationController.show({
|
||||
message: `Saved API Key`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
editKey = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.keyApi.deleteKey(deleteKey.id);
|
||||
notificationController.show({
|
||||
message: `Removed API Key: ${deleteKey.name}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove API Key');
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
deleteKey = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if newKey}
|
||||
<APIKeyForm
|
||||
title="New API Key"
|
||||
submitText="Create"
|
||||
apiKey={newKey}
|
||||
on:submit={handleCreate}
|
||||
on:cancel={() => (newKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if secret}
|
||||
<APIKeySecret {secret} on:done={() => (secret = '')} />
|
||||
{/if}
|
||||
|
||||
{#if editKey}
|
||||
<APIKeyForm
|
||||
submitText="Save"
|
||||
apiKey={editKey}
|
||||
on:submit={handleUpdate}
|
||||
on:cancel={() => (editKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if deleteKey}
|
||||
<DeleteConfirmDialogue
|
||||
prompt="Are you sure you want to delete this API Key?"
|
||||
on:confirm={() => handleDelete()}
|
||||
on:cancel={() => (deleteKey = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<section class="my-4">
|
||||
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
||||
<div class="flex justify-end mb-2">
|
||||
<button
|
||||
on:click={() => (newKey = { name: 'API Key' })}
|
||||
class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>New API Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if keys.length > 0}
|
||||
<table class="text-left w-full">
|
||||
<thead
|
||||
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/3 font-medium text-sm">Name</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Created</th>
|
||||
<th class="text-center w-1/3 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="overflow-y-auto rounded-md w-full block border dark:border-immich-dark-gray">
|
||||
{#each keys as key, i}
|
||||
{#key key.id}
|
||||
<tr
|
||||
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
|
||||
i % 2 == 0
|
||||
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">{key.name}</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis"
|
||||
>{new Date(key.createdAt).toLocaleDateString(locale, format)}
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/3 text-ellipsis">
|
||||
<button
|
||||
on:click={() => (editKey = key)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<PencilOutline size="16" />
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (deleteKey = key)}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
|
||||
>
|
||||
<TrashCanOutline size="16" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
@ -5,6 +5,7 @@
|
||||
import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
|
||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||
import OAuthSettings from './oauth-settings.svelte';
|
||||
import UserAPIKeyList from './user-api-key-list.svelte';
|
||||
import UserProfileSettings from './user-profile-settings.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
@ -32,6 +33,10 @@
|
||||
<ChangePasswordSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="API Keys" subtitle="View and manage your API keys">
|
||||
<UserAPIKeyList />
|
||||
</SettingAccordion>
|
||||
|
||||
{#if oauthEnabled}
|
||||
<SettingAccordion
|
||||
title="OAuth"
|
||||
|
Loading…
Reference in New Issue
Block a user