diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index 10d3343a7..5bb569d5d 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -77,6 +77,7 @@ const setupOAuth = async (token: string, dto: Partial) => { ...defaults.oauth, buttonText: 'Login with Immich', issuerUrl: `${authServer.internal}/.well-known/openid-configuration`, + accountManagementUrl: authServer.internal, allowInsecureRequests: true, ...dto, }; diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index c6ab9a670..ea410186c 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -121,6 +121,7 @@ describe('/server', () => { expect(body).toEqual({ loginPageMessage: '', oauthButtonText: 'Login with OAuth', + oauthAccountManagementUrl: '', trashDays: 30, userDeleteDelay: 7, isInitialized: true, diff --git a/i18n/en.json b/i18n/en.json index 92bc2d131..57a8ae950 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -294,6 +294,8 @@ "notification_enable_email_notifications": "Enable email notifications", "notification_settings": "Notification Settings", "notification_settings_description": "Manage notification settings, including email", + "oauth_account_management_url": "Account Management URL", + "oauth_account_management_url_description": "Location in the external identity provider where a user can manage their settings or profile.", "oauth_allow_insecure_requests": "Allow insecure requests", "oauth_allow_insecure_requests_description": "WARNING: This disables TLS certificate validation for OAuth requests and may expose you to MITM attacks.", "oauth_auto_launch": "Auto launch", @@ -1379,6 +1381,7 @@ "manage_media_access_settings": "Open settings", "manage_media_access_subtitle": "Allow the Immich app to manage and move media files.", "manage_media_access_title": "Media Management Access", + "manage_oauth_account": "Manage OAuth Account", "manage_sharing_with_other_users": "Manage sharing with other users", "manage_sharing_with_partners": "Manage sharing with partners", "manage_the_app_settings": "Manage the app settings", diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 584510e36..2d471be15 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18232,6 +18232,11 @@ }, "AdminConfigOAuthDto": { "properties": { + "accountManagementUrl": { + "default": "", + "description": "Account management URL", + "type": "string" + }, "allowInsecureRequests": { "description": "Allow insecure requests", "type": "boolean" @@ -24593,6 +24598,11 @@ "minimum": -9007199254740991, "type": "integer" }, + "oauthAccountManagementUrl": { + "default": "", + "description": "OAuth account management URL", + "type": "string" + }, "oauthButtonText": { "description": "OAuth button text", "type": "string" diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index f0ee02832..e511b4d58 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -313,6 +313,8 @@ export type AdminConfigNotificationsDto = { smtp: AdminConfigSmtpDto; }; export type AdminConfigOAuthDto = { + /** Account management URL */ + accountManagementUrl?: string; /** Allow insecure requests */ allowInsecureRequests: boolean; /** Auto launch */ @@ -2521,6 +2523,8 @@ export type ServerConfigDto = { mapLightStyleUrl: string; /** People min faces server default */ minFaces: number; + /** OAuth account management URL */ + oauthAccountManagementUrl?: string; /** OAuth button text */ oauthButtonText: string; /** Whether public user registration is enabled */ diff --git a/server/src/dtos/config.dto.ts b/server/src/dtos/config.dto.ts index a7dbaf0cc..b0f99996e 100644 --- a/server/src/dtos/config.dto.ts +++ b/server/src/dtos/config.dto.ts @@ -291,6 +291,10 @@ const AdminConfigSchemaWithVisibility = z defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'), enabled: configBool.describe('Enabled').meta({ visibility: Public }), issuerUrl: emptyOrUrl('Issuer URL must be an empty string or a valid URL').describe('Issuer URL'), + accountManagementUrl: emptyOrUrl('Account management URL must be an empty string or a valid URL') + .describe('Account management URL') + .optional() + .default(''), scope: z.string().describe('Scope'), prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'), endSessionEndpoint: emptyOrUrl('endSessionEndpoint must be an empty string or a valid URL').describe( @@ -668,6 +672,7 @@ export const defaults = Object.freeze({ defaultStorageQuota: null, enabled: false, issuerUrl: '', + accountManagementUrl: '', endSessionEndpoint: '', mobileOverrideEnabled: false, mobileRedirectUri: '', diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 0215558aa..f2aaa11d9 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -114,6 +114,7 @@ const ServerMediaTypesResponseSchema = z const ServerConfigSchema = z .object({ oauthButtonText: z.string().describe('OAuth button text'), + oauthAccountManagementUrl: z.string().describe('OAuth account management URL').optional().default(''), loginPageMessage: z.string().describe('Login page message'), trashDays: z.int().describe('Number of days before trashed assets are permanently deleted'), userDeleteDelay: z.int().describe('Delay in days before deleted users are permanently removed'), diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index 1a09fff70..0d56343f5 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -22,6 +22,7 @@ export type OAuthConfig = { clientId: string; clientSecret?: string; issuerUrl: string; + accountManagementUrl: string; endSessionEndpoint: string; mobileOverrideEnabled: boolean; mobileRedirectUri: string; diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index b4b1af35d..82f53bcf3 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -160,6 +160,7 @@ describe(ServerService.name, () => { await expect(sut.getSystemConfig()).resolves.toEqual({ loginPageMessage: '', oauthButtonText: 'Login with OAuth', + oauthAccountManagementUrl: '', trashDays: 30, userDeleteDelay: 7, isInitialized: false, diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 57342f950..6732099fa 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -120,6 +120,7 @@ export class ServerService extends BaseService { trashDays: config.trash.days, userDeleteDelay: config.user.deleteDelay, oauthButtonText: config.oauth.buttonText, + oauthAccountManagementUrl: config.oauth.accountManagementUrl, isInitialized, isOnboarded: onboarding?.isOnboarded || false, externalDomain: config.server.externalDomain, diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 90d91ccf4..e6ada2f0b 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -154,6 +154,7 @@ const updatedConfig = Object.freeze({ enabled: true, }, oauth: { + accountManagementUrl: '', autoLaunch: true, autoRegister: true, buttonText: 'Login with OAuth', diff --git a/web/src/routes/(user)/user-settings/OauthSettings.svelte b/web/src/routes/(user)/user-settings/OauthSettings.svelte index 0da42b2b2..2b6e20cf2 100644 --- a/web/src/routes/(user)/user-settings/OauthSettings.svelte +++ b/web/src/routes/(user)/user-settings/OauthSettings.svelte @@ -1,5 +1,6 @@