mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server): storage label claim (#3278)
* feat: storage label claim * chore: open api
This commit is contained in:
		
							
								
								
									
										6
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -2596,6 +2596,12 @@ export interface SystemConfigOAuthDto { | |||||||
|      * @memberof SystemConfigOAuthDto |      * @memberof SystemConfigOAuthDto | ||||||
|      */ |      */ | ||||||
|     'scope': string; |     'scope': string; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {string} | ||||||
|  |      * @memberof SystemConfigOAuthDto | ||||||
|  |      */ | ||||||
|  |     'storageLabelClaim': string; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/SystemConfigOAuthDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/SystemConfigOAuthDto.md
									
									
									
										generated
									
									
									
								
							| @@ -13,6 +13,7 @@ Name | Type | Description | Notes | |||||||
| **clientId** | **String** |  |  | **clientId** | **String** |  |  | ||||||
| **clientSecret** | **String** |  |  | **clientSecret** | **String** |  |  | ||||||
| **scope** | **String** |  |  | **scope** | **String** |  |  | ||||||
|  | **storageLabelClaim** | **String** |  |  | ||||||
| **buttonText** | **String** |  |  | **buttonText** | **String** |  |  | ||||||
| **autoRegister** | **bool** |  |  | **autoRegister** | **bool** |  |  | ||||||
| **autoLaunch** | **bool** |  |  | **autoLaunch** | **bool** |  |  | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ class SystemConfigOAuthDto { | |||||||
|     required this.clientId, |     required this.clientId, | ||||||
|     required this.clientSecret, |     required this.clientSecret, | ||||||
|     required this.scope, |     required this.scope, | ||||||
|  |     required this.storageLabelClaim, | ||||||
|     required this.buttonText, |     required this.buttonText, | ||||||
|     required this.autoRegister, |     required this.autoRegister, | ||||||
|     required this.autoLaunch, |     required this.autoLaunch, | ||||||
| @@ -35,6 +36,8 @@ class SystemConfigOAuthDto { | |||||||
| 
 | 
 | ||||||
|   String scope; |   String scope; | ||||||
| 
 | 
 | ||||||
|  |   String storageLabelClaim; | ||||||
|  | 
 | ||||||
|   String buttonText; |   String buttonText; | ||||||
| 
 | 
 | ||||||
|   bool autoRegister; |   bool autoRegister; | ||||||
| @@ -52,6 +55,7 @@ class SystemConfigOAuthDto { | |||||||
|      other.clientId == clientId && |      other.clientId == clientId && | ||||||
|      other.clientSecret == clientSecret && |      other.clientSecret == clientSecret && | ||||||
|      other.scope == scope && |      other.scope == scope && | ||||||
|  |      other.storageLabelClaim == storageLabelClaim && | ||||||
|      other.buttonText == buttonText && |      other.buttonText == buttonText && | ||||||
|      other.autoRegister == autoRegister && |      other.autoRegister == autoRegister && | ||||||
|      other.autoLaunch == autoLaunch && |      other.autoLaunch == autoLaunch && | ||||||
| @@ -66,6 +70,7 @@ class SystemConfigOAuthDto { | |||||||
|     (clientId.hashCode) + |     (clientId.hashCode) + | ||||||
|     (clientSecret.hashCode) + |     (clientSecret.hashCode) + | ||||||
|     (scope.hashCode) + |     (scope.hashCode) + | ||||||
|  |     (storageLabelClaim.hashCode) + | ||||||
|     (buttonText.hashCode) + |     (buttonText.hashCode) + | ||||||
|     (autoRegister.hashCode) + |     (autoRegister.hashCode) + | ||||||
|     (autoLaunch.hashCode) + |     (autoLaunch.hashCode) + | ||||||
| @@ -73,7 +78,7 @@ class SystemConfigOAuthDto { | |||||||
|     (mobileRedirectUri.hashCode); |     (mobileRedirectUri.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SystemConfigOAuthDto[enabled=$enabled, issuerUrl=$issuerUrl, clientId=$clientId, clientSecret=$clientSecret, scope=$scope, buttonText=$buttonText, autoRegister=$autoRegister, autoLaunch=$autoLaunch, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri]'; |   String toString() => 'SystemConfigOAuthDto[enabled=$enabled, issuerUrl=$issuerUrl, clientId=$clientId, clientSecret=$clientSecret, scope=$scope, storageLabelClaim=$storageLabelClaim, buttonText=$buttonText, autoRegister=$autoRegister, autoLaunch=$autoLaunch, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -82,6 +87,7 @@ class SystemConfigOAuthDto { | |||||||
|       json[r'clientId'] = this.clientId; |       json[r'clientId'] = this.clientId; | ||||||
|       json[r'clientSecret'] = this.clientSecret; |       json[r'clientSecret'] = this.clientSecret; | ||||||
|       json[r'scope'] = this.scope; |       json[r'scope'] = this.scope; | ||||||
|  |       json[r'storageLabelClaim'] = this.storageLabelClaim; | ||||||
|       json[r'buttonText'] = this.buttonText; |       json[r'buttonText'] = this.buttonText; | ||||||
|       json[r'autoRegister'] = this.autoRegister; |       json[r'autoRegister'] = this.autoRegister; | ||||||
|       json[r'autoLaunch'] = this.autoLaunch; |       json[r'autoLaunch'] = this.autoLaunch; | ||||||
| @@ -103,6 +109,7 @@ class SystemConfigOAuthDto { | |||||||
|         clientId: mapValueOfType<String>(json, r'clientId')!, |         clientId: mapValueOfType<String>(json, r'clientId')!, | ||||||
|         clientSecret: mapValueOfType<String>(json, r'clientSecret')!, |         clientSecret: mapValueOfType<String>(json, r'clientSecret')!, | ||||||
|         scope: mapValueOfType<String>(json, r'scope')!, |         scope: mapValueOfType<String>(json, r'scope')!, | ||||||
|  |         storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!, | ||||||
|         buttonText: mapValueOfType<String>(json, r'buttonText')!, |         buttonText: mapValueOfType<String>(json, r'buttonText')!, | ||||||
|         autoRegister: mapValueOfType<bool>(json, r'autoRegister')!, |         autoRegister: mapValueOfType<bool>(json, r'autoRegister')!, | ||||||
|         autoLaunch: mapValueOfType<bool>(json, r'autoLaunch')!, |         autoLaunch: mapValueOfType<bool>(json, r'autoLaunch')!, | ||||||
| @@ -160,6 +167,7 @@ class SystemConfigOAuthDto { | |||||||
|     'clientId', |     'clientId', | ||||||
|     'clientSecret', |     'clientSecret', | ||||||
|     'scope', |     'scope', | ||||||
|  |     'storageLabelClaim', | ||||||
|     'buttonText', |     'buttonText', | ||||||
|     'autoRegister', |     'autoRegister', | ||||||
|     'autoLaunch', |     'autoLaunch', | ||||||
|   | |||||||
| @@ -41,6 +41,11 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // String storageLabelClaim | ||||||
|  |     test('to test the property `storageLabelClaim`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     // String buttonText |     // String buttonText | ||||||
|     test('to test the property `buttonText`', () async { |     test('to test the property `buttonText`', () async { | ||||||
|       // TODO |       // TODO | ||||||
|   | |||||||
| @@ -6503,6 +6503,9 @@ | |||||||
|           "scope": { |           "scope": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "storageLabelClaim": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|           "buttonText": { |           "buttonText": { | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
| @@ -6525,6 +6528,7 @@ | |||||||
|           "clientId", |           "clientId", | ||||||
|           "clientSecret", |           "clientSecret", | ||||||
|           "scope", |           "scope", | ||||||
|  |           "storageLabelClaim", | ||||||
|           "buttonText", |           "buttonText", | ||||||
|           "autoRegister", |           "autoRegister", | ||||||
|           "autoLaunch", |           "autoLaunch", | ||||||
|   | |||||||
| @@ -240,11 +240,19 @@ export class AuthService { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`); |       this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`); | ||||||
|  |       this.logger.verbose(`OAuth Profile: ${JSON.stringify(profile)}`); | ||||||
|  |  | ||||||
|  |       let storageLabel: string | null = profile[config.oauth.storageLabelClaim as keyof OAuthProfile] as string; | ||||||
|  |       if (typeof storageLabel !== 'string') { | ||||||
|  |         storageLabel = null; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       user = await this.userCore.createUser({ |       user = await this.userCore.createUser({ | ||||||
|         firstName: profile.given_name || '', |         firstName: profile.given_name || '', | ||||||
|         lastName: profile.family_name || '', |         lastName: profile.family_name || '', | ||||||
|         email: profile.email, |         email: profile.email, | ||||||
|         oauthId: profile.sub, |         oauthId: profile.sub, | ||||||
|  |         storageLabel, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,6 +25,9 @@ export class SystemConfigOAuthDto { | |||||||
|   @IsString() |   @IsString() | ||||||
|   scope!: string; |   scope!: string; | ||||||
|  |  | ||||||
|  |   @IsString() | ||||||
|  |   storageLabelClaim!: string; | ||||||
|  |  | ||||||
|   @IsString() |   @IsString() | ||||||
|   buttonText!: string; |   buttonText!: string; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -48,6 +48,7 @@ export const defaults = Object.freeze<SystemConfig>({ | |||||||
|     mobileOverrideEnabled: false, |     mobileOverrideEnabled: false, | ||||||
|     mobileRedirectUri: '', |     mobileRedirectUri: '', | ||||||
|     scope: 'openid email profile', |     scope: 'openid email profile', | ||||||
|  |     storageLabelClaim: 'preferred_username', | ||||||
|     buttonText: 'Login with OAuth', |     buttonText: 'Login with OAuth', | ||||||
|     autoRegister: true, |     autoRegister: true, | ||||||
|     autoLaunch: false, |     autoLaunch: false, | ||||||
|   | |||||||
| @@ -53,6 +53,7 @@ const updatedConfig = Object.freeze<SystemConfig>({ | |||||||
|     mobileOverrideEnabled: false, |     mobileOverrideEnabled: false, | ||||||
|     mobileRedirectUri: '', |     mobileRedirectUri: '', | ||||||
|     scope: 'openid email profile', |     scope: 'openid email profile', | ||||||
|  |     storageLabelClaim: 'preferred_username', | ||||||
|   }, |   }, | ||||||
|   passwordLogin: { |   passwordLogin: { | ||||||
|     enabled: true, |     enabled: true, | ||||||
|   | |||||||
| @@ -8,9 +8,9 @@ import { | |||||||
| } from '@nestjs/common'; | } from '@nestjs/common'; | ||||||
| import { constants, createReadStream, ReadStream } from 'fs'; | import { constants, createReadStream, ReadStream } from 'fs'; | ||||||
| import fs from 'fs/promises'; | import fs from 'fs/promises'; | ||||||
|  | import sanitize from 'sanitize-filename'; | ||||||
| import { AuthUserDto } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
| import { ICryptoRepository } from '../crypto'; | import { ICryptoRepository } from '../crypto'; | ||||||
| import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto'; |  | ||||||
| import { IUserRepository, UserListFilter } from './user.repository'; | import { IUserRepository, UserListFilter } from './user.repository'; | ||||||
|  |  | ||||||
| const SALT_ROUNDS = 10; | const SALT_ROUNDS = 10; | ||||||
| @@ -67,13 +67,13 @@ export class UserCore { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async createUser(createUserDto: CreateUserDto | CreateAdminDto | CreateUserOAuthDto): Promise<UserEntity> { |   async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> { | ||||||
|     const user = await this.userRepository.getByEmail(createUserDto.email); |     const user = await this.userRepository.getByEmail(dto.email); | ||||||
|     if (user) { |     if (user) { | ||||||
|       throw new BadRequestException('User exists'); |       throw new BadRequestException('User exists'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!(createUserDto as CreateAdminDto).isAdmin) { |     if (!dto.isAdmin) { | ||||||
|       const localAdmin = await this.userRepository.getAdmin(); |       const localAdmin = await this.userRepository.getAdmin(); | ||||||
|       if (!localAdmin) { |       if (!localAdmin) { | ||||||
|         throw new BadRequestException('The first registered account must the administrator.'); |         throw new BadRequestException('The first registered account must the administrator.'); | ||||||
| @@ -81,10 +81,13 @@ export class UserCore { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const payload: Partial<UserEntity> = { ...createUserDto }; |       const payload: Partial<UserEntity> = { ...dto }; | ||||||
|       if (payload.password) { |       if (payload.password) { | ||||||
|         payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); |         payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); | ||||||
|       } |       } | ||||||
|  |       if (payload.storageLabel) { | ||||||
|  |         payload.storageLabel = sanitize(payload.storageLabel); | ||||||
|  |       } | ||||||
|       return this.userRepository.create(payload); |       return this.userRepository.create(payload); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       Logger.error(e, 'Create new user'); |       Logger.error(e, 'Create new user'); | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ export enum SystemConfigKey { | |||||||
|   OAUTH_CLIENT_ID = 'oauth.clientId', |   OAUTH_CLIENT_ID = 'oauth.clientId', | ||||||
|   OAUTH_CLIENT_SECRET = 'oauth.clientSecret', |   OAUTH_CLIENT_SECRET = 'oauth.clientSecret', | ||||||
|   OAUTH_SCOPE = 'oauth.scope', |   OAUTH_SCOPE = 'oauth.scope', | ||||||
|  |   OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim', | ||||||
|   OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch', |   OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch', | ||||||
|   OAUTH_BUTTON_TEXT = 'oauth.buttonText', |   OAUTH_BUTTON_TEXT = 'oauth.buttonText', | ||||||
|   OAUTH_AUTO_REGISTER = 'oauth.autoRegister', |   OAUTH_AUTO_REGISTER = 'oauth.autoRegister', | ||||||
| @@ -89,6 +90,7 @@ export interface SystemConfig { | |||||||
|     clientId: string; |     clientId: string; | ||||||
|     clientSecret: string; |     clientSecret: string; | ||||||
|     scope: string; |     scope: string; | ||||||
|  |     storageLabelClaim: string; | ||||||
|     buttonText: string; |     buttonText: string; | ||||||
|     autoRegister: boolean; |     autoRegister: boolean; | ||||||
|     autoLaunch: boolean; |     autoLaunch: boolean; | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -2596,6 +2596,12 @@ export interface SystemConfigOAuthDto { | |||||||
|      * @memberof SystemConfigOAuthDto |      * @memberof SystemConfigOAuthDto | ||||||
|      */ |      */ | ||||||
|     'scope': string; |     'scope': string; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {string} | ||||||
|  |      * @memberof SystemConfigOAuthDto | ||||||
|  |      */ | ||||||
|  |     'storageLabelClaim': string; | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
|   | |||||||
| @@ -155,6 +155,16 @@ | |||||||
|           isEdited={!(oauthConfig.scope == savedConfig.scope)} |           isEdited={!(oauthConfig.scope == savedConfig.scope)} | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  |         <SettingInputField | ||||||
|  |           inputType={SettingInputFieldType.TEXT} | ||||||
|  |           label="STORAGE LABEL CLAIM" | ||||||
|  |           desc="Automatically set the user's storage label to the value of this claim." | ||||||
|  |           bind:value={oauthConfig.storageLabelClaim} | ||||||
|  |           required={true} | ||||||
|  |           disabled={!oauthConfig.storageLabelClaim} | ||||||
|  |           isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)} | ||||||
|  |         /> | ||||||
|  |  | ||||||
|         <SettingInputField |         <SettingInputField | ||||||
|           inputType={SettingInputFieldType.TEXT} |           inputType={SettingInputFieldType.TEXT} | ||||||
|           label="BUTTON TEXT" |           label="BUTTON TEXT" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user