feat: add oauth account management url (#30873)

This commit is contained in:
Sacha Brouté
2026-08-20 21:31:34 +00:00
committed by GitHub
parent 24532c4d82
commit c7f6197e75
13 changed files with 46 additions and 1 deletions
@@ -77,6 +77,7 @@ const setupOAuth = async (token: string, dto: Partial<AdminConfigOAuthDto>) => {
...defaults.oauth,
buttonText: 'Login with Immich',
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
accountManagementUrl: authServer.internal,
allowInsecureRequests: true,
...dto,
};
@@ -121,6 +121,7 @@ describe('/server', () => {
expect(body).toEqual({
loginPageMessage: '',
oauthButtonText: 'Login with OAuth',
oauthAccountManagementUrl: '',
trashDays: 30,
userDeleteDelay: 7,
isInitialized: true,
+3
View File
@@ -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",
+10
View File
@@ -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"
+4
View File
@@ -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 */
+5
View File
@@ -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<SystemConfig>({
defaultStorageQuota: null,
enabled: false,
issuerUrl: '',
accountManagementUrl: '',
endSessionEndpoint: '',
mobileOverrideEnabled: false,
mobileRedirectUri: '',
+1
View File
@@ -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'),
@@ -22,6 +22,7 @@ export type OAuthConfig = {
clientId: string;
clientSecret?: string;
issuerUrl: string;
accountManagementUrl: string;
endSessionEndpoint: string;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
@@ -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,
+1
View File
@@ -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,
@@ -154,6 +154,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
enabled: true,
},
oauth: {
accountManagementUrl: '',
autoLaunch: true,
autoRegister: true,
buttonText: 'Login with OAuth',
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { oauth } from '$lib/utils';
@@ -41,13 +42,18 @@
<section class="my-4">
<div in:fade={{ duration: 500 }}>
<div class="flex justify-end sm:ms-8">
<div class="flex justify-end gap-3 sm:ms-8">
{#if loading}
<div class="flex place-content-center place-items-center">
<LoadingSpinner />
</div>
{:else if featureFlagsManager.value.oauth}
{#if authManager.user.oauthId}
{#if serverConfigManager.value.oauthAccountManagementUrl}
<Button shape="round" size="small" href={serverConfigManager.value.oauthAccountManagementUrl}
>{$t('manage_oauth_account')}</Button
>
{/if}
<Button shape="round" size="small" onclick={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
{:else}
<Button shape="round" size="small" onclick={() => oauth.authorize(location)}>{$t('link_to_oauth')}</Button>
@@ -251,6 +251,16 @@
isEdited={configToEdit.oauth.buttonText !== config.oauth.buttonText}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_account_management_url')}
description={$t('admin.oauth_account_management_url_description')}
bind:value={configToEdit.oauth.accountManagementUrl}
required={false}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={configToEdit.oauth.accountManagementUrl !== config.oauth.accountManagementUrl}
/>
<SettingSwitch
title={$t('admin.oauth_auto_register')}
subtitle={$t('admin.oauth_auto_register_description')}