From 538d5c81ea0f4bc1c58eaa6e1c1bd30893a1e45c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 8 Aug 2025 15:42:38 -0400 Subject: [PATCH] feat: reset oauth ids (#20798) --- i18n/en.json | 4 ++ mobile/openapi/README.md | Bin 39029 -> 39167 bytes mobile/openapi/lib/api.dart | Bin 14064 -> 14096 bytes mobile/openapi/lib/api/auth_admin_api.dart | Bin 0 -> 1533 bytes mobile/openapi/lib/model/permission.dart | Bin 23843 -> 24034 bytes open-api/immich-openapi-specs.json | 31 +++++++- open-api/typescript-sdk/src/fetch-client.ts | 12 +++- .../src/controllers/auth-admin.controller.ts | 18 +++++ server/src/controllers/index.ts | 2 + server/src/enum.ts | 2 + server/src/repositories/user.repository.ts | 4 ++ server/src/services/auth-admin.service.ts | 11 +++ server/src/services/index.ts | 2 + .../specs/services/auth-admin.service.spec.ts | 66 ++++++++++++++++++ .../settings/auth/auth-settings.svelte | 42 +++++++++-- 15 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 mobile/openapi/lib/api/auth_admin_api.dart create mode 100644 server/src/controllers/auth-admin.controller.ts create mode 100644 server/src/services/auth-admin.service.ts create mode 100644 server/test/medium/specs/services/auth-admin.service.spec.ts diff --git a/i18n/en.json b/i18n/en.json index bc52dbd85..2554bb578 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -355,6 +355,9 @@ "trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them", "trash_settings": "Trash Settings", "trash_settings_description": "Manage trash settings", + "unlink_all_oauth_accounts": "Unlink all OAuth accounts", + "unlink_all_oauth_accounts_description": "Remember to unlink all OAuth accounts before migrating to a new provider.", + "unlink_all_oauth_accounts_prompt": "Are you sure you want to unlink all OAuth accounts? This will reset the OAuth ID for each user and cannot be undone.", "user_cleanup_job": "User cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", @@ -921,6 +924,7 @@ "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "quota_higher_than_disk_size": "You set a quota higher than the disk size", + "something_went_wrong": "Something went wrong", "unable_to_add_album_users": "Unable to add users to album", "unable_to_add_assets_to_shared_link": "Unable to add assets to shared link", "unable_to_add_comment": "Unable to add comment", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index c4349ff6570fda51c9a347c21d3acc6673ea9aee..2397d55c7845d74a5b98d0859aa5390b49fdbae5 100644 GIT binary patch delta 128 zcmeymf$9H7rVUrcA{|q5GxHn^GPM+H6r#1XO7n6u^RgXta{L`jOEMgjlk-dSN{YdX zw6tP1Qu34a^}#YwZF;#W$`Cz?IXU@>Ks||2J(C4>B>6!C`Ve_tpn}PY60)0LiuE)C E0Ax=vfdBvi delta 18 acmeyrk?HFOrVUrcCU;7TY%Uh>X#@aS{0N8u diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8c1fa1a80ac07287d39833a89d496d625a6795b2..8ecb9cd5f55032ad20341c00ee45cf58b69cab72 100644 GIT binary patch delta 16 Ycmey6J0Wkw5$4H%S!6aJU^XxY07#_=TmS$7 delta 12 TcmbP`_aS$~5$4TLn01T+D@+B< diff --git a/mobile/openapi/lib/api/auth_admin_api.dart b/mobile/openapi/lib/api/auth_admin_api.dart new file mode 100644 index 0000000000000000000000000000000000000000..d22b449aab0613b0c328b1f1ff281d083f85ab21 GIT binary patch literal 1533 zcmcIkOHbQC5Wf3YOixLqV7EOX9!Ud)5~(FAxT-3I%wmu6md&o&T|+IzfA8$tPC^x- zUV89jX6O0MHycGk6v5Zle=U~ zdY4So*P=EQT$}CgsglMrX%n7NLY{Nf-ra0XCb&eC2qo{x%oa>0yYG{yjDbmIi%GWRa#C`54&arde?{73!Kr{TkX!S6J%GKn1cw{{<1u8I zvr5>NBc%e!xnu%Xy0wQtjR^3#;091;rQ*^8{WA&7)wQFfs6ngB;*gXXv|t*r1;UND zaZQAkBoOW=Lfk-!ddZF9N`{o2!Z&X?SC&TUQxX1!a=~n+WfSWxju&usfut0eSZ$_$e}fC#ol6m8TUGAqrpzq0ny2uy>+^E z3!P@O(2$$yva~gfK~_T0HoQ9ZJKgvV94;@elmE=|mZ*=4XK#}9FJd+<&@JWNDnBu9 z|DEN{*m(~9E=EX~maWXO$`D3};1=T2YrepWb{fQf;l;x zn^e_!C)*jZOJV59$8kWn*1R~Wb=Nbc+JTZ+}J1gN3-&P)WXb|>|h}=DTYND Li~h|bG5m}GNH{l9 delta 24 gcmaE~n{n|j#tm(%o3E)V@op9~F4Nq6H-?oF0Ev { + return this.service.unlinkAll(auth); + } +} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 9c39e580b..137abf103 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -4,6 +4,7 @@ import { APIKeyController } from 'src/controllers/api-key.controller'; import { AppController } from 'src/controllers/app.controller'; import { AssetMediaController } from 'src/controllers/asset-media.controller'; import { AssetController } from 'src/controllers/asset.controller'; +import { AuthAdminController } from 'src/controllers/auth-admin.controller'; import { AuthController } from 'src/controllers/auth.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { DuplicateController } from 'src/controllers/duplicate.controller'; @@ -40,6 +41,7 @@ export const controllers = [ AssetController, AssetMediaController, AuthController, + AuthAdminController, DownloadController, DuplicateController, FaceController, diff --git a/server/src/enum.ts b/server/src/enum.ts index 8c7ee85a3..02ef22288 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -235,6 +235,8 @@ export enum Permission { AdminUserRead = 'adminUser.read', AdminUserUpdate = 'adminUser.update', AdminUserDelete = 'adminUser.delete', + + AdminAuthUnlinkAll = 'adminAuth.unlinkAll', } export enum SharedLinkType { diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 9d5f19b26..a63a4cc55 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -194,6 +194,10 @@ export class UserRepository { .executeTakeFirstOrThrow(); } + async updateAll(dto: Updateable) { + await this.db.updateTable('user').set(dto).execute(); + } + restore(id: string) { return this.db .updateTable('user') diff --git a/server/src/services/auth-admin.service.ts b/server/src/services/auth-admin.service.ts new file mode 100644 index 000000000..3648a1995 --- /dev/null +++ b/server/src/services/auth-admin.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { BaseService } from 'src/services/base.service'; + +@Injectable() +export class AuthAdminService extends BaseService { + async unlinkAll(_auth: AuthDto) { + // TODO replace '' with null + await this.userRepository.updateAll({ oauthId: '' }); + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 88b68d2c1..cad38ca1f 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -5,6 +5,7 @@ import { ApiService } from 'src/services/api.service'; import { AssetMediaService } from 'src/services/asset-media.service'; import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; +import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; import { BackupService } from 'src/services/backup.service'; import { CliService } from 'src/services/cli.service'; @@ -49,6 +50,7 @@ export const services = [ AssetService, AuditService, AuthService, + AuthAdminService, BackupService, CliService, DatabaseService, diff --git a/server/test/medium/specs/services/auth-admin.service.spec.ts b/server/test/medium/specs/services/auth-admin.service.spec.ts new file mode 100644 index 000000000..fa2a69f66 --- /dev/null +++ b/server/test/medium/specs/services/auth-admin.service.spec.ts @@ -0,0 +1,66 @@ +import { Kysely } from 'kysely'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { DB } from 'src/schema'; +import { AuthAdminService } from 'src/services/auth-admin.service'; +import { newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(AuthAdminService, { + database: db || defaultDatabase, + real: [UserRepository], + mock: [LoggingRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AuthAdminService.name, () => { + describe('unlinkAll', () => { + it('should reset user.oauthId', async () => { + const { sut, ctx } = setup(); + const userRepo = ctx.get(UserRepository); + const { user } = await ctx.newUser({ oauthId: 'test-oauth-id' }); + const auth = factory.auth(); + + await expect(sut.unlinkAll(auth)).resolves.toBeUndefined(); + await expect(userRepo.get(user.id, { withDeleted: true })).resolves.toEqual( + expect.objectContaining({ oauthId: '' }), + ); + }); + + it('should reset a deleted user', async () => { + const { sut, ctx } = setup(); + const userRepo = ctx.get(UserRepository); + const { user } = await ctx.newUser({ oauthId: 'test-oauth-id', deletedAt: new Date() }); + const auth = factory.auth(); + + await expect(sut.unlinkAll(auth)).resolves.toBeUndefined(); + await expect(userRepo.get(user.id, { withDeleted: true })).resolves.toEqual( + expect.objectContaining({ oauthId: '' }), + ); + }); + + it('should reset multiple users', async () => { + const { sut, ctx } = setup(); + const userRepo = ctx.get(UserRepository); + const { user: user1 } = await ctx.newUser({ oauthId: '1' }); + const { user: user2 } = await ctx.newUser({ oauthId: '2', deletedAt: new Date() }); + const auth = factory.auth(); + + await expect(sut.unlinkAll(auth)).resolves.toBeUndefined(); + await expect(userRepo.get(user1.id, { withDeleted: true })).resolves.toEqual( + expect.objectContaining({ oauthId: '' }), + ); + await expect(userRepo.get(user2.id, { withDeleted: true })).resolves.toEqual( + expect.objectContaining({ oauthId: '' }), + ); + }); + }); +}); diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index ce6dc2617..ef371910c 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -1,5 +1,9 @@
@@ -56,7 +82,7 @@ subtitle={$t('admin.oauth_settings_description')} >