mirror of
https://github.com/immich-app/immich.git
synced 2026-09-30 13:23:21 +08:00
feat: add oauth account management url (#30873)
This commit is contained in:
@@ -77,6 +77,7 @@ const setupOAuth = async (token: string, dto: Partial<AdminConfigOAuthDto>) => {
|
|||||||
...defaults.oauth,
|
...defaults.oauth,
|
||||||
buttonText: 'Login with Immich',
|
buttonText: 'Login with Immich',
|
||||||
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
|
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
|
||||||
|
accountManagementUrl: authServer.internal,
|
||||||
allowInsecureRequests: true,
|
allowInsecureRequests: true,
|
||||||
...dto,
|
...dto,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ describe('/server', () => {
|
|||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
loginPageMessage: '',
|
loginPageMessage: '',
|
||||||
oauthButtonText: 'Login with OAuth',
|
oauthButtonText: 'Login with OAuth',
|
||||||
|
oauthAccountManagementUrl: '',
|
||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
userDeleteDelay: 7,
|
userDeleteDelay: 7,
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
|
|||||||
@@ -294,6 +294,8 @@
|
|||||||
"notification_enable_email_notifications": "Enable email notifications",
|
"notification_enable_email_notifications": "Enable email notifications",
|
||||||
"notification_settings": "Notification Settings",
|
"notification_settings": "Notification Settings",
|
||||||
"notification_settings_description": "Manage notification settings, including email",
|
"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": "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_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",
|
"oauth_auto_launch": "Auto launch",
|
||||||
@@ -1379,6 +1381,7 @@
|
|||||||
"manage_media_access_settings": "Open settings",
|
"manage_media_access_settings": "Open settings",
|
||||||
"manage_media_access_subtitle": "Allow the Immich app to manage and move media files.",
|
"manage_media_access_subtitle": "Allow the Immich app to manage and move media files.",
|
||||||
"manage_media_access_title": "Media Management Access",
|
"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_other_users": "Manage sharing with other users",
|
||||||
"manage_sharing_with_partners": "Manage sharing with partners",
|
"manage_sharing_with_partners": "Manage sharing with partners",
|
||||||
"manage_the_app_settings": "Manage the app settings",
|
"manage_the_app_settings": "Manage the app settings",
|
||||||
|
|||||||
@@ -18232,6 +18232,11 @@
|
|||||||
},
|
},
|
||||||
"AdminConfigOAuthDto": {
|
"AdminConfigOAuthDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"accountManagementUrl": {
|
||||||
|
"default": "",
|
||||||
|
"description": "Account management URL",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"allowInsecureRequests": {
|
"allowInsecureRequests": {
|
||||||
"description": "Allow insecure requests",
|
"description": "Allow insecure requests",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -24593,6 +24598,11 @@
|
|||||||
"minimum": -9007199254740991,
|
"minimum": -9007199254740991,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"oauthAccountManagementUrl": {
|
||||||
|
"default": "",
|
||||||
|
"description": "OAuth account management URL",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"oauthButtonText": {
|
"oauthButtonText": {
|
||||||
"description": "OAuth button text",
|
"description": "OAuth button text",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -313,6 +313,8 @@ export type AdminConfigNotificationsDto = {
|
|||||||
smtp: AdminConfigSmtpDto;
|
smtp: AdminConfigSmtpDto;
|
||||||
};
|
};
|
||||||
export type AdminConfigOAuthDto = {
|
export type AdminConfigOAuthDto = {
|
||||||
|
/** Account management URL */
|
||||||
|
accountManagementUrl?: string;
|
||||||
/** Allow insecure requests */
|
/** Allow insecure requests */
|
||||||
allowInsecureRequests: boolean;
|
allowInsecureRequests: boolean;
|
||||||
/** Auto launch */
|
/** Auto launch */
|
||||||
@@ -2521,6 +2523,8 @@ export type ServerConfigDto = {
|
|||||||
mapLightStyleUrl: string;
|
mapLightStyleUrl: string;
|
||||||
/** People min faces server default */
|
/** People min faces server default */
|
||||||
minFaces: number;
|
minFaces: number;
|
||||||
|
/** OAuth account management URL */
|
||||||
|
oauthAccountManagementUrl?: string;
|
||||||
/** OAuth button text */
|
/** OAuth button text */
|
||||||
oauthButtonText: string;
|
oauthButtonText: string;
|
||||||
/** Whether public user registration is enabled */
|
/** Whether public user registration is enabled */
|
||||||
|
|||||||
@@ -291,6 +291,10 @@ const AdminConfigSchemaWithVisibility = z
|
|||||||
defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'),
|
defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'),
|
||||||
enabled: configBool.describe('Enabled').meta({ visibility: Public }),
|
enabled: configBool.describe('Enabled').meta({ visibility: Public }),
|
||||||
issuerUrl: emptyOrUrl('Issuer URL must be an empty string or a valid URL').describe('Issuer URL'),
|
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'),
|
scope: z.string().describe('Scope'),
|
||||||
prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'),
|
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(
|
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,
|
defaultStorageQuota: null,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
issuerUrl: '',
|
issuerUrl: '',
|
||||||
|
accountManagementUrl: '',
|
||||||
endSessionEndpoint: '',
|
endSessionEndpoint: '',
|
||||||
mobileOverrideEnabled: false,
|
mobileOverrideEnabled: false,
|
||||||
mobileRedirectUri: '',
|
mobileRedirectUri: '',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ const ServerMediaTypesResponseSchema = z
|
|||||||
const ServerConfigSchema = z
|
const ServerConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
oauthButtonText: z.string().describe('OAuth button text'),
|
oauthButtonText: z.string().describe('OAuth button text'),
|
||||||
|
oauthAccountManagementUrl: z.string().describe('OAuth account management URL').optional().default(''),
|
||||||
loginPageMessage: z.string().describe('Login page message'),
|
loginPageMessage: z.string().describe('Login page message'),
|
||||||
trashDays: z.int().describe('Number of days before trashed assets are permanently deleted'),
|
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'),
|
userDeleteDelay: z.int().describe('Delay in days before deleted users are permanently removed'),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type OAuthConfig = {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
issuerUrl: string;
|
issuerUrl: string;
|
||||||
|
accountManagementUrl: string;
|
||||||
endSessionEndpoint: string;
|
endSessionEndpoint: string;
|
||||||
mobileOverrideEnabled: boolean;
|
mobileOverrideEnabled: boolean;
|
||||||
mobileRedirectUri: string;
|
mobileRedirectUri: string;
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ describe(ServerService.name, () => {
|
|||||||
await expect(sut.getSystemConfig()).resolves.toEqual({
|
await expect(sut.getSystemConfig()).resolves.toEqual({
|
||||||
loginPageMessage: '',
|
loginPageMessage: '',
|
||||||
oauthButtonText: 'Login with OAuth',
|
oauthButtonText: 'Login with OAuth',
|
||||||
|
oauthAccountManagementUrl: '',
|
||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
userDeleteDelay: 7,
|
userDeleteDelay: 7,
|
||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export class ServerService extends BaseService {
|
|||||||
trashDays: config.trash.days,
|
trashDays: config.trash.days,
|
||||||
userDeleteDelay: config.user.deleteDelay,
|
userDeleteDelay: config.user.deleteDelay,
|
||||||
oauthButtonText: config.oauth.buttonText,
|
oauthButtonText: config.oauth.buttonText,
|
||||||
|
oauthAccountManagementUrl: config.oauth.accountManagementUrl,
|
||||||
isInitialized,
|
isInitialized,
|
||||||
isOnboarded: onboarding?.isOnboarded || false,
|
isOnboarded: onboarding?.isOnboarded || false,
|
||||||
externalDomain: config.server.externalDomain,
|
externalDomain: config.server.externalDomain,
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
|
accountManagementUrl: '',
|
||||||
autoLaunch: true,
|
autoLaunch: true,
|
||||||
autoRegister: true,
|
autoRegister: true,
|
||||||
buttonText: 'Login with OAuth',
|
buttonText: 'Login with OAuth',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { oauth } from '$lib/utils';
|
import { oauth } from '$lib/utils';
|
||||||
@@ -41,13 +42,18 @@
|
|||||||
|
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
<div in:fade={{ duration: 500 }}>
|
<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}
|
{#if loading}
|
||||||
<div class="flex place-content-center place-items-center">
|
<div class="flex place-content-center place-items-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
{:else if featureFlagsManager.value.oauth}
|
{:else if featureFlagsManager.value.oauth}
|
||||||
{#if authManager.user.oauthId}
|
{#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>
|
<Button shape="round" size="small" onclick={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button shape="round" size="small" onclick={() => oauth.authorize(location)}>{$t('link_to_oauth')}</Button>
|
<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}
|
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
|
<SettingSwitch
|
||||||
title={$t('admin.oauth_auto_register')}
|
title={$t('admin.oauth_auto_register')}
|
||||||
subtitle={$t('admin.oauth_auto_register_description')}
|
subtitle={$t('admin.oauth_auto_register_description')}
|
||||||
|
|||||||
Reference in New Issue
Block a user