From aad47ce45a0d8deb89a64f0d353137d63a830eb0 Mon Sep 17 00:00:00 2001 From: izzy Date: Fri, 14 Nov 2025 15:49:12 +0000 Subject: [PATCH] refactor: use `action` property for setting maint. mode --- e2e/src/api/specs/maintenance.e2e-spec.ts | 12 +-- mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + mobile/openapi/lib/api_helper.dart | 3 + .../openapi/lib/model/maintenance_action.dart | 85 +++++++++++++++++++ .../lib/model/set_maintenance_mode_dto.dart | 16 ++-- open-api/immich-openapi-specs.json | 17 +++- open-api/typescript-sdk/src/fetch-client.ts | 6 +- .../src/controllers/maintenance.controller.ts | 4 +- server/src/dtos/maintenance.dto.ts | 7 +- server/src/enum.ts | 5 ++ .../maintenance-worker.controller.ts | 4 +- .../admin-settings/MaintenanceSettings.svelte | 4 +- web/src/routes/maintenance/+page.svelte | 4 +- 15 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 mobile/openapi/lib/model/maintenance_action.dart diff --git a/e2e/src/api/specs/maintenance.e2e-spec.ts b/e2e/src/api/specs/maintenance.e2e-spec.ts index 0b9569a791..68b3e1b238 100644 --- a/e2e/src/api/specs/maintenance.e2e-spec.ts +++ b/e2e/src/api/specs/maintenance.e2e-spec.ts @@ -39,7 +39,7 @@ describe('/admin/maintenance', () => { describe.sequential('POST /', () => { it('should require authentication', async () => { const { status, body } = await request(app).post('/admin/maintenance').send({ - maintenanceMode: false, + action: 'end', }); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); @@ -49,7 +49,7 @@ describe('/admin/maintenance', () => { const { status, body } = await request(app) .post('/admin/maintenance') .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send({ maintenanceMode: false }); + .send({ action: 'end' }); expect(status).toBe(403); expect(body).toEqual(errorDto.forbidden); }); @@ -58,7 +58,7 @@ describe('/admin/maintenance', () => { const { status } = await request(app) .post('/admin/maintenance') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ maintenanceMode: false }); + .send({ action: 'end' }); expect(status).toBe(201); }); @@ -67,7 +67,7 @@ describe('/admin/maintenance', () => { .post('/admin/maintenance') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ - maintenanceMode: true, + action: 'start', }); expect(status).toBe(201); @@ -139,7 +139,7 @@ describe('/admin/maintenance', () => { const { status } = await request(app) .post('/admin/maintenance') .set('cookie', cookie!) - .send({ maintenanceMode: true }); + .send({ action: 'start' }); expect(status).toBe(201); }); }); @@ -151,7 +151,7 @@ describe('/admin/maintenance', () => { it('should exit maintenance mode', async () => { console.info('using', cookie!); const { status } = await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ - maintenanceMode: false, + action: 'end', }); expect(status).toBe(201); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2edf495df9..3a6eb9385e 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -405,6 +405,7 @@ Class | Method | HTTP request | Description - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) - [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md) + - [MaintenanceAction](doc//MaintenanceAction.md) - [MaintenanceAuthDto](doc//MaintenanceAuthDto.md) - [MaintenanceLoginDto](doc//MaintenanceLoginDto.md) - [ManualJobName](doc//ManualJobName.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a26ac0d079..23906fc2d7 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -168,6 +168,7 @@ part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; part 'model/machine_learning_availability_checks_dto.dart'; +part 'model/maintenance_action.dart'; part 'model/maintenance_auth_dto.dart'; part 'model/maintenance_login_dto.dart'; part 'model/manual_job_name.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index b380dbb141..63698415ea 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -390,6 +390,8 @@ class ApiClient { return LogoutResponseDto.fromJson(value); case 'MachineLearningAvailabilityChecksDto': return MachineLearningAvailabilityChecksDto.fromJson(value); + case 'MaintenanceAction': + return MaintenanceActionTypeTransformer().decode(value); case 'MaintenanceAuthDto': return MaintenanceAuthDto.fromJson(value); case 'MaintenanceLoginDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 1d197a8f91..9abd14c8ea 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -103,6 +103,9 @@ String parameterToString(dynamic value) { if (value is LogLevel) { return LogLevelTypeTransformer().encode(value).toString(); } + if (value is MaintenanceAction) { + return MaintenanceActionTypeTransformer().encode(value).toString(); + } if (value is ManualJobName) { return ManualJobNameTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/maintenance_action.dart b/mobile/openapi/lib/model/maintenance_action.dart new file mode 100644 index 0000000000..9be628961f --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_action.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class MaintenanceAction { + /// Instantiate a new enum with the provided [value]. + const MaintenanceAction._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const start = MaintenanceAction._(r'start'); + static const end = MaintenanceAction._(r'end'); + + /// List of all possible values in this [enum][MaintenanceAction]. + static const values = [ + start, + end, + ]; + + static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MaintenanceAction.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MaintenanceAction] to String, +/// and [decode] dynamic data back to [MaintenanceAction]. +class MaintenanceActionTypeTransformer { + factory MaintenanceActionTypeTransformer() => _instance ??= const MaintenanceActionTypeTransformer._(); + + const MaintenanceActionTypeTransformer._(); + + String encode(MaintenanceAction data) => data.value; + + /// Decodes a [dynamic value][data] to a MaintenanceAction. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MaintenanceAction? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'start': return MaintenanceAction.start; + case r'end': return MaintenanceAction.end; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MaintenanceActionTypeTransformer] instance. + static MaintenanceActionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index ce18b80d97..c724337529 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -13,26 +13,26 @@ part of openapi.api; class SetMaintenanceModeDto { /// Returns a new [SetMaintenanceModeDto] instance. SetMaintenanceModeDto({ - required this.maintenanceMode, + required this.action, }); - bool maintenanceMode; + MaintenanceAction action; @override bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto && - other.maintenanceMode == maintenanceMode; + other.action == action; @override int get hashCode => // ignore: unnecessary_parenthesis - (maintenanceMode.hashCode); + (action.hashCode); @override - String toString() => 'SetMaintenanceModeDto[maintenanceMode=$maintenanceMode]'; + String toString() => 'SetMaintenanceModeDto[action=$action]'; Map toJson() { final json = {}; - json[r'maintenanceMode'] = this.maintenanceMode; + json[r'action'] = this.action; return json; } @@ -45,7 +45,7 @@ class SetMaintenanceModeDto { final json = value.cast(); return SetMaintenanceModeDto( - maintenanceMode: mapValueOfType(json, r'maintenanceMode')!, + action: MaintenanceAction.fromJson(json[r'action'])!, ); } return null; @@ -93,7 +93,7 @@ class SetMaintenanceModeDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'maintenanceMode', + 'action', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b3992d1be2..1b592df6e8 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -16306,6 +16306,13 @@ ], "type": "object" }, + "MaintenanceAction": { + "enum": [ + "start", + "end" + ], + "type": "string" + }, "MaintenanceAuthDto": { "properties": { "username": { @@ -18578,12 +18585,16 @@ }, "SetMaintenanceModeDto": { "properties": { - "maintenanceMode": { - "type": "boolean" + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/MaintenanceAction" + } + ] } }, "required": [ - "maintenanceMode" + "action" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 1ad417ed7b..61e60d4470 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -41,7 +41,7 @@ export type ActivityStatisticsResponseDto = { likes: number; }; export type SetMaintenanceModeDto = { - maintenanceMode: boolean; + action: MaintenanceAction; }; export type MaintenanceLoginDto = { token?: string; @@ -4881,6 +4881,10 @@ export enum UserAvatarColor { Gray = "gray", Amber = "amber" } +export enum MaintenanceAction { + Start = "start", + End = "end" +} export enum NotificationLevel { Success = "success", Error = "error", diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index 8adb11eaa9..1d97f3552a 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -4,7 +4,7 @@ import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; -import { ApiTag, ImmichCookie, Permission } from 'src/enum'; +import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MaintenanceService } from 'src/services/maintenance.service'; import { respondWithCookie } from 'src/utils/response'; @@ -36,7 +36,7 @@ export class MaintenanceController { @Body() dto: SetMaintenanceModeDto, @Res({ passthrough: true }) res: Response, ): Promise { - if (dto.maintenanceMode) { + if (dto.action === MaintenanceAction.Start) { const { jwt } = await this.service.startMaintenance(auth.user.name); return respondWithCookie(res, undefined, { isSecure: false, diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index ab8a172a24..fe6960c0a4 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,8 +1,9 @@ -import { ValidateBoolean, ValidateString } from 'src/validation'; +import { MaintenanceAction } from 'src/enum'; +import { ValidateEnum, ValidateString } from 'src/validation'; export class SetMaintenanceModeDto { - @ValidateBoolean() - maintenanceMode!: boolean; + @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction' }) + action!: MaintenanceAction; } export class MaintenanceLoginDto { diff --git a/server/src/enum.ts b/server/src/enum.ts index cfd5d6bfac..9361dad65a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -646,6 +646,11 @@ export enum DatabaseLock { MemoryCreation = 777, } +export enum MaintenanceAction { + Start = 'start', + End = 'end', +} + export enum ExitCode { AppRestart = 7, } diff --git a/server/src/maintenance/maintenance-worker.controller.ts b/server/src/maintenance/maintenance-worker.controller.ts index 3ca93ef49a..f49ba6d0af 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common'; import { Request, Response } from 'express'; import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { ServerConfigDto } from 'src/dtos/server.dto'; -import { ImmichCookie } from 'src/enum'; +import { ImmichCookie, MaintenanceAction } from 'src/enum'; import { MaintenanceRoute } from 'src/maintenance/maintenance-auth.guard'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; import { respondWithCookie } from 'src/utils/response'; @@ -33,7 +33,7 @@ export class MaintenanceWorkerController { @Post('admin/maintenance') @MaintenanceRoute() async setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): Promise { - if (!dto.maintenanceMode) { + if (dto.action === MaintenanceAction.End) { await this.service.endMaintenance(); } } diff --git a/web/src/lib/components/admin-settings/MaintenanceSettings.svelte b/web/src/lib/components/admin-settings/MaintenanceSettings.svelte index a5181c550b..592091c62a 100644 --- a/web/src/lib/components/admin-settings/MaintenanceSettings.svelte +++ b/web/src/lib/components/admin-settings/MaintenanceSettings.svelte @@ -1,6 +1,6 @@