diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 3da7cbe107..e522854059 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -37,15 +37,15 @@ jobs: - uses: actions/setup-java@v4 with: - distribution: "zulu" - java-version: "11.0.21+9" - cache: "gradle" + distribution: 'zulu' + java-version: '17' + cache: 'gradle' - name: Setup Flutter SDK uses: subosito/flutter-action@v2 with: - channel: "stable" - flutter-version: "3.19.3" + channel: 'stable' + flutter-version: '3.19.3' cache: true - name: Create the Keystore diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d74c961393..51b947418e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -208,7 +208,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.16.9' + flutter-version: '3.19.3' - name: Run tests working-directory: ./mobile run: flutter test -j 1 diff --git a/cli/package-lock.json b/cli/package-lock.json index 11154fec52..5075514973 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -47,15 +47,15 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5" + "@types/node": "^20.11.0", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index a890d674bc..256f3619f1 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -120,7 +120,8 @@ The default configuration looks like this: "previewFormat": "jpeg", "previewSize": 1440, "quality": 80, - "colorspace": "p3" + "colorspace": "p3", + "extractEmbedded": false }, "newVersionCheck": { "enabled": true diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 6cd8dd90ec..4643b8b01f 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -81,15 +81,15 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5" + "@types/node": "^20.11.0", + "typescript": "^5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/e2e/package.json b/e2e/package.json index 9023de8162..34ef229a2a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.101.0", + "version": "1.102.3", "description": "", "main": "index.js", "type": "module", diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index 4a6e1a773a..9174128bb8 100644 --- a/e2e/src/api/specs/auth.e2e-spec.ts +++ b/e2e/src/api/specs/auth.e2e-spec.ts @@ -112,9 +112,29 @@ describe('/auth/*', () => { const cookies = headers['set-cookie']; expect(cookies).toHaveLength(3); - expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`); - expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'); - expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'); + expect(cookies[0].split(';').map((item) => item.trim())).toEqual([ + `immich_access_token=${token}`, + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[1].split(';').map((item) => item.trim())).toEqual([ + 'immich_auth_type=password', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'HttpOnly', + 'SameSite=Lax', + ]); + expect(cookies[2].split(';').map((item) => item.trim())).toEqual([ + 'immich_is_authenticated=true', + 'Max-Age=34560000', + 'Path=/', + expect.stringContaining('Expires='), + 'SameSite=Lax', + ]); }); }); diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 5cfd6a8b98..690bfae744 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { LoginResponseDto } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, utils } from 'src/utils'; @@ -162,19 +162,4 @@ describe('/server-info', () => { }); }); }); - - describe('POST /server-info/admin-onboarding', () => { - it('should set admin onboarding', async () => { - const config = await getServerConfig({}); - expect(config.isOnboarded).toBe(false); - - const { status } = await request(app) - .post('/server-info/admin-onboarding') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(204); - - const newConfig = await getServerConfig({}); - expect(newConfig.isOnboarded).toBe(true); - }); - }); }); diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/api/specs/system-metadata.e2e-spec.ts new file mode 100644 index 0000000000..bd17bf2524 --- /dev/null +++ b/e2e/src/api/specs/system-metadata.e2e-spec.ts @@ -0,0 +1,76 @@ +import { LoginResponseDto, getServerConfig } from '@immich/sdk'; +import { createUserDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/server-info', () => { + let admin: LoginResponseDto; + let nonAdmin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + }); + + describe('POST /system-metadata/admin-onboarding', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should set admin onboarding', async () => { + const config = await getServerConfig({}); + expect(config.isOnboarded).toBe(false); + + const { status } = await request(app) + .post('/system-metadata/admin-onboarding') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ isOnboarded: true }); + expect(status).toBe(204); + + const newConfig = await getServerConfig({}); + expect(newConfig.isOnboarded).toBe(true); + }); + }); + + describe('GET /system-metadata/reverse-geocoding-state', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should only work for admins', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${nonAdmin.accessToken}`); + expect(status).toBe(403); + expect(body).toEqual(errorDto.forbidden); + }); + + it('should get the reverse geocoding state', async () => { + const { status, body } = await request(app) + .get('/system-metadata/reverse-geocoding-state') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + lastUpdate: expect.any(String), + lastImportFileName: 'cities500.txt', + }); + }); + }); +}); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 14ffbea745..ee4dad654d 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -24,8 +24,8 @@ import { getConfigDefaults, login, searchMetadata, - setAdminOnboarding, signUpAdmin, + updateAdminOnboarding, updateAlbumUser, updateConfig, validate, @@ -265,7 +265,10 @@ export const utils = { await signUpAdmin({ signUpDto: signupDto.admin }); const response = await login({ loginCredentialDto: loginDto.admin }); if (options.onboarding) { - await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) }); + await updateAdminOnboarding( + { adminOnboardingUpdateDto: { isOnboarded: true } }, + { headers: asBearerAuth(response.accessToken) }, + ); } return response; }, diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index e5d8e06d43..76f51d964e 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.101.0" +version = "1.102.3" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 96d2db23f5..85d6206e1f 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,14 +1,14 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id "kotlin-kapt" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + localPropertiesFile.withInputStream { localProperties.load(it) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') @@ -21,18 +21,12 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } - android { compileSdkVersion 34 @@ -50,7 +44,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "app.alextran.immich" minSdkVersion 26 targetSdkVersion 33 @@ -88,6 +81,13 @@ flutter { } dependencies { + def kotlin_version = '1.9.23' + def kotlin_coroutines_version = '1.8.0' + def work_version = '2.9.0' + def concurrent_version = '1.1.0' + def guava_version = '33.1.0-android' + def glide_version = '4.16.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.work:work-runtime-ktx:$work_version" diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index 660e1d55ba..b6b78c2cba 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -276,7 +276,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_ERROR_ID = 2 private const val NOTIFICATION_DETAIL_ID = 3 private const val ONE_MINUTE = 60000L @@ -304,7 +304,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) if (workInfoList != null) { for (workInfo in workInfoList) { - if (workInfo.getState() == WorkInfo.State.ENQUEUED) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { val workRequest = buildWorkRequest(requireWifi, requireCharging) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") @@ -346,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct .setRequiresBatteryNotLow(true) .setRequiresCharging(requireCharging) .build(); - + val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) @@ -359,4 +359,4 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } } -private const val TAG = "BackupWorker" \ No newline at end of file +private const val TAG = "BackupWorker" diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 4dacde5a9d..5e374c9f64 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -1,21 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.20' - ext.kotlin_coroutines_version = '1.7.1' - ext.work_version = '2.7.1' - ext.concurrent_version = '1.1.0' - ext.guava_version = '33.0.0-android' - ext.glide_version = '4.14.2' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -34,3 +16,7 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +tasks.named('wrapper') { + distributionType = Wrapper.DistributionType.ALL +} \ No newline at end of file diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 65307abde8..94a9a7e0bd 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 131, - "android.injected.version.name" => "1.101.0", + "android.injected.version.code" => 136, + "android.injected.version.name" => "1.102.3", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index d39c4a373f..358fb9618c 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index 7787882b74..6357330c9e 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip -distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80 \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip +distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12 \ No newline at end of file diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 44e62bcf06..7ea6533b65 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.4.2" apply false + id "org.jetbrains.kotlin.android" version "1.9.23" apply false + id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false +} + +include ":app" diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 1a6ca76757..46155d0c53 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -296,6 +296,7 @@ "motion_photos_page_title": "Motion Photos", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "no_assets_to_show" : "No assets to show", "notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Settings", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index eaa4fc06ed..f9ce46c4d5 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -296,6 +296,7 @@ "motion_photos_page_title": "Photos avec mouvement", "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.", "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.", + "no_assets_to_show" : "Aucun élément à afficher", "notification_permission_dialog_cancel": "Annuler", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", "notification_permission_dialog_settings": "Paramètres", @@ -509,5 +510,7 @@ "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89", "viewer_remove_from_stack": "Retirer de la pile", "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", - "viewer_unstack": "Désempiler" + "viewer_unstack": "Désempiler", + "haptic_feedback_title": "Retour haptique", + "haptic_feedback_switch": "Activer le retour haptique" } \ No newline at end of file diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 1894e39798..9eb77da4d4 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -525,7 +525,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -553,7 +553,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 64b4ea5474..517c5b29ef 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.101.0 + 1.102.2 CFBundleSignature ???? CFBundleVersion - 147 + 150 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 6cf9173c1c..dd62415ff5 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.101.0" + version_number: "1.102.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 1d6f7ff460..c3a94c1c23 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart index c9cc6c04a9..482f1efc4f 100644 --- a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart +++ b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart @@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget { const Center(child: ImmichLoadingIndicator()); Widget buildEmptyIndicator() => - emptyIndicator ?? const Center(child: Text("No assets to show")); + emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index 64bc1ec081..47b550f9d0 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -25,7 +25,6 @@ class SplashScreenPage extends HookConsumerWidget { void performLoggingIn() async { bool isSuccess = false; bool deviceIsOffline = false; - if (accessToken != null && serverUrl != null) { try { // Resolve API server endpoint from user provided serverUrl @@ -51,11 +50,15 @@ class SplashScreenPage extends HookConsumerWidget { offlineLogin: deviceIsOffline, ); } catch (error, stackTrace) { + ref.read(authenticationProvider.notifier).logout(); + log.severe( 'Cannot set success login info', error, stackTrace, ); + + context.pushRoute(const LoginRoute()); } } @@ -73,11 +76,6 @@ class SplashScreenPage extends HookConsumerWidget { } context.replaceRoute(const TabControllerRoute()); } else { - log.severe( - 'Unable to login through offline or online methods - logging out completely', - ); - - ref.read(authenticationProvider.notifier).logout(); // User was unable to login through either offline or online methods context.replaceRoute(const LoginRoute()); } diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index b1479ab83f..64229329aa 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -13,11 +13,10 @@ doc/ActivityCreateDto.md doc/ActivityResponseDto.md doc/ActivityStatisticsResponseDto.md doc/AddUsersDto.md +doc/AdminOnboardingUpdateDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md -doc/AlbumUserResponseDto.md -doc/AlbumUserRole.md doc/AllJobStatusResponseDto.md doc/AssetApi.md doc/AssetBulkDeleteDto.md @@ -71,6 +70,7 @@ doc/FaceApi.md doc/FaceDto.md doc/FileChecksumDto.md doc/FileChecksumResponseDto.md +doc/FileReportApi.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md @@ -124,6 +124,7 @@ doc/QueueStatusDto.md doc/ReactionLevel.md doc/ReactionType.md doc/RecognitionConfig.md +doc/ReverseGeocodingStateResponseDto.md doc/ScanLibraryDto.md doc/SearchAlbumResponseDto.md doc/SearchApi.md @@ -175,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md +doc/SystemMetadataApi.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -187,7 +189,6 @@ doc/TranscodeHWAccel.md doc/TranscodePolicy.md doc/TrashApi.md doc/UpdateAlbumDto.md -doc/UpdateAlbumUserDto.md doc/UpdateAssetDto.md doc/UpdateLibraryDto.md doc/UpdatePartnerDto.md @@ -215,6 +216,7 @@ lib/api/audit_api.dart lib/api/authentication_api.dart lib/api/download_api.dart lib/api/face_api.dart +lib/api/file_report_api.dart lib/api/job_api.dart lib/api/library_api.dart lib/api/memory_api.dart @@ -227,6 +229,7 @@ lib/api/sessions_api.dart lib/api/shared_link_api.dart lib/api/sync_api.dart lib/api/system_config_api.dart +lib/api/system_metadata_api.dart lib/api/tag_api.dart lib/api/timeline_api.dart lib/api/trash_api.dart @@ -243,10 +246,9 @@ lib/model/activity_create_dto.dart lib/model/activity_response_dto.dart lib/model/activity_statistics_response_dto.dart lib/model/add_users_dto.dart +lib/model/admin_onboarding_update_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart -lib/model/album_user_response_dto.dart -lib/model/album_user_role.dart lib/model/all_job_status_response_dto.dart lib/model/api_key_create_dto.dart lib/model/api_key_create_response_dto.dart @@ -346,6 +348,7 @@ lib/model/queue_status_dto.dart lib/model/reaction_level.dart lib/model/reaction_type.dart lib/model/recognition_config.dart +lib/model/reverse_geocoding_state_response_dto.dart lib/model/scan_library_dto.dart lib/model/search_album_response_dto.dart lib/model/search_asset_response_dto.dart @@ -400,7 +403,6 @@ lib/model/tone_mapping.dart lib/model/transcode_hw_accel.dart lib/model/transcode_policy.dart lib/model/update_album_dto.dart -lib/model/update_album_user_dto.dart lib/model/update_asset_dto.dart lib/model/update_library_dto.dart lib/model/update_partner_dto.dart @@ -423,11 +425,10 @@ test/activity_create_dto_test.dart test/activity_response_dto_test.dart test/activity_statistics_response_dto_test.dart test/add_users_dto_test.dart +test/admin_onboarding_update_dto_test.dart test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart -test/album_user_response_dto_test.dart -test/album_user_role_test.dart test/all_job_status_response_dto_test.dart test/api_key_api_test.dart test/api_key_create_dto_test.dart @@ -486,6 +487,7 @@ test/face_api_test.dart test/face_dto_test.dart test/file_checksum_dto_test.dart test/file_checksum_response_dto_test.dart +test/file_report_api_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart @@ -539,6 +541,7 @@ test/queue_status_dto_test.dart test/reaction_level_test.dart test/reaction_type_test.dart test/recognition_config_test.dart +test/reverse_geocoding_state_response_dto_test.dart test/scan_library_dto_test.dart test/search_album_response_dto_test.dart test/search_api_test.dart @@ -590,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart +test/system_metadata_api_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart @@ -602,7 +606,6 @@ test/transcode_hw_accel_test.dart test/transcode_policy_test.dart test/trash_api_test.dart test/update_album_dto_test.dart -test/update_album_user_dto_test.dart test/update_asset_dto_test.dart test/update_library_dto_test.dart test/update_partner_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2ba78660aa..5439d48208 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.101.0 +- API version: 1.102.3 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements @@ -91,7 +91,6 @@ Class | Method | HTTP request | Description *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | -*AlbumApi* | [**updateAlbumUser**](doc//AlbumApi.md#updatealbumuser) | **PUT** /album/{id}/permission/{userId} | *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | @@ -113,10 +112,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset | *AssetApi* | [**updateStackParent**](doc//AssetApi.md#updatestackparent) | **PUT** /asset/stack/parent | *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload | -*AuditApi* | [**fixAuditFiles**](doc//AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix | *AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes | -*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report | -*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | @@ -127,6 +123,9 @@ Class | Method | HTTP request | Description *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face | *FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} | +*FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /report/fix | +*FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /report | +*FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /report/checksum | *JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs | *JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} | *LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library | @@ -180,7 +179,6 @@ Class | Method | HTTP request | Description *ServerInfoApi* | [**getSupportedMediaTypes**](doc//ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | *ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme | *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} | *SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions | @@ -199,6 +197,9 @@ Class | Method | HTTP request | Description *SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json | *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | +*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | +*SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +*SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | *TagApi* | [**createTag**](doc//TagApi.md#createtag) | **POST** /tag | *TagApi* | [**deleteTag**](doc//TagApi.md#deletetag) | **DELETE** /tag/{id} | *TagApi* | [**getAllTags**](doc//TagApi.md#getalltags) | **GET** /tag | @@ -234,10 +235,9 @@ Class | Method | HTTP request | Description - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) + - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) - - [AlbumUserRole](doc//AlbumUserRole.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) @@ -333,6 +333,7 @@ Class | Method | HTTP request | Description - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [RecognitionConfig](doc//RecognitionConfig.md) + - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [ScanLibraryDto](doc//ScanLibraryDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) @@ -387,7 +388,6 @@ Class | Method | HTTP request | Description - [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodePolicy](doc//TranscodePolicy.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md) - - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.md) diff --git a/mobile/openapi/doc/AdminOnboardingUpdateDto.md b/mobile/openapi/doc/AdminOnboardingUpdateDto.md new file mode 100644 index 0000000000..b250843019 --- /dev/null +++ b/mobile/openapi/doc/AdminOnboardingUpdateDto.md @@ -0,0 +1,15 @@ +# openapi.model.AdminOnboardingUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**isOnboarded** | **bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AuditApi.md b/mobile/openapi/doc/AuditApi.md index 8514cdec73..2c768c40d1 100644 --- a/mobile/openapi/doc/AuditApi.md +++ b/mobile/openapi/doc/AuditApi.md @@ -9,66 +9,9 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- -[**fixAuditFiles**](AuditApi.md#fixauditfiles) | **POST** /audit/file-report/fix | [**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes | -[**getAuditFiles**](AuditApi.md#getauditfiles) | **GET** /audit/file-report | -[**getFileChecksums**](AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | -# **fixAuditFiles** -> fixAuditFiles(fileReportFixDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); -final fileReportFixDto = FileReportFixDto(); // FileReportFixDto | - -try { - api_instance.fixAuditFiles(fileReportFixDto); -} catch (e) { - print('Exception when calling AuditApi->fixAuditFiles: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| | - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **getAuditDeletes** > AuditDeletesResponseDto getAuditDeletes(after, entityType, userId) @@ -128,109 +71,3 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **getAuditFiles** -> FileReportDto getAuditFiles() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); - -try { - final result = api_instance.getAuditFiles(); - print(result); -} catch (e) { - print('Exception when calling AuditApi->getAuditFiles: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -[**FileReportDto**](FileReportDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - -# **getFileChecksums** -> List getFileChecksums(fileChecksumDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AuditApi(); -final fileChecksumDto = FileChecksumDto(); // FileChecksumDto | - -try { - final result = api_instance.getFileChecksums(fileChecksumDto); - print(result); -} catch (e) { - print('Exception when calling AuditApi->getFileChecksums: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| | - -### Return type - -[**List**](FileChecksumResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/mobile/openapi/doc/FileReportApi.md b/mobile/openapi/doc/FileReportApi.md new file mode 100644 index 0000000000..b722c86041 --- /dev/null +++ b/mobile/openapi/doc/FileReportApi.md @@ -0,0 +1,176 @@ +# openapi.api.FileReportApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**fixAuditFiles**](FileReportApi.md#fixauditfiles) | **POST** /report/fix | +[**getAuditFiles**](FileReportApi.md#getauditfiles) | **GET** /report | +[**getFileChecksums**](FileReportApi.md#getfilechecksums) | **POST** /report/checksum | + + +# **fixAuditFiles** +> fixAuditFiles(fileReportFixDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); +final fileReportFixDto = FileReportFixDto(); // FileReportFixDto | + +try { + api_instance.fixAuditFiles(fileReportFixDto); +} catch (e) { + print('Exception when calling FileReportApi->fixAuditFiles: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **fileReportFixDto** | [**FileReportFixDto**](FileReportFixDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getAuditFiles** +> FileReportDto getAuditFiles() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); + +try { + final result = api_instance.getAuditFiles(); + print(result); +} catch (e) { + print('Exception when calling FileReportApi->getAuditFiles: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**FileReportDto**](FileReportDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getFileChecksums** +> List getFileChecksums(fileChecksumDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = FileReportApi(); +final fileChecksumDto = FileChecksumDto(); // FileChecksumDto | + +try { + final result = api_instance.getFileChecksums(fileChecksumDto); + print(result); +} catch (e) { + print('Exception when calling FileReportApi->getFileChecksums: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **fileChecksumDto** | [**FileChecksumDto**](FileChecksumDto.md)| | + +### Return type + +[**List**](FileChecksumResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md new file mode 100644 index 0000000000..87f8aa8ab7 --- /dev/null +++ b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md @@ -0,0 +1,16 @@ +# openapi.model.ReverseGeocodingStateResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**lastImportFileName** | **String** | | +**lastUpdate** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/ServerInfoApi.md b/mobile/openapi/doc/ServerInfoApi.md index cb5cf0fd3e..e8121a8001 100644 --- a/mobile/openapi/doc/ServerInfoApi.md +++ b/mobile/openapi/doc/ServerInfoApi.md @@ -17,7 +17,6 @@ Method | HTTP request | Description [**getSupportedMediaTypes**](ServerInfoApi.md#getsupportedmediatypes) | **GET** /server-info/media-types | [**getTheme**](ServerInfoApi.md#gettheme) | **GET** /server-info/theme | [**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping | -[**setAdminOnboarding**](ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | # **getServerConfig** @@ -344,53 +343,3 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **setAdminOnboarding** -> setAdminOnboarding() - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = ServerInfoApi(); - -try { - api_instance.setAdminOnboarding(); -} catch (e) { - print('Exception when calling ServerInfoApi->setAdminOnboarding: $e\n'); -} -``` - -### Parameters -This endpoint does not need any parameter. - -### Return type - -void (empty response body) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: Not defined - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - diff --git a/mobile/openapi/doc/SystemConfigImageDto.md b/mobile/openapi/doc/SystemConfigImageDto.md index 1b9bbe726d..81e88045d5 100644 --- a/mobile/openapi/doc/SystemConfigImageDto.md +++ b/mobile/openapi/doc/SystemConfigImageDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **colorspace** | [**Colorspace**](Colorspace.md) | | +**extractEmbedded** | **bool** | | **previewFormat** | [**ImageFormat**](ImageFormat.md) | | **previewSize** | **int** | | **quality** | **int** | | diff --git a/mobile/openapi/doc/SystemMetadataApi.md b/mobile/openapi/doc/SystemMetadataApi.md new file mode 100644 index 0000000000..f8c2347afe --- /dev/null +++ b/mobile/openapi/doc/SystemMetadataApi.md @@ -0,0 +1,172 @@ +# openapi.api.SystemMetadataApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**getAdminOnboarding**](SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | +[**getReverseGeocodingState**](SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | +[**updateAdminOnboarding**](SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | + + +# **getAdminOnboarding** +> AdminOnboardingUpdateDto getAdminOnboarding() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); + +try { + final result = api_instance.getAdminOnboarding(); + print(result); +} catch (e) { + print('Exception when calling SystemMetadataApi->getAdminOnboarding: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**AdminOnboardingUpdateDto**](AdminOnboardingUpdateDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getReverseGeocodingState** +> ReverseGeocodingStateResponseDto getReverseGeocodingState() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); + +try { + final result = api_instance.getReverseGeocodingState(); + print(result); +} catch (e) { + print('Exception when calling SystemMetadataApi->getReverseGeocodingState: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**ReverseGeocodingStateResponseDto**](ReverseGeocodingStateResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **updateAdminOnboarding** +> updateAdminOnboarding(adminOnboardingUpdateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemMetadataApi(); +final adminOnboardingUpdateDto = AdminOnboardingUpdateDto(); // AdminOnboardingUpdateDto | + +try { + api_instance.updateAdminOnboarding(adminOnboardingUpdateDto); +} catch (e) { + print('Exception when calling SystemMetadataApi->updateAdminOnboarding: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **adminOnboardingUpdateDto** | [**AdminOnboardingUpdateDto**](AdminOnboardingUpdateDto.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 04a8578f1f..e7821e5e8f 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -37,6 +37,7 @@ part 'api/audit_api.dart'; part 'api/authentication_api.dart'; part 'api/download_api.dart'; part 'api/face_api.dart'; +part 'api/file_report_api.dart'; part 'api/job_api.dart'; part 'api/library_api.dart'; part 'api/memory_api.dart'; @@ -49,6 +50,7 @@ part 'api/sessions_api.dart'; part 'api/shared_link_api.dart'; part 'api/sync_api.dart'; part 'api/system_config_api.dart'; +part 'api/system_metadata_api.dart'; part 'api/tag_api.dart'; part 'api/timeline_api.dart'; part 'api/trash_api.dart'; @@ -62,6 +64,7 @@ part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; +part 'model/admin_onboarding_update_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/album_user_response_dto.dart'; @@ -161,6 +164,7 @@ part 'model/queue_status_dto.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/recognition_config.dart'; +part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/scan_library_dto.dart'; part 'model/search_album_response_dto.dart'; part 'model/search_asset_response_dto.dart'; diff --git a/mobile/openapi/lib/api/audit_api.dart b/mobile/openapi/lib/api/audit_api.dart index 871c8e1905..83dde34da7 100644 --- a/mobile/openapi/lib/api/audit_api.dart +++ b/mobile/openapi/lib/api/audit_api.dart @@ -16,45 +16,6 @@ class AuditApi { final ApiClient apiClient; - /// Performs an HTTP 'POST /audit/file-report/fix' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report/fix'; - - // ignore: prefer_final_locals - Object? postBody = fileReportFixDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { - final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response]. /// Parameters: /// @@ -115,95 +76,4 @@ class AuditApi { } return null; } - - /// Performs an HTTP 'GET /audit/file-report' operation and returns the [Response]. - Future getAuditFilesWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future getAuditFiles() async { - final response = await getAuditFilesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; - - } - return null; - } - - /// Performs an HTTP 'POST /audit/file-report/checksum' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { - // ignore: prefer_const_declarations - final path = r'/audit/file-report/checksum'; - - // ignore: prefer_final_locals - Object? postBody = fileChecksumDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { - final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } } diff --git a/mobile/openapi/lib/api/file_report_api.dart b/mobile/openapi/lib/api/file_report_api.dart new file mode 100644 index 0000000000..df307e12c7 --- /dev/null +++ b/mobile/openapi/lib/api/file_report_api.dart @@ -0,0 +1,148 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 FileReportApi { + FileReportApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /report/fix' operation and returns the [Response]. + /// Parameters: + /// + /// * [FileReportFixDto] fileReportFixDto (required): + Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { + // ignore: prefer_const_declarations + final path = r'/report/fix'; + + // ignore: prefer_final_locals + Object? postBody = fileReportFixDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [FileReportFixDto] fileReportFixDto (required): + Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { + final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /report' operation and returns the [Response]. + Future getAuditFilesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/report'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAuditFiles() async { + final response = await getAuditFilesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; + + } + return null; + } + + /// Performs an HTTP 'POST /report/checksum' operation and returns the [Response]. + /// Parameters: + /// + /// * [FileChecksumDto] fileChecksumDto (required): + Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { + // ignore: prefer_const_declarations + final path = r'/report/checksum'; + + // ignore: prefer_final_locals + Object? postBody = fileChecksumDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [FileChecksumDto] fileChecksumDto (required): + Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { + final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } +} diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart index 77840acd19..b67045add1 100644 --- a/mobile/openapi/lib/api/server_info_api.dart +++ b/mobile/openapi/lib/api/server_info_api.dart @@ -343,37 +343,4 @@ class ServerInfoApi { } return null; } - - /// Performs an HTTP 'POST /server-info/admin-onboarding' operation and returns the [Response]. - Future setAdminOnboardingWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/server-info/admin-onboarding'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future setAdminOnboarding() async { - final response = await setAdminOnboardingWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } } diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart new file mode 100644 index 0000000000..f3952fda8a --- /dev/null +++ b/mobile/openapi/lib/api/system_metadata_api.dart @@ -0,0 +1,139 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 SystemMetadataApi { + SystemMetadataApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /system-metadata/admin-onboarding' operation and returns the [Response]. + Future getAdminOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/admin-onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAdminOnboarding() async { + final response = await getAdminOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AdminOnboardingUpdateDto',) as AdminOnboardingUpdateDto; + + } + return null; + } + + /// Performs an HTTP 'GET /system-metadata/reverse-geocoding-state' operation and returns the [Response]. + Future getReverseGeocodingStateWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/reverse-geocoding-state'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getReverseGeocodingState() async { + final response = await getReverseGeocodingStateWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ReverseGeocodingStateResponseDto',) as ReverseGeocodingStateResponseDto; + + } + return null; + } + + /// Performs an HTTP 'POST /system-metadata/admin-onboarding' operation and returns the [Response]. + /// Parameters: + /// + /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): + Future updateAdminOnboardingWithHttpInfo(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/system-metadata/admin-onboarding'; + + // ignore: prefer_final_locals + Object? postBody = adminOnboardingUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [AdminOnboardingUpdateDto] adminOnboardingUpdateDto (required): + Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto,) async { + final response = await updateAdminOnboardingWithHttpInfo(adminOnboardingUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index d7cdcad18b..a92f1df7a7 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -198,14 +198,12 @@ class ApiClient { return ActivityStatisticsResponseDto.fromJson(value); case 'AddUsersDto': return AddUsersDto.fromJson(value); + case 'AdminOnboardingUpdateDto': + return AdminOnboardingUpdateDto.fromJson(value); case 'AlbumCountResponseDto': return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': return AlbumResponseDto.fromJson(value); - case 'AlbumUserResponseDto': - return AlbumUserResponseDto.fromJson(value); - case 'AlbumUserRole': - return AlbumUserRoleTypeTransformer().decode(value); case 'AllJobStatusResponseDto': return AllJobStatusResponseDto.fromJson(value); case 'AssetBulkDeleteDto': @@ -396,6 +394,8 @@ class ApiClient { return ReactionTypeTypeTransformer().decode(value); case 'RecognitionConfig': return RecognitionConfig.fromJson(value); + case 'ReverseGeocodingStateResponseDto': + return ReverseGeocodingStateResponseDto.fromJson(value); case 'ScanLibraryDto': return ScanLibraryDto.fromJson(value); case 'SearchAlbumResponseDto': @@ -504,8 +504,6 @@ class ApiClient { return TranscodePolicyTypeTransformer().decode(value); case 'UpdateAlbumDto': return UpdateAlbumDto.fromJson(value); - case 'UpdateAlbumUserDto': - return UpdateAlbumUserDto.fromJson(value); case 'UpdateAssetDto': return UpdateAssetDto.fromJson(value); case 'UpdateLibraryDto': diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart new file mode 100644 index 0000000000..50c4ae090e --- /dev/null +++ b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 AdminOnboardingUpdateDto { + /// Returns a new [AdminOnboardingUpdateDto] instance. + AdminOnboardingUpdateDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is AdminOnboardingUpdateDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'AdminOnboardingUpdateDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [AdminOnboardingUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AdminOnboardingUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AdminOnboardingUpdateDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AdminOnboardingUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AdminOnboardingUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AdminOnboardingUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AdminOnboardingUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart new file mode 100644 index 0000000000..71e1d3ad99 --- /dev/null +++ b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 ReverseGeocodingStateResponseDto { + /// Returns a new [ReverseGeocodingStateResponseDto] instance. + ReverseGeocodingStateResponseDto({ + required this.lastImportFileName, + required this.lastUpdate, + }); + + String? lastImportFileName; + + String? lastUpdate; + + @override + bool operator ==(Object other) => identical(this, other) || other is ReverseGeocodingStateResponseDto && + other.lastImportFileName == lastImportFileName && + other.lastUpdate == lastUpdate; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (lastImportFileName == null ? 0 : lastImportFileName!.hashCode) + + (lastUpdate == null ? 0 : lastUpdate!.hashCode); + + @override + String toString() => 'ReverseGeocodingStateResponseDto[lastImportFileName=$lastImportFileName, lastUpdate=$lastUpdate]'; + + Map toJson() { + final json = {}; + if (this.lastImportFileName != null) { + json[r'lastImportFileName'] = this.lastImportFileName; + } else { + // json[r'lastImportFileName'] = null; + } + if (this.lastUpdate != null) { + json[r'lastUpdate'] = this.lastUpdate; + } else { + // json[r'lastUpdate'] = null; + } + return json; + } + + /// Returns a new [ReverseGeocodingStateResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReverseGeocodingStateResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return ReverseGeocodingStateResponseDto( + lastImportFileName: mapValueOfType(json, r'lastImportFileName'), + lastUpdate: mapValueOfType(json, r'lastUpdate'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReverseGeocodingStateResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ReverseGeocodingStateResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReverseGeocodingStateResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ReverseGeocodingStateResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'lastImportFileName', + 'lastUpdate', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 1c830861af..7072e11270 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -14,6 +14,7 @@ class SystemConfigImageDto { /// Returns a new [SystemConfigImageDto] instance. SystemConfigImageDto({ required this.colorspace, + required this.extractEmbedded, required this.previewFormat, required this.previewSize, required this.quality, @@ -23,6 +24,8 @@ class SystemConfigImageDto { Colorspace colorspace; + bool extractEmbedded; + ImageFormat previewFormat; int previewSize; @@ -36,6 +39,7 @@ class SystemConfigImageDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && other.colorspace == colorspace && + other.extractEmbedded == extractEmbedded && other.previewFormat == previewFormat && other.previewSize == previewSize && other.quality == quality && @@ -46,6 +50,7 @@ class SystemConfigImageDto { int get hashCode => // ignore: unnecessary_parenthesis (colorspace.hashCode) + + (extractEmbedded.hashCode) + (previewFormat.hashCode) + (previewSize.hashCode) + (quality.hashCode) + @@ -53,11 +58,12 @@ class SystemConfigImageDto { (thumbnailSize.hashCode); @override - String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; Map toJson() { final json = {}; json[r'colorspace'] = this.colorspace; + json[r'extractEmbedded'] = this.extractEmbedded; json[r'previewFormat'] = this.previewFormat; json[r'previewSize'] = this.previewSize; json[r'quality'] = this.quality; @@ -75,6 +81,7 @@ class SystemConfigImageDto { return SystemConfigImageDto( colorspace: Colorspace.fromJson(json[r'colorspace'])!, + extractEmbedded: mapValueOfType(json, r'extractEmbedded')!, previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, previewSize: mapValueOfType(json, r'previewSize')!, quality: mapValueOfType(json, r'quality')!, @@ -128,6 +135,7 @@ class SystemConfigImageDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'colorspace', + 'extractEmbedded', 'previewFormat', 'previewSize', 'quality', diff --git a/mobile/openapi/test/admin_onboarding_update_dto_test.dart b/mobile/openapi/test/admin_onboarding_update_dto_test.dart new file mode 100644 index 0000000000..09cc73e977 --- /dev/null +++ b/mobile/openapi/test/admin_onboarding_update_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AdminOnboardingUpdateDto +void main() { + // final instance = AdminOnboardingUpdateDto(); + + group('test AdminOnboardingUpdateDto', () { + // bool isOnboarded + test('to test the property `isOnboarded`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/audit_api_test.dart b/mobile/openapi/test/audit_api_test.dart index 8161d4e4db..8114283a1a 100644 --- a/mobile/openapi/test/audit_api_test.dart +++ b/mobile/openapi/test/audit_api_test.dart @@ -17,25 +17,10 @@ void main() { // final instance = AuditApi(); group('tests for AuditApi', () { - //Future fixAuditFiles(FileReportFixDto fileReportFixDto) async - test('test fixAuditFiles', () async { - // TODO - }); - //Future getAuditDeletes(DateTime after, EntityType entityType, { String userId }) async test('test getAuditDeletes', () async { // TODO }); - //Future getAuditFiles() async - test('test getAuditFiles', () async { - // TODO - }); - - //Future> getFileChecksums(FileChecksumDto fileChecksumDto) async - test('test getFileChecksums', () async { - // TODO - }); - }); } diff --git a/mobile/openapi/test/file_report_api_test.dart b/mobile/openapi/test/file_report_api_test.dart new file mode 100644 index 0000000000..255c787002 --- /dev/null +++ b/mobile/openapi/test/file_report_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for FileReportApi +void main() { + // final instance = FileReportApi(); + + group('tests for FileReportApi', () { + //Future fixAuditFiles(FileReportFixDto fileReportFixDto) async + test('test fixAuditFiles', () async { + // TODO + }); + + //Future getAuditFiles() async + test('test getAuditFiles', () async { + // TODO + }); + + //Future> getFileChecksums(FileChecksumDto fileChecksumDto) async + test('test getFileChecksums', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart new file mode 100644 index 0000000000..91fdfcfea4 --- /dev/null +++ b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ReverseGeocodingStateResponseDto +void main() { + // final instance = ReverseGeocodingStateResponseDto(); + + group('test ReverseGeocodingStateResponseDto', () { + // String lastImportFileName + test('to test the property `lastImportFileName`', () async { + // TODO + }); + + // String lastUpdate + test('to test the property `lastUpdate`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart index 68cd1c348b..dac465116e 100644 --- a/mobile/openapi/test/server_info_api_test.dart +++ b/mobile/openapi/test/server_info_api_test.dart @@ -57,10 +57,5 @@ void main() { // TODO }); - //Future setAdminOnboarding() async - test('test setAdminOnboarding', () async { - // TODO - }); - }); } diff --git a/mobile/openapi/test/system_config_image_dto_test.dart b/mobile/openapi/test/system_config_image_dto_test.dart index aef907bbe6..b46340455b 100644 --- a/mobile/openapi/test/system_config_image_dto_test.dart +++ b/mobile/openapi/test/system_config_image_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // bool extractEmbedded + test('to test the property `extractEmbedded`', () async { + // TODO + }); + // ImageFormat previewFormat test('to test the property `previewFormat`', () async { // TODO diff --git a/mobile/openapi/test/system_metadata_api_test.dart b/mobile/openapi/test/system_metadata_api_test.dart new file mode 100644 index 0000000000..bc1ce6f6f3 --- /dev/null +++ b/mobile/openapi/test/system_metadata_api_test.dart @@ -0,0 +1,36 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for SystemMetadataApi +void main() { + // final instance = SystemMetadataApi(); + + group('tests for SystemMetadataApi', () { + //Future getAdminOnboarding() async + test('test getAdminOnboarding', () async { + // TODO + }); + + //Future getReverseGeocodingState() async + test('test getReverseGeocodingState', () async { + // TODO + }); + + //Future updateAdminOnboarding(AdminOnboardingUpdateDto adminOnboardingUpdateDto) async + test('test updateAdminOnboarding', () async { + // TODO + }); + + }); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 6bf2b09026..54648fd20b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1804,5 +1804,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.16.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 8dcc892a06..8ae3a21318 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,10 +2,10 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.101.0+131 +version: 1.102.3+136 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: flutter: diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 145e7eeec7..0c56971ff3 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2398,118 +2398,6 @@ ] } }, - "/audit/file-report": { - "get": { - "operationId": "getAuditFiles", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, - "/audit/file-report/checksum": { - "post": { - "operationId": "getFileChecksums", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileChecksumDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/FileChecksumResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, - "/audit/file-report/fix": { - "post": { - "operationId": "fixAuditFiles", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportFixDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Audit" - ] - } - }, "/auth/admin-sign-up": { "post": { "operationId": "signUpAdmin", @@ -4482,6 +4370,118 @@ ] } }, + "/report": { + "get": { + "operationId": "getAuditFiles", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileReportDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, + "/report/checksum": { + "post": { + "operationId": "getFileChecksums", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileChecksumDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/FileChecksumResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, + "/report/fix": { + "post": { + "operationId": "fixAuditFiles", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileReportFixDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "File Report" + ] + } + }, "/search": { "get": { "deprecated": true, @@ -4961,31 +4961,6 @@ ] } }, - "/server-info/admin-onboarding": { - "post": { - "operationId": "setAdminOnboarding", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Server Info" - ] - } - }, "/server-info/config": { "get": { "operationId": "getServerConfig", @@ -5938,6 +5913,103 @@ ] } }, + "/system-metadata/admin-onboarding": { + "get": { + "operationId": "getAdminOnboarding", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + }, + "post": { + "operationId": "updateAdminOnboarding", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + } + }, + "/system-metadata/reverse-geocoding-state": { + "get": { + "operationId": "getReverseGeocodingState", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReverseGeocodingStateResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Metadata" + ] + } + }, "/tag": { "get": { "operationId": "getAllTags", @@ -7059,7 +7131,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.101.0", + "version": "1.102.3", "contact": {} }, "tags": [], @@ -7233,6 +7305,17 @@ ], "type": "object" }, + "AdminOnboardingUpdateDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, "AlbumCountResponseDto": { "properties": { "notShared": { @@ -9702,6 +9785,23 @@ ], "type": "object" }, + "ReverseGeocodingStateResponseDto": { + "properties": { + "lastImportFileName": { + "nullable": true, + "type": "string" + }, + "lastUpdate": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "lastImportFileName", + "lastUpdate" + ], + "type": "object" + }, "ScanLibraryDto": { "properties": { "refreshAllFiles": { @@ -10615,6 +10715,9 @@ "colorspace": { "$ref": "#/components/schemas/Colorspace" }, + "extractEmbedded": { + "type": "boolean" + }, "previewFormat": { "$ref": "#/components/schemas/ImageFormat" }, @@ -10633,6 +10736,7 @@ }, "required": [ "colorspace", + "extractEmbedded", "previewFormat", "previewSize", "quality", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 8def6adffd..a6a751ccfa 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 887fece059..dd2360cede 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 8b5140f0c1..afd9c5ac17 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.101.0 + * 1.102.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -325,27 +325,6 @@ export type AuditDeletesResponseDto = { ids: string[]; needsFullSync: boolean; }; -export type FileReportItemDto = { - checksum?: string; - entityId: string; - entityType: PathEntityType; - pathType: PathType; - pathValue: string; -}; -export type FileReportDto = { - extras: string[]; - orphans: FileReportItemDto[]; -}; -export type FileChecksumDto = { - filenames: string[]; -}; -export type FileChecksumResponseDto = { - checksum: string; - filename: string; -}; -export type FileReportFixDto = { - items: FileReportItemDto[]; -}; export type SignUpDto = { email: string; name: string; @@ -608,6 +587,27 @@ export type AssetFaceUpdateDto = { export type PersonStatisticsResponseDto = { assets: number; }; +export type FileReportItemDto = { + checksum?: string; + entityId: string; + entityType: PathEntityType; + pathType: PathType; + pathValue: string; +}; +export type FileReportDto = { + extras: string[]; + orphans: FileReportItemDto[]; +}; +export type FileChecksumDto = { + filenames: string[]; +}; +export type FileChecksumResponseDto = { + checksum: string; + filename: string; +}; +export type FileReportFixDto = { + items: FileReportItemDto[]; +}; export type SearchFacetCountResponseDto = { count: number; value: string; @@ -873,6 +873,7 @@ export type SystemConfigFFmpegDto = { }; export type SystemConfigImageDto = { colorspace: Colorspace; + extractEmbedded: boolean; previewFormat: ImageFormat; previewSize: number; quality: number; @@ -1006,6 +1007,13 @@ export type SystemConfigTemplateStorageOptionDto = { weekOptions: string[]; yearOptions: string[]; }; +export type AdminOnboardingUpdateDto = { + isOnboarded: boolean; +}; +export type ReverseGeocodingStateResponseDto = { + lastImportFileName: string | null; + lastUpdate: string | null; +}; export type CreateTagDto = { name: string; "type": TagTypeEnum; @@ -1670,35 +1678,6 @@ export function getAuditDeletes({ after, entityType, userId }: { ...opts })); } -export function getAuditFiles(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: FileReportDto; - }>("/audit/file-report", { - ...opts - })); -} -export function getFileChecksums({ fileChecksumDto }: { - fileChecksumDto: FileChecksumDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; - data: FileChecksumResponseDto[]; - }>("/audit/file-report/checksum", oazapfts.json({ - ...opts, - method: "POST", - body: fileChecksumDto - }))); -} -export function fixAuditFiles({ fileReportFixDto }: { - fileReportFixDto: FileReportFixDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/audit/file-report/fix", oazapfts.json({ - ...opts, - method: "POST", - body: fileReportFixDto - }))); -} export function signUpAdmin({ signUpDto }: { signUpDto: SignUpDto; }, opts?: Oazapfts.RequestOpts) { @@ -2225,6 +2204,35 @@ export function getPersonThumbnail({ id }: { ...opts })); } +export function getAuditFiles(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: FileReportDto; + }>("/report", { + ...opts + })); +} +export function getFileChecksums({ fileChecksumDto }: { + fileChecksumDto: FileChecksumDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: FileChecksumResponseDto[]; + }>("/report/checksum", oazapfts.json({ + ...opts, + method: "POST", + body: fileChecksumDto + }))); +} +export function fixAuditFiles({ fileReportFixDto }: { + fileReportFixDto: FileReportFixDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/report/fix", oazapfts.json({ + ...opts, + method: "POST", + body: fileReportFixDto + }))); +} export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: { clip?: boolean; motion?: boolean; @@ -2349,12 +2357,6 @@ export function getServerInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function setAdminOnboarding(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/server-info/admin-onboarding", { - ...opts, - method: "POST" - })); -} export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2616,6 +2618,31 @@ export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getAdminOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminOnboardingUpdateDto; + }>("/system-metadata/admin-onboarding", { + ...opts + })); +} +export function updateAdminOnboarding({ adminOnboardingUpdateDto }: { + adminOnboardingUpdateDto: AdminOnboardingUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/system-metadata/admin-onboarding", oazapfts.json({ + ...opts, + method: "POST", + body: adminOnboardingUpdateDto + }))); +} +export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ReverseGeocodingStateResponseDto; + }>("/system-metadata/reverse-geocoding-state", { + ...opts + })); +} export function getAllTags(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2971,20 +2998,6 @@ export enum EntityType { Asset = "ASSET", Album = "ALBUM" } -export enum PathEntityType { - Asset = "asset", - Person = "person", - User = "user" -} -export enum PathType { - Original = "original", - Preview = "preview", - Thumbnail = "thumbnail", - EncodedVideo = "encoded_video", - Sidecar = "sidecar", - Face = "face", - Profile = "profile" -} export enum JobName { ThumbnailGeneration = "thumbnailGeneration", MetadataExtraction = "metadataExtraction", @@ -3016,6 +3029,20 @@ export enum Type2 { export enum MemoryType { OnThisDay = "on_this_day" } +export enum PathEntityType { + Asset = "asset", + Person = "person", + User = "user" +} +export enum PathType { + Original = "original", + Preview = "preview", + Thumbnail = "thumbnail", + EncodedVideo = "encoded_video", + Sidecar = "sidecar", + Face = "face", + Profile = "profile" +} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/renovate.json b/renovate.json index afa68011d0..3e2c50d7f1 100644 --- a/renovate.json +++ b/renovate.json @@ -27,7 +27,8 @@ "matchFileNames": ["mobile/**"], "groupName": "mobile", "matchUpdateTypes": ["minor", "patch"], - "schedule": "on tuesday" + "schedule": "on tuesday", + "addLabels": ["📱mobile"] }, { "groupName": "exiftool", diff --git a/server/package-lock.json b/server/package-lock.json index 286f1006b9..eb062059f3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index d5828822cd..274eddd304 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.101.0", + "version": "1.102.3", "description": "", "author": "", "private": true, diff --git a/server/src/constants.ts b/server/src/constants.ts index 1289701dd8..d9d4232396 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -26,12 +26,7 @@ export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); export const MOBILE_REDIRECT = 'app.immich:/'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; -export const IMMICH_ACCESS_COOKIE = 'immich_access_token'; -export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated'; -export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type'; -export const IMMICH_API_KEY_NAME = 'api_key'; -export const IMMICH_API_KEY_HEADER = 'x-api-key'; -export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token'; + export enum AuthType { PASSWORD = 'password', OAUTH = 'oauth', diff --git a/server/src/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts index 1487e78d47..8eea6a6e3e 100644 --- a/server/src/controllers/audit.controller.ts +++ b/server/src/controllers/audit.controller.ts @@ -1,15 +1,8 @@ -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { - AuditDeletesDto, - AuditDeletesResponseDto, - FileChecksumDto, - FileChecksumResponseDto, - FileReportDto, - FileReportFixDto, -} from 'src/dtos/audit.dto'; +import { AuditDeletesDto, AuditDeletesResponseDto } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AuditService } from 'src/services/audit.service'; @ApiTags('Audit') @@ -22,22 +15,4 @@ export class AuditController { getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise { return this.service.getDeletes(auth, dto); } - - @AdminRoute() - @Get('file-report') - getAuditFiles(): Promise { - return this.service.getFileReport(); - } - - @AdminRoute() - @Post('file-report/checksum') - getFileChecksums(@Body() dto: FileChecksumDto): Promise { - return this.service.getChecksums(dto); - } - - @AdminRoute() - @Post('file-report/fix') - fixAuditFiles(@Body() dto: FileReportFixDto): Promise { - return this.service.fixItems(dto.items); - } } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index f4e7666207..a4c7494f2b 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,10 +1,11 @@ import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants'; +import { AuthType } from 'src/constants'; import { AuthDto, ChangePasswordDto, + ImmichCookie, LoginCredentialDto, LoginResponseDto, LogoutResponseDto, @@ -14,6 +15,7 @@ import { import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; @ApiTags('Authentication') @Controller('auth') @@ -28,9 +30,15 @@ export class AuthController { @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const { response, cookie } = await this.service.login(loginCredential, loginDetails); - res.header('Set-Cookie', cookie); - return response; + const body = await this.service.login(loginCredential, loginDetails); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, + { key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD }, + { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + ], + }); } @PublicRoute() @@ -53,15 +61,18 @@ export class AuthController { @Post('logout') @HttpCode(HttpStatus.OK) - logout( + async logout( @Req() request: Request, @Res({ passthrough: true }) res: Response, @Auth() auth: AuthDto, ): Promise { - res.clearCookie(IMMICH_ACCESS_COOKIE); - res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); - res.clearCookie(IMMICH_IS_AUTHENTICATED); + const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE]; - return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); + const body = await this.service.logout(auth, authType); + return respondWithoutCookie(res, body, [ + ImmichCookie.ACCESS_TOKEN, + ImmichCookie.AUTH_TYPE, + ImmichCookie.IS_AUTHENTICATED, + ]); } } diff --git a/server/src/controllers/file-report.controller.ts b/server/src/controllers/file-report.controller.ts new file mode 100644 index 0000000000..6bdf726073 --- /dev/null +++ b/server/src/controllers/file-report.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; +import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { AuditService } from 'src/services/audit.service'; + +@ApiTags('File Report') +@Controller('report') +@Authenticated() +export class ReportController { + constructor(private service: AuditService) {} + + @AdminRoute() + @Get() + getAuditFiles(): Promise { + return this.service.getFileReport(); + } + + @AdminRoute() + @Post('/checksum') + getFileChecksums(@Body() dto: FileChecksumDto): Promise { + return this.service.getChecksums(dto); + } + + @AdminRoute() + @Post('/fix') + fixAuditFiles(@Body() dto: FileReportFixDto): Promise { + return this.service.fixItems(dto.items); + } +} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 5e109f1eb3..bd10c41a43 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -8,6 +8,7 @@ import { AuditController } from 'src/controllers/audit.controller'; import { AuthController } from 'src/controllers/auth.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { FaceController } from 'src/controllers/face.controller'; +import { ReportController } from 'src/controllers/file-report.controller'; import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; import { MemoryController } from 'src/controllers/memory.controller'; @@ -20,19 +21,20 @@ import { SessionController } from 'src/controllers/session.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { SyncController } from 'src/controllers/sync.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { SystemMetadataController } from 'src/controllers/system-metadata.controller'; import { TagController } from 'src/controllers/tag.controller'; import { TimelineController } from 'src/controllers/timeline.controller'; import { TrashController } from 'src/controllers/trash.controller'; import { UserController } from 'src/controllers/user.controller'; export const controllers = [ - ActivityController, - AssetsController, - AssetControllerV1, - AssetController, - AppController, - AlbumController, APIKeyController, + ActivityController, + AlbumController, + AppController, + AssetController, + AssetControllerV1, + AssetsController, AuditController, AuthController, DownloadController, @@ -42,15 +44,17 @@ export const controllers = [ MemoryController, OAuthController, PartnerController, + PersonController, + ReportController, SearchController, ServerInfoController, SessionController, SharedLinkController, SyncController, SystemConfigController, + SystemMetadataController, TagController, TimelineController, TrashController, UserController, - PersonController, ]; diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index debbd4e676..d87fb11d88 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -1,8 +1,10 @@ import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; +import { AuthType } from 'src/constants'; import { AuthDto, + ImmichCookie, LoginResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, @@ -11,6 +13,7 @@ import { import { UserResponseDto } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { respondWithCookie } from 'src/utils/response'; @ApiTags('OAuth') @Controller('oauth') @@ -41,9 +44,15 @@ export class OAuthController { @Body() dto: OAuthCallbackDto, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const { response, cookie } = await this.service.callback(dto, loginDetails); - res.header('Set-Cookie', cookie); - return response; + const body = await this.service.callback(dto, loginDetails); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: [ + { key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, + { key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH }, + { key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, + ], + }); } @Post('link') diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index e32b0d191c..35e5e17594 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ServerConfigDto, @@ -65,11 +65,4 @@ export class ServerInfoController { getSupportedMediaTypes(): ServerMediaTypesResponseDto { return this.service.getSupportedMediaTypes(); } - - @AdminRoute() - @Post('admin-onboarding') - @HttpCode(HttpStatus.NO_CONTENT) - setAdminOnboarding(): Promise { - return this.service.setAdminOnboarding(); - } } diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index a7a8e3a1c6..58f2939b93 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -1,18 +1,19 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; -import { AuthDto } from 'src/dtos/auth.dto'; +import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto, SharedLinkResponseDto, } from 'src/dtos/shared-link.dto'; -import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; +import { respondWithCookie } from 'src/utils/response'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Shared Link') @@ -33,20 +34,17 @@ export class SharedLinkController { @Query() dto: SharedLinkPasswordDto, @Req() request: Request, @Res({ passthrough: true }) res: Response, + @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE]; + const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN]; if (sharedLinkToken) { dto.token = sharedLinkToken; } - const response = await this.service.getMine(auth, dto); - if (response.token) { - res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, { - expires: new Date(Date.now() + 1000 * 60 * 60 * 24), - httpOnly: true, - sameSite: 'lax', - }); - } - return response; + const body = await this.service.getMine(auth, dto); + return respondWithCookie(res, body, { + isSecure: loginDetails.isSecure, + values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [], + }); } @Get(':id') diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts new file mode 100644 index 0000000000..7f186fec03 --- /dev/null +++ b/server/src/controllers/system-metadata.controller.ts @@ -0,0 +1,28 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; + +@ApiTags('System Metadata') +@Controller('system-metadata') +@Authenticated({ admin: true }) +export class SystemMetadataController { + constructor(private service: SystemMetadataService) {} + + @Get('admin-onboarding') + getAdminOnboarding(): Promise { + return this.service.getAdminOnboarding(); + } + + @Post('admin-onboarding') + @HttpCode(HttpStatus.NO_CONTENT) + updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { + return this.service.updateAdminOnboarding(dto); + } + + @Get('reverse-geocoding-state') + getReverseGeocodingState(): Promise { + return this.service.getReverseGeocodingState(); + } +} diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index f1c16e5698..4e5f4742a4 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { APP_MEDIA_LOCATION } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; @@ -308,4 +309,8 @@ export class StorageCore { static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string { return join(this.getNestedFolder(folder, ownerId, filename), filename); } + + static getTempPathInDir(dir: string): string { + return join(dir, `${randomUUID()}.tmp`); + } } diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 9cbe3b8414..2520840173 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -120,6 +120,7 @@ export const defaults = Object.freeze({ previewSize: 1440, quality: 80, colorspace: Colorspace.P3, + extractEmbedded: false, }, newVersionCheck: { enabled: true, diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 4651c010b9..5c1e01b818 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -6,6 +6,25 @@ import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; +export enum ImmichCookie { + ACCESS_TOKEN = 'immich_access_token', + AUTH_TYPE = 'immich_auth_type', + IS_AUTHENTICATED = 'immich_is_authenticated', + SHARED_LINK_TOKEN = 'immich_shared_link_token', +} + +export enum ImmichHeader { + API_KEY = 'x-api-key', + USER_TOKEN = 'x-immich-user-token', + SESSION_TOKEN = 'x-immich-session-token', + SHARED_LINK_TOKEN = 'x-immich-share-key', +} + +export type CookieResponse = { + isSecure: boolean; + values: Array<{ key: ImmichCookie; value: string }>; +}; + export class AuthDto { user!: UserEntity; @@ -39,7 +58,7 @@ export class LoginResponseDto { export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto { return { - accessToken: accessToken, + accessToken, userId: entity.id, userEmail: entity.email, name: entity.name, diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 9f80e8d6a3..d23eef4994 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -417,6 +417,9 @@ class SystemConfigImageDto { @IsEnum(Colorspace) @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) colorspace!: Colorspace; + + @ValidateBoolean() + extractEmbedded!: boolean; } class SystemConfigTrashDto { diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts new file mode 100644 index 0000000000..1c04435341 --- /dev/null +++ b/server/src/dtos/system-metadata.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean } from 'class-validator'; + +export class AdminOnboardingUpdateDto { + @IsBoolean() + isOnboarded!: boolean; +} + +export class AdminOnboardingResponseDto { + isOnboarded!: boolean; +} + +export class ReverseGeocodingStateResponseDto { + lastUpdate!: string | null; + lastImportFileName!: string | null; +} diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts index a8a550fd6d..7126297ce3 100644 --- a/server/src/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -114,6 +114,7 @@ export const SystemConfigKey = { IMAGE_PREVIEW_SIZE: 'image.previewSize', IMAGE_QUALITY: 'image.quality', IMAGE_COLORSPACE: 'image.colorspace', + IMAGE_EXTRACT_EMBEDDED: 'image.extractEmbedded', TRASH_ENABLED: 'trash.enabled', TRASH_DAYS: 'trash.days', @@ -284,6 +285,7 @@ export interface SystemConfig { previewSize: number; quality: number; colorspace: Colorspace; + extractEmbedded: boolean; }; newVersionCheck: { enabled: boolean; diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 5e51e94a52..a82b38b6de 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -34,6 +34,11 @@ export interface VideoFormat { bitrate: number; } +export interface ImageDimensions { + width: number; + height: number; +} + export interface VideoInfo { format: VideoFormat; videoStreams: VideoStreamInfo[]; @@ -70,9 +75,11 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig { export interface IMediaRepository { // image + extract(input: string, output: string): Promise; resize(input: string | Buffer, output: string, options: ResizeOptions): Promise; crop(input: string, options: CropOptions): Promise; generateThumbhash(imagePath: string): Promise; + getImageDimensions(input: string): Promise; // video probe(input: string): Promise; diff --git a/server/src/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts index 3e2c9574a4..62c2ecec7b 100644 --- a/server/src/interfaces/session.interface.ts +++ b/server/src/interfaces/session.interface.ts @@ -2,10 +2,12 @@ import { SessionEntity } from 'src/entities/session.entity'; export const ISessionRepository = 'ISessionRepository'; +type E = SessionEntity; + export interface ISessionRepository { - create(dto: Partial): Promise; - update(dto: Partial): Promise; + create>(dto: T): Promise; + update>(dto: T): Promise; delete(id: string): Promise; - getByToken(token: string): Promise; - getByUserId(userId: string): Promise; + getByToken(token: string): Promise; + getByUserId(userId: string): Promise; } diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index 8b3abe6693..1253e99bbb 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -10,7 +10,6 @@ import { import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; -import { IMMICH_API_KEY_NAME } from 'src/constants'; import { AuthDto } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; @@ -21,6 +20,7 @@ export enum Metadata { ADMIN_ROUTE = 'admin_route', SHARED_ROUTE = 'shared_route', PUBLIC_SECURITY = 'public_security', + API_KEY_SECURITY = 'api_key', } export interface AuthenticatedOptions { @@ -32,7 +32,7 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => { const decorators: MethodDecorator[] = [ ApiBearerAuth(), ApiCookieAuth(), - ApiSecurity(IMMICH_API_KEY_NAME), + ApiSecurity(Metadata.API_KEY_SECURITY), SetMetadata(Metadata.AUTH_ROUTE, true), ]; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 3936ad7e42..434fb585f8 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { exiftool } from 'exiftool-vendored'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import fs from 'node:fs/promises'; import { Writable } from 'node:stream'; @@ -9,6 +10,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { CropOptions, IMediaRepository, + ImageDimensions, ResizeOptions, TranscodeOptions, VideoInfo, @@ -26,6 +28,23 @@ export class MediaRepository implements IMediaRepository { constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { this.logger.setContext(MediaRepository.name); } + + async extract(input: string, output: string): Promise { + try { + await exiftool.extractJpgFromRaw(input, output); + } catch (error: any) { + this.logger.debug('Could not extract JPEG from image, trying preview', error.message); + try { + await exiftool.extractPreview(input, output); + } catch (error: any) { + this.logger.debug('Could not extract preview from image', error.message); + return false; + } + } + + return true; + } + crop(input: string | Buffer, options: CropOptions): Promise { return sharp(input, { failOn: 'none' }) .pipelineColorspace('rgb16') @@ -133,6 +152,11 @@ export class MediaRepository implements IMediaRepository { return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data)); } + async getImageDimensions(input: string): Promise { + const { width = 0, height = 0 } = await sharp(input).metadata(); + return { width, height }; + } + private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) { return ffmpeg(input, { niceness: 10 }) .inputOptions(options.inputOptions) @@ -140,9 +164,4 @@ export class MediaRepository implements IMediaRepository { .output(output) .on('error', (error, stdout, stderr) => this.logger.error(stderr || error)); } - - private chainPath(existing: string, path: string) { - const separator = existing.endsWith(':') ? '' : ':'; - return `${existing}${separator}${path}`; - } } diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 8eeb0064ac..e7d37407d9 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -36,8 +36,8 @@ export class MetadataRepository implements IMetadataRepository { this.logger.log('Initializing metadata repository'); const geodataDate = await readFile(geodataDatePath, 'utf8'); + // TODO move to metadata service init const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); - if (geocodingMetadata?.lastUpdate === geodataDate) { return; } diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 5e42039bc6..ed2da7a05f 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -31,12 +31,12 @@ export class SessionRepository implements ISessionRepository { }); } - create(session: Partial): Promise { - return this.repository.save(session); + create>(dto: T): Promise { + return this.repository.save(dto); } - update(session: Partial): Promise { - return this.repository.save(session); + update>(dto: T): Promise { + return this.repository.save(dto); } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 9d83d5261f..f00e10b13c 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -143,20 +143,6 @@ describe('AuthService', () => { await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password); expect(userMock.getByEmail).toHaveBeenCalledTimes(1); }); - - it('should generate the cookie headers (insecure)', async () => { - userMock.getByEmail.mockResolvedValue(userStub.user1); - sessionMock.create.mockResolvedValue(sessionStub.valid); - await expect( - sut.login(fixtures.login, { - clientIp: '127.0.0.1', - isSecure: false, - deviceOS: '', - deviceType: '', - }), - ).resolves.toEqual(loginResponseStub.user1insecure); - expect(userMock.getByEmail).toHaveBeenCalledTimes(1); - }); }); describe('changePassword', () => { @@ -354,10 +340,7 @@ describe('AuthService', () => { sessionMock.getByToken.mockResolvedValue(sessionStub.inactive); sessionMock.update.mockResolvedValue(sessionStub.valid); const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ - user: userStub.user1, - session: sessionStub.valid, - }); + await expect(sut.validate(headers, {})).resolves.toBeDefined(); expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 7e81d15ce5..72fee12f45 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -10,23 +10,16 @@ import cookieParser from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client'; -import { - AuthType, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_AUTH_TYPE_COOKIE, - IMMICH_IS_AUTHENTICATED, - LOGIN_URL, - MOBILE_REDIRECT, -} from 'src/constants'; +import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants'; import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { UserCore } from 'src/cores/user.core'; import { AuthDto, ChangePasswordDto, + ImmichCookie, + ImmichHeader, LoginCredentialDto, - LoginResponseDto, LogoutResponseDto, OAuthAuthorizeResponseDto, OAuthCallbackDto, @@ -55,11 +48,6 @@ export interface LoginDetails { deviceOS: string; } -interface LoginResponse { - response: LoginResponseDto; - cookie: string[]; -} - interface OAuthProfile extends UserinfoResponse { email: string; } @@ -95,7 +83,7 @@ export class AuthService { custom.setHttpOptionsDefaults({ timeout: 30_000 }); } - async login(dto: LoginCredentialDto, details: LoginDetails): Promise { + async login(dto: LoginCredentialDto, details: LoginDetails) { const config = await this.configCore.getConfig(); if (!config.passwordLogin.enabled) { throw new UnauthorizedException('Password login has been disabled'); @@ -114,7 +102,7 @@ export class AuthService { throw new UnauthorizedException('Incorrect email or password'); } - return this.createLoginResponse(user, AuthType.PASSWORD, details); + return this.createLoginResponse(user, details); } async logout(auth: AuthDto, authType: AuthType): Promise { @@ -161,13 +149,13 @@ export class AuthService { } async validate(headers: IncomingHttpHeaders, params: Record): Promise { - const shareKey = (headers['x-immich-share-key'] || params.key) as string; - const session = (headers['x-immich-user-token'] || - headers['x-immich-session-token'] || + const shareKey = (headers[ImmichHeader.SHARED_LINK_TOKEN] || params.key) as string; + const session = (headers[ImmichHeader.USER_TOKEN] || + headers[ImmichHeader.SESSION_TOKEN] || params.sessionKey || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; - const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string; + const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string; if (shareKey) { return this.validateSharedLink(shareKey); @@ -204,10 +192,7 @@ export class AuthService { return { url }; } - async callback( - dto: OAuthCallbackDto, - loginDetails: LoginDetails, - ): Promise<{ response: LoginResponseDto; cookie: string[] }> { + async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) { const config = await this.configCore.getConfig(); const profile = await this.getOAuthProfile(config, dto.url); this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`); @@ -256,7 +241,7 @@ export class AuthService { }); } - return this.createLoginResponse(user, AuthType.OAUTH, loginDetails); + return this.createLoginResponse(user, loginDetails); } async link(auth: AuthDto, dto: OAuthCallbackDto): Promise { @@ -353,7 +338,7 @@ export class AuthService { private getCookieToken(headers: IncomingHttpHeaders): string | null { const cookies = cookieParser.parse(headers.cookie || ''); - return cookies[IMMICH_ACCESS_COOKIE] || null; + return cookies[ImmichCookie.ACCESS_TOKEN] || null; } async validateSharedLink(key: string | string[]): Promise { @@ -389,14 +374,14 @@ export class AuthService { private async validateSession(tokenValue: string): Promise { const hashedToken = this.cryptoRepository.hashSha256(tokenValue); - let session = await this.sessionRepository.getByToken(hashedToken); + const session = await this.sessionRepository.getByToken(hashedToken); if (session?.user) { const now = DateTime.now(); const updatedAt = DateTime.fromJSDate(session.updatedAt); const diff = now.diff(updatedAt, ['hours']); if (diff.hours > 1) { - session = await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); + await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); } return { user: session.user, session: session }; @@ -405,7 +390,7 @@ export class AuthService { throw new UnauthorizedException('Invalid user token'); } - private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) { + private async createLoginResponse(user: UserEntity, loginDetails: LoginDetails) { const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); @@ -416,28 +401,7 @@ export class AuthService { deviceType: loginDetails.deviceType, }); - const response = mapLoginResponse(user, key); - const cookie = this.getCookies(response, authType, loginDetails); - return { response, cookie }; - } - - private getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) { - const maxAge = 400 * 24 * 3600; // 400 days - - let authTypeCookie = ''; - let accessTokenCookie = ''; - let isAuthenticatedCookie = ''; - - if (isSecure) { - accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - } else { - accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Path=/; Max-Age=${maxAge}; SameSite=Lax;`; - } - return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie]; + return mapLoginResponse(user, key); } private getClaim(profile: OAuthProfile, options: ClaimOptions): T { diff --git a/server/src/services/index.ts b/server/src/services/index.ts index db3d6083e9..2305708caa 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -25,6 +25,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; import { SyncService } from 'src/services/sync.service'; import { SystemConfigService } from 'src/services/system-config.service'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; import { TagService } from 'src/services/tag.service'; import { TimelineService } from 'src/services/timeline.service'; import { TrashService } from 'src/services/trash.service'; @@ -58,6 +59,7 @@ export const services = [ StorageTemplateService, SyncService, SystemConfigService, + SystemMetadataService, TagService, TimelineService, TrashService, diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index c6301c7c33..6f02e72253 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -393,14 +393,12 @@ describe(MediaService.name, () => { }); it('should generate a P3 thumbnail for a wide gamut image', async () => { - assetMock.getByIds.mockResolvedValue([ - { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, - ]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.resize).toHaveBeenCalledWith( - '/original/path.jpg', + assetStub.imageDng.originalPath, 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', { format: ImageFormat.WEBP, @@ -415,7 +413,96 @@ describe(MediaService.name, () => { }); }); - describe('handleGenerateThumbhashThumbnail', () => { + it('should extract embedded image if enabled and available', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(mediaMock.resize.mock.calls).toEqual([ + [ + extractedPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ], + ]); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); + + it('should resize original image if embedded image is too small', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.resize.mock.calls).toEqual([ + [ + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ], + ]); + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); + + it('should resize original image if embedded image not found', async () => { + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.resize).toHaveBeenCalledWith( + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + it('should resize original image if embedded image extraction is not enabled', async () => { + configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: false }]); + assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + + expect(mediaMock.extract).not.toHaveBeenCalled(); + expect(mediaMock.resize).toHaveBeenCalledWith( + assetStub.imageDng.originalPath, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + describe('handleGenerateThumbhash', () => { it('should skip thumbhash generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); await sut.handleGenerateThumbhash({ id: assetStub.image.id }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index ca72b6cbdd..1795db86d0 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { dirname } from 'node:path'; import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; @@ -42,6 +43,7 @@ import { VAAPIConfig, VP9Config, } from 'src/utils/media'; +import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @Injectable() @@ -195,9 +197,21 @@ export class MediaService { switch (asset.type) { case AssetType.IMAGE: { - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; - const imageOptions = { format, size, colorspace, quality: image.quality }; - await this.mediaRepository.resize(asset.originalPath, path, imageOptions); + const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath); + const extractedPath = StorageCore.getTempPathInDir(dirname(path)); + const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath)); + + try { + const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize)); + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const imageOptions = { format, size, colorspace, quality: image.quality }; + + await this.mediaRepository.resize(useExtracted ? extractedPath : asset.originalPath, path, imageOptions); + } finally { + if (didExtract) { + await this.storageRepository.unlink(extractedPath); + } + } break; } @@ -527,7 +541,7 @@ export class MediaService { } } - parseBitrateToBps(bitrateString: string) { + private parseBitrateToBps(bitrateString: string) { const bitrateValue = Number.parseInt(bitrateString); if (Number.isNaN(bitrateValue)) { @@ -542,4 +556,11 @@ export class MediaService { return bitrateValue; } } + + private async shouldUseExtractedImage(extractedPath: string, targetSize: number) { + const { width, height } = await this.mediaRepository.getImageDimensions(extractedPath); + const extractedSize = Math.min(width, height); + + return extractedSize >= targetSize; + } } diff --git a/server/src/services/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts index 836909b74f..115ab4b6a1 100644 --- a/server/src/services/server-info.service.spec.ts +++ b/server/src/services/server-info.service.spec.ts @@ -1,5 +1,4 @@ import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; import { IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -207,13 +206,6 @@ describe(ServerInfoService.name, () => { }); }); - describe('setAdminOnboarding', () => { - it('should set admin onboarding to true', async () => { - await sut.setAdminOnboarding(); - expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - }); - }); - describe('getStats', () => { it('should total up usage by user', async () => { userMock.getUserStats.mockResolvedValue([ diff --git a/server/src/services/server-info.service.ts b/server/src/services/server-info.service.ts index bb092896bf..52bf8bd1d3 100644 --- a/server/src/services/server-info.service.ts +++ b/server/src/services/server-info.service.ts @@ -51,7 +51,9 @@ export class ServerInfoService { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { - await this.setAdminOnboarding(); + await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: true, + }); } } @@ -105,10 +107,6 @@ export class ServerInfoService { }; } - setAdminOnboarding(): Promise { - return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); - } - async getStatistics(): Promise { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 49bf8d6544..5f55effcac 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -129,6 +129,7 @@ const updatedConfig = Object.freeze({ previewSize: 1440, quality: 80, colorspace: Colorspace.P3, + extractEmbedded: false, }, newVersionCheck: { enabled: true, diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts new file mode 100644 index 0000000000..9d11c1c72a --- /dev/null +++ b/server/src/services/system-metadata.service.spec.ts @@ -0,0 +1,31 @@ +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { SystemMetadataService } from 'src/services/system-metadata.service'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { Mocked } from 'vitest'; + +describe(SystemMetadataService.name, () => { + let sut: SystemMetadataService; + let metadataMock: Mocked; + + beforeEach(() => { + metadataMock = newSystemMetadataRepositoryMock(); + sut = new SystemMetadataService(metadataMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('updateAdminOnboarding', () => { + it('should update isOnboarded to true', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); + }); + + it('should update isOnboarded to false', async () => { + await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); + expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); + }); + }); +}); diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts new file mode 100644 index 0000000000..e8fddfc13c --- /dev/null +++ b/server/src/services/system-metadata.service.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + AdminOnboardingResponseDto, + AdminOnboardingUpdateDto, + ReverseGeocodingStateResponseDto, +} from 'src/dtos/system-metadata.dto'; +import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; + +@Injectable() +export class SystemMetadataService { + constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {} + + async getAdminOnboarding(): Promise { + const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING); + return { isOnboarded: false, ...value }; + } + + async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise { + await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + isOnboarded: dto.isOnboarded, + }); + } + + async getReverseGeocodingState(): Promise { + const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + return { lastUpdate: null, lastImportFileName: null, ...value }; + } +} diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index d0a6d4d740..ff38ded631 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -436,7 +436,7 @@ export class AV1Config extends BaseConfig { export class NVENCConfig extends BaseHWConfig { getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC]; + return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1]; } getBaseInputOptions() { @@ -566,7 +566,7 @@ export class QSVConfig extends BaseHWConfig { } getSupportedCodecs() { - return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9]; + return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9, VideoCodec.AV1]; } // recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index bce75e1e10..cbbf751bc5 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -106,12 +106,6 @@ describe('mimeTypes', () => { expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); }); - it('should be a sorted list', () => { - const keys = Object.keys(mimeTypes.profile); - // TODO: use toSorted in NodeJS 20. - expect(keys).toEqual([...keys].sort()); - }); - for (const [extension, v] of Object.entries(mimeTypes.profile)) { it(`should lookup ${extension}`, () => { expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); @@ -128,12 +122,6 @@ describe('mimeTypes', () => { expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); }); - it('should be a sorted list', () => { - const keys = Object.keys(mimeTypes.image); - // TODO: use toSorted in NodeJS 20. - expect(keys).toEqual([...keys].sort()); - }); - it('should contain only image mime types', () => { const values = Object.values(mimeTypes.image).flat(); expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('image/'))); @@ -157,7 +145,6 @@ describe('mimeTypes', () => { it('should be a sorted list', () => { const keys = Object.keys(mimeTypes.video); - // TODO: use toSorted in NodeJS 20. expect(keys).toEqual([...keys].sort()); }); @@ -184,7 +171,6 @@ describe('mimeTypes', () => { it('should be a sorted list', () => { const keys = Object.keys(mimeTypes.sidecar); - // TODO: use toSorted in NodeJS 20. expect(keys).toEqual([...keys].sort()); }); @@ -198,4 +184,20 @@ describe('mimeTypes', () => { }); } }); + + describe('raw', () => { + it('should contain only lowercase mime types', () => { + const keys = Object.keys(mimeTypes.raw); + expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase())); + + const values = Object.values(mimeTypes.raw).flat(); + expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase())); + }); + + for (const [extension, v] of Object.entries(mimeTypes.video)) { + it(`should lookup ${extension}`, () => { + expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]); + }); + } + }); }); diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index a888e4f423..495efc9ebc 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -1,12 +1,10 @@ import { extname } from 'node:path'; import { AssetType } from 'src/entities/asset.entity'; -const image: Record = { +const raw: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], '.ari': ['image/ari', 'image/x-arriflex-ari'], '.arw': ['image/arw', 'image/x-sony-arw'], - '.avif': ['image/avif'], - '.bmp': ['image/bmp'], '.cap': ['image/cap', 'image/x-phaseone-cap'], '.cin': ['image/cin', 'image/x-phantom-cin'], '.cr2': ['image/cr2', 'image/x-canon-cr2'], @@ -16,16 +14,7 @@ const image: Record = { '.dng': ['image/dng', 'image/x-adobe-dng'], '.erf': ['image/erf', 'image/x-epson-erf'], '.fff': ['image/fff', 'image/x-hasselblad-fff'], - '.gif': ['image/gif'], - '.heic': ['image/heic'], - '.heif': ['image/heif'], - '.hif': ['image/hif'], '.iiq': ['image/iiq', 'image/x-phaseone-iiq'], - '.insp': ['image/jpeg'], - '.jpe': ['image/jpeg'], - '.jpeg': ['image/jpeg'], - '.jpg': ['image/jpeg'], - '.jxl': ['image/jxl'], '.k25': ['image/k25', 'image/x-kodak-k25'], '.kdc': ['image/kdc', 'image/x-kodak-kdc'], '.mrw': ['image/mrw', 'image/x-minolta-mrw'], @@ -33,7 +22,6 @@ const image: Record = { '.orf': ['image/orf', 'image/x-olympus-orf'], '.ori': ['image/ori', 'image/x-olympus-ori'], '.pef': ['image/pef', 'image/x-pentax-pef'], - '.png': ['image/png'], '.psd': ['image/psd', 'image/vnd.adobe.photoshop'], '.raf': ['image/raf', 'image/x-fuji-raf'], '.raw': ['image/raw', 'image/x-panasonic-raw'], @@ -42,11 +30,27 @@ const image: Record = { '.sr2': ['image/sr2', 'image/x-sony-sr2'], '.srf': ['image/srf', 'image/x-sony-srf'], '.srw': ['image/srw', 'image/x-samsung-srw'], + '.x3f': ['image/x3f', 'image/x-sigma-x3f'], +}; + +const image: Record = { + ...raw, + '.avif': ['image/avif'], + '.bmp': ['image/bmp'], + '.gif': ['image/gif'], + '.heic': ['image/heic'], + '.heif': ['image/heif'], + '.hif': ['image/hif'], + '.insp': ['image/jpeg'], + '.jpe': ['image/jpeg'], + '.jpeg': ['image/jpeg'], + '.jpg': ['image/jpeg'], + '.jxl': ['image/jxl'], + '.png': ['image/png'], '.svg': ['image/svg'], '.tif': ['image/tiff'], '.tiff': ['image/tiff'], '.webp': ['image/webp'], - '.x3f': ['image/x3f', 'image/x-sigma-x3f'], }; const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); @@ -77,22 +81,25 @@ const sidecar: Record = { '.xmp': ['application/xml', 'text/xml'], }; +const types = { ...image, ...video, ...sidecar }; + const isType = (filename: string, r: Record) => extname(filename).toLowerCase() in r; -const lookup = (filename: string) => - ({ ...image, ...video, ...sidecar })[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; +const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; export const mimeTypes = { image, profile, sidecar, video, + raw, isAsset: (filename: string) => isType(filename, image) || isType(filename, video), isImage: (filename: string) => isType(filename, image), isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), + isRaw: (filename: string) => isType(filename, raw), lookup, assetType: (filename: string) => { const contentType = lookup(filename); diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index c11c936a1a..8262b6024b 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -10,13 +10,8 @@ import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.inte import _ from 'lodash'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; -import { - CLIP_MODEL_INFO, - IMMICH_ACCESS_COOKIE, - IMMICH_API_KEY_HEADER, - IMMICH_API_KEY_NAME, - serverVersion, -} from 'src/constants'; +import { CLIP_MODEL_INFO, serverVersion } from 'src/constants'; +import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Metadata } from 'src/middleware/auth.guard'; @@ -143,14 +138,14 @@ export const useSwagger = (app: INestApplication, isDevelopment: boolean) => { scheme: 'Bearer', in: 'header', }) - .addCookieAuth(IMMICH_ACCESS_COOKIE) + .addCookieAuth(ImmichCookie.ACCESS_TOKEN) .addApiKey( { type: 'apiKey', in: 'header', - name: IMMICH_API_KEY_HEADER, + name: ImmichHeader.API_KEY, }, - IMMICH_API_KEY_NAME, + Metadata.API_KEY_SECURITY, ) .addServer('/api') .build(); diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts new file mode 100644 index 0000000000..f318ca3300 --- /dev/null +++ b/server/src/utils/response.ts @@ -0,0 +1,36 @@ +import { CookieOptions, Response } from 'express'; +import { Duration } from 'luxon'; +import { CookieResponse, ImmichCookie } from 'src/dtos/auth.dto'; + +export const respondWithCookie = (res: Response, body: T, { isSecure, values }: CookieResponse) => { + const defaults: CookieOptions = { + path: '/', + sameSite: 'lax', + httpOnly: true, + secure: isSecure, + maxAge: Duration.fromObject({ days: 400 }).toMillis(), + }; + + const cookieOptions: Record = { + [ImmichCookie.AUTH_TYPE]: defaults, + [ImmichCookie.ACCESS_TOKEN]: defaults, + // no httpOnly so that the client can know the auth state + [ImmichCookie.IS_AUTHENTICATED]: { ...defaults, httpOnly: false }, + [ImmichCookie.SHARED_LINK_TOKEN]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() }, + }; + + for (const { key, value } of values) { + const options = cookieOptions[key]; + res.cookie(key, value, options); + } + + return body; +}; + +export const respondWithoutCookie = (res: Response, body: T, cookies: ImmichCookie[]) => { + for (const cookie of cookies) { + res.clearCookie(cookie); + } + + return body; +}; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 620a6f2595..c6fe89d6fe 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -759,4 +759,45 @@ export const assetStub = { fileSizeInByte: 5000, } as ExifEntity, }), + imageDng: Object.freeze({ + id: 'asset-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.dng', + previewPath: '/uploads/user-id/thumbs/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + thumbnailPath: '/uploads/user-id/webp/path.ext', + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + isReadOnly: false, + duration: null, + isVisible: true, + isExternal: false, + livePhotoVideo: null, + livePhotoVideoId: null, + isOffline: false, + libraryId: 'library-id', + library: libraryStub.uploadLibrary1, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.jpg', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + } as ExifEntity, + }), }; diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index a4753a02e7..96a0bc0141 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -129,51 +129,21 @@ export const loginResponseStub = { }, }, user1oauth: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=oauth; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - ], + accessToken: 'cmFuZG9tLWJ5dGVz', + userId: 'user-id', + userEmail: 'immich@test.com', + name: 'immich_name', + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, }, user1password: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=password; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;', - ], - }, - user1insecure: { - response: { - accessToken: 'cmFuZG9tLWJ5dGVz', - userId: 'user-id', - userEmail: 'immich@test.com', - name: 'immich_name', - profileImagePath: '', - isAdmin: false, - shouldChangePassword: false, - }, - cookie: [ - 'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;', - 'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;', - ], + accessToken: 'cmFuZG9tLWJ5dGVz', + userId: 'user-id', + userEmail: 'immich@test.com', + name: 'immich_name', + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, }, }; diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 2eea47b6ac..da3e05fe81 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -4,9 +4,11 @@ import { Mocked, vitest } from 'vitest'; export const newMediaRepositoryMock = (): Mocked => { return { generateThumbhash: vitest.fn(), + extract: vitest.fn().mockResolvedValue(false), resize: vitest.fn(), crop: vitest.fn(), probe: vitest.fn(), transcode: vitest.fn(), + getImageDimensions: vitest.fn(), }; }; diff --git a/server/test/repositories/session.repository.mock.ts b/server/test/repositories/session.repository.mock.ts index 1a034e79f0..d510eb53f7 100644 --- a/server/test/repositories/session.repository.mock.ts +++ b/server/test/repositories/session.repository.mock.ts @@ -3,8 +3,8 @@ import { Mocked, vitest } from 'vitest'; export const newSessionRepositoryMock = (): Mocked => { return { - create: vitest.fn(), - update: vitest.fn(), + create: vitest.fn() as any, + update: vitest.fn() as any, delete: vitest.fn(), getByToken: vitest.fn(), getByUserId: vitest.fn(), diff --git a/web/package-lock.json b/web/package-lock.json index b5e3a6c2f9..89b02cc4cd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,17 +1,19 @@ { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", + "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", @@ -63,7 +65,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" @@ -1590,6 +1592,22 @@ "three": "^0.161.0" } }, + "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz", + "integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.7.2" + } + }, + "node_modules/@photo-sphere-viewer/video-plugin": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz", + "integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==", + "peerDependencies": { + "@photo-sphere-viewer/core": "5.7.2" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.24", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", diff --git a/web/package.json b/web/package.json index 8418af55aa..34c2ee83a3 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.101.0", + "version": "1.102.3", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", @@ -61,6 +61,8 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", + "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", + "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index 5b984e2305..2a1853f904 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -101,6 +101,16 @@ isEdited={config.image.colorspace !== savedConfig.image.colorspace} {disabled} /> + + (config.image.extractEmbedded = !config.image.extractEmbedded)} + isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} + {disabled} + />
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index abcb248f1c..6772ff5db0 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -20,6 +20,7 @@ mdiFolderDownloadOutline, mdiHeart, mdiHeartOutline, + mdiHistory, mdiImageAlbum, mdiImageMinusOutline, mdiImageOutline, @@ -52,6 +53,7 @@ type MenuItemEvent = | 'addToAlbum' + | 'restoreAsset' | 'addToSharedAlbum' | 'asProfileImage' | 'setAsAlbumCover' @@ -70,6 +72,7 @@ delete: void; toggleArchive: void; addToAlbum: void; + restoreAsset: void; addToSharedAlbum: void; asProfileImage: void; setAsAlbumCover: void; @@ -208,12 +211,16 @@ {#if showDownloadButton} onMenuClick('download')} text="Download" /> {/if} - onMenuClick('addToAlbum')} text="Add to album" /> - onMenuClick('addToSharedAlbum')} - text="Add to shared album" - /> + {#if asset.isTrashed} + onMenuClick('restoreAsset')} text="Restore" /> + {:else} + onMenuClick('addToAlbum')} text="Add to album" /> + onMenuClick('addToSharedAlbum')} + text="Add to shared album" + /> + {/if} {#if isOwner} {#if hasStackChildren} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 46c95636d0..28899a7525 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -27,6 +27,7 @@ getActivityStatistics, getAllAlbums, runAssetJobs, + restoreAssets, updateAsset, updateAlbumInfo, type ActivityResponseDto, @@ -49,7 +50,7 @@ import PanoramaViewer from './panorama-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; - import VideoViewer from './video-viewer.svelte'; + import VideoViewer from './video-wrapper-viewer.svelte'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; @@ -403,6 +404,22 @@ await handleGetAllAlbums(); }; + const handleRestoreAsset = async () => { + try { + await restoreAssets({ bulkIdsDto: { ids: [asset.id] } }); + asset.isTrashed = false; + + dispatch('action', { type: AssetAction.RESTORE, asset }); + + notificationController.show({ + type: NotificationType.Info, + message: `Restored asset`, + }); + } catch (error) { + handleError(error, 'Error restoring asset'); + } + }; + const toggleArchive = async () => { try { const data = await updateAsset({ @@ -556,6 +573,7 @@ on:delete={() => trashOrDelete()} on:favorite={toggleFavorite} on:addToAlbum={() => openAlbumPicker(false)} + on:restoreAsset={() => handleRestoreAsset()} on:addToSharedAlbum={() => openAlbumPicker(true)} on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} @@ -604,6 +622,7 @@ {:else} navigateAsset()} on:onVideoStarted={handleVideoStarted} @@ -624,6 +643,7 @@ {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} (shouldPlayMotionPhoto = false)} /> @@ -637,6 +657,7 @@ {:else} navigateAsset()} on:onVideoStarted={handleVideoStarted} diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte index 66d8f63099..592053e5b8 100644 --- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte @@ -1,22 +1,39 @@
- {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])} + {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])} - {:then [data, module]} - + {:then [data, module, adapter, plugins, navbar]} + {:catch} Failed to load asset {/await} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 796622e7fe..0c0e707693 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,17 +1,32 @@ + +{#if projectionType === ProjectionType.EQUIRECTANGULAR} + +{:else} + +{/if} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index a3f4c51563..8cfb0b8b16 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -169,6 +169,7 @@ switch (action) { case removeAction: case AssetAction.TRASH: + case AssetAction.RESTORE: case AssetAction.DELETE: { // find the next asset to show or close the viewer (await handleNext()) || (await handlePrevious()) || handleClose(); diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/components/slideshow-settings.svelte index d0ac5eab93..64480dfdbf 100644 --- a/web/src/lib/components/slideshow-settings.svelte +++ b/web/src/lib/components/slideshow-settings.svelte @@ -4,7 +4,14 @@ SettingInputFieldType, } from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; - import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js'; + import { + mdiArrowDownThin, + mdiArrowUpThin, + mdiFitToPageOutline, + mdiFitToScreenOutline, + mdiPanorama, + mdiShuffle, + } from '@mdi/js'; import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store'; import Button from './elements/buttons/button.svelte'; import type { RenderedOption } from './elements/dropdown.svelte'; @@ -23,6 +30,7 @@ const lookOptions: Record = { [SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' }, [SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' }, + [SlideshowLook.BlurredBackground]: { icon: mdiPanorama, title: 'Blurred background' }, }; const handleToggle = ( diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index af5558c261..98d6d742d2 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -5,7 +5,7 @@ export enum AssetAction { UNFAVORITE = 'unfavorite', TRASH = 'trash', DELETE = 'delete', - // RESTORE = 'restore', + RESTORE = 'restore', ADD = 'add', } diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index 453f216912..daa4e98791 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -16,11 +16,13 @@ export enum SlideshowNavigation { export enum SlideshowLook { Contain = 'contain', Cover = 'cover', + BlurredBackground = 'blurred-background', } export const slideshowLookCssMapping: Record = { [SlideshowLook.Contain]: 'object-contain', [SlideshowLook.Cover]: 'object-cover', + [SlideshowLook.BlurredBackground]: 'object-contain', }; function createSlideshowStore() { diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index ba9db04c74..09d36890f5 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -89,7 +89,7 @@ const handleSearch = async (force: boolean) => { $page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName); - await goto($page.url); + await goto($page.url, { keepFocus: true }); await handleSearchPeople(force); }; diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index 5be97f2821..2728f25b02 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -39,8 +39,12 @@ try { await emptyTrash(); + const deletedAssetIds = assetStore.assets.map((a) => a.id); + const numberOfAssets = deletedAssetIds.length; + assetStore.removeAssets(deletedAssetIds); + notificationController.show({ - message: `Empty trash initiated. Refresh the page to see the changes`, + message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, type: NotificationType.Info, }); } catch (error) { @@ -52,8 +56,12 @@ try { await restoreTrash(); + const restoredAssetIds = assetStore.assets.map((a) => a.id); + const numberOfAssets = restoredAssetIds.length; + assetStore.removeAssets(restoredAssetIds); + notificationController.show({ - message: `Restore trash initiated. Refresh the page to see the changes`, + message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, type: NotificationType.Info, }); } catch (error) { diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 09139a7f7e..4647ad8bde 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -5,7 +5,7 @@ import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; - import { setAdminOnboarding } from '@immich/sdk'; + import { updateAdminOnboarding } from '@immich/sdk'; let index = 0; @@ -28,7 +28,7 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { - await setAdminOnboarding(); + await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); await goto(AppRoute.PHOTOS); } else { index++;