Browse Source

feat(web): user profile (#1148)

* fix: allow updateUser for admin account

* feat: update user first/last name

* feat(web): change password
Jason Rasmussen 2 years ago
parent
commit
14db7a09e3

+ 3 - 0
mobile/openapi/.openapi-generator/FILES

@@ -19,6 +19,7 @@ doc/AssetFileUploadResponseDto.md
 doc/AssetResponseDto.md
 doc/AssetTypeEnum.md
 doc/AuthenticationApi.md
+doc/ChangePasswordDto.md
 doc/CheckDuplicateAssetDto.md
 doc/CheckDuplicateAssetResponseDto.md
 doc/CheckExistingAssetsDto.md
@@ -114,6 +115,7 @@ lib/model/asset_count_by_user_id_response_dto.dart
 lib/model/asset_file_upload_response_dto.dart
 lib/model/asset_response_dto.dart
 lib/model/asset_type_enum.dart
+lib/model/change_password_dto.dart
 lib/model/check_duplicate_asset_dto.dart
 lib/model/check_duplicate_asset_response_dto.dart
 lib/model/check_existing_assets_dto.dart
@@ -186,6 +188,7 @@ test/asset_file_upload_response_dto_test.dart
 test/asset_response_dto_test.dart
 test/asset_type_enum_test.dart
 test/authentication_api_test.dart
+test/change_password_dto_test.dart
 test/check_duplicate_asset_dto_test.dart
 test/check_duplicate_asset_response_dto_test.dart
 test/check_existing_assets_dto_test.dart

+ 3 - 1
mobile/openapi/README.md

@@ -3,7 +3,7 @@ Immich API
 
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 
-- API version: 1.38.2
+- API version: 1.39.0
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 
 ## Requirements
@@ -96,6 +96,7 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{assetId} | 
 *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload | 
 *AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up | 
+*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | 
 *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | 
 *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | 
 *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | 
@@ -147,6 +148,7 @@ Class | Method | HTTP request | Description
  - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
  - [AssetResponseDto](doc//AssetResponseDto.md)
  - [AssetTypeEnum](doc//AssetTypeEnum.md)
+ - [ChangePasswordDto](doc//ChangePasswordDto.md)
  - [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md)
  - [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
  - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)

+ 48 - 0
mobile/openapi/doc/AuthenticationApi.md

@@ -10,6 +10,7 @@ All URIs are relative to */api*
 Method | HTTP request | Description
 ------------- | ------------- | -------------
 [**adminSignUp**](AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up | 
+[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password | 
 [**login**](AuthenticationApi.md#login) | **POST** /auth/login | 
 [**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout | 
 [**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | 
@@ -56,6 +57,53 @@ No authorization required
 
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
+# **changePassword**
+> UserResponseDto changePassword(changePasswordDto)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = AuthenticationApi();
+final changePasswordDto = ChangePasswordDto(); // ChangePasswordDto | 
+
+try {
+    final result = api_instance.changePassword(changePasswordDto);
+    print(result);
+} catch (e) {
+    print('Exception when calling AuthenticationApi->changePassword: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **changePasswordDto** | [**ChangePasswordDto**](ChangePasswordDto.md)|  | 
+
+### Return type
+
+[**UserResponseDto**](UserResponseDto.md)
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
 # **login**
 > LoginResponseDto login(loginCredentialDto)
 

+ 16 - 0
mobile/openapi/doc/ChangePasswordDto.md

@@ -0,0 +1,16 @@
+# openapi.model.ChangePasswordDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**password** | **String** |  | 
+**newPassword** | **String** |  | 
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+

+ 1 - 0
mobile/openapi/lib/api.dart

@@ -51,6 +51,7 @@ part 'model/asset_count_by_user_id_response_dto.dart';
 part 'model/asset_file_upload_response_dto.dart';
 part 'model/asset_response_dto.dart';
 part 'model/asset_type_enum.dart';
+part 'model/change_password_dto.dart';
 part 'model/check_duplicate_asset_dto.dart';
 part 'model/check_duplicate_asset_response_dto.dart';
 part 'model/check_existing_assets_dto.dart';

+ 47 - 0
mobile/openapi/lib/api/authentication_api.dart

@@ -63,6 +63,53 @@ class AuthenticationApi {
     return null;
   }
 
+  /// Performs an HTTP 'POST /auth/change-password' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [ChangePasswordDto] changePasswordDto (required):
+  Future<Response> changePasswordWithHttpInfo(ChangePasswordDto changePasswordDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/auth/change-password';
+
+    // ignore: prefer_final_locals
+    Object? postBody = changePasswordDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [ChangePasswordDto] changePasswordDto (required):
+  Future<UserResponseDto?> changePassword(ChangePasswordDto changePasswordDto,) async {
+    final response = await changePasswordWithHttpInfo(changePasswordDto,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
+    
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
   /// Parameters:
   ///

+ 2 - 0
mobile/openapi/lib/api_client.dart

@@ -218,6 +218,8 @@ class ApiClient {
           return AssetResponseDto.fromJson(value);
         case 'AssetTypeEnum':
           return AssetTypeEnumTypeTransformer().decode(value);
+        case 'ChangePasswordDto':
+          return ChangePasswordDto.fromJson(value);
         case 'CheckDuplicateAssetDto':
           return CheckDuplicateAssetDto.fromJson(value);
         case 'CheckDuplicateAssetResponseDto':

+ 119 - 0
mobile/openapi/lib/model/change_password_dto.dart

@@ -0,0 +1,119 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class ChangePasswordDto {
+  /// Returns a new [ChangePasswordDto] instance.
+  ChangePasswordDto({
+    required this.password,
+    required this.newPassword,
+  });
+
+  String password;
+
+  String newPassword;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is ChangePasswordDto &&
+     other.password == password &&
+     other.newPassword == newPassword;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (password.hashCode) +
+    (newPassword.hashCode);
+
+  @override
+  String toString() => 'ChangePasswordDto[password=$password, newPassword=$newPassword]';
+
+  Map<String, dynamic> toJson() {
+    final _json = <String, dynamic>{};
+      _json[r'password'] = password;
+      _json[r'newPassword'] = newPassword;
+    return _json;
+  }
+
+  /// Returns a new [ChangePasswordDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static ChangePasswordDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "ChangePasswordDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "ChangePasswordDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return ChangePasswordDto(
+        password: mapValueOfType<String>(json, r'password')!,
+        newPassword: mapValueOfType<String>(json, r'newPassword')!,
+      );
+    }
+    return null;
+  }
+
+  static List<ChangePasswordDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <ChangePasswordDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = ChangePasswordDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, ChangePasswordDto> mapFromJson(dynamic json) {
+    final map = <String, ChangePasswordDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = ChangePasswordDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of ChangePasswordDto-objects as value to a dart map
+  static Map<String, List<ChangePasswordDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<ChangePasswordDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = ChangePasswordDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'password',
+    'newPassword',
+  };
+}
+

+ 5 - 0
mobile/openapi/test/authentication_api_test.dart

@@ -22,6 +22,11 @@ void main() {
       // TODO
     });
 
+    //Future<UserResponseDto> changePassword(ChangePasswordDto changePasswordDto) async
+    test('test changePassword', () async {
+      // TODO
+    });
+
     //Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async
     test('test login', () async {
       // TODO

+ 32 - 0
mobile/openapi/test/change_password_dto_test.dart

@@ -0,0 +1,32 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for ChangePasswordDto
+void main() {
+  // final instance = ChangePasswordDto();
+
+  group('test ChangePasswordDto', () {
+    // String password
+    test('to test the property `password`', () async {
+      // TODO
+    });
+
+    // String newPassword
+    test('to test the property `newPassword`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 9 - 0
server/apps/immich/src/api-v1/auth/auth.controller.ts

@@ -5,7 +5,9 @@ import { AuthType, IMMICH_AUTH_TYPE_COOKIE } from '../../constants/jwt.constant'
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { Authenticated } from '../../decorators/authenticated.decorator';
 import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
+import { UserResponseDto } from '../user/response-dto/user-response.dto';
 import { AuthService } from './auth.service';
+import { ChangePasswordDto } from './dto/change-password.dto';
 import { LoginCredentialDto } from './dto/login-credential.dto';
 import { SignUpDto } from './dto/sign-up.dto';
 import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto';
@@ -45,6 +47,13 @@ export class AuthController {
     return new ValidateAccessTokenResponseDto(true);
   }
 
+  @Authenticated()
+  @ApiBearerAuth()
+  @Post('change-password')
+  async changePassword(@GetAuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
+    return this.authService.changePassword(authUser, dto);
+  }
+
   @Post('/logout')
   async logout(@Req() req: Request, @Res({ passthrough: true }) response: Response): Promise<LogoutResponseDto> {
     const authType: AuthType = req.cookies[IMMICH_AUTH_TYPE_COOKIE];

+ 27 - 1
server/apps/immich/src/api-v1/auth/auth.service.ts

@@ -1,9 +1,18 @@
-import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
+import {
+  BadRequestException,
+  Inject,
+  Injectable,
+  InternalServerErrorException,
+  Logger,
+  UnauthorizedException,
+} from '@nestjs/common';
 import * as bcrypt from 'bcrypt';
 import { UserEntity } from '../../../../../libs/database/src/entities/user.entity';
 import { AuthType } from '../../constants/jwt.constant';
+import { AuthUserDto } from '../../decorators/auth-user.decorator';
 import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
 import { IUserRepository, USER_REPOSITORY } from '../user/user-repository';
+import { ChangePasswordDto } from './dto/change-password.dto';
 import { LoginCredentialDto } from './dto/login-credential.dto';
 import { SignUpDto } from './dto/sign-up.dto';
 import { AdminSignupResponseDto, mapAdminSignupResponse } from './response-dto/admin-signup-response.dto';
@@ -48,6 +57,23 @@ export class AuthService {
     return { successful: true, redirectUri: '/auth/login' };
   }
 
+  public async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) {
+    const { password, newPassword } = dto;
+    const user = await this.userRepository.getByEmail(authUser.email, true);
+    if (!user) {
+      throw new UnauthorizedException();
+    }
+
+    const valid = await this.validatePassword(password, user);
+    if (!valid) {
+      throw new BadRequestException('Wrong password');
+    }
+
+    user.password = newPassword;
+
+    return this.userRepository.update(user.id, user);
+  }
+
   public async adminSignUp(dto: SignUpDto): Promise<AdminSignupResponseDto> {
     const adminUser = await this.userRepository.getAdmin();
 

+ 15 - 0
server/apps/immich/src/api-v1/auth/dto/change-password.dto.ts

@@ -0,0 +1,15 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString, MinLength } from 'class-validator';
+
+export class ChangePasswordDto {
+  @IsString()
+  @IsNotEmpty()
+  @ApiProperty({ example: 'password' })
+  password!: string;
+
+  @IsString()
+  @IsNotEmpty()
+  @MinLength(8)
+  @ApiProperty({ example: 'password' })
+  newPassword!: string;
+}

+ 1 - 1
server/apps/immich/src/api-v1/user/user-repository.ts

@@ -86,7 +86,7 @@ export class UserRepository implements IUserRepository {
     if (user.isAdmin) {
       const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
 
-      if (adminUser) {
+      if (adminUser && adminUser.id !== id) {
         throw new BadRequestException('Admin user exists');
       }
 

+ 53 - 0
server/immich-openapi-specs.json

@@ -1707,6 +1707,42 @@
         ]
       }
     },
+    "/auth/change-password": {
+      "post": {
+        "operationId": "changePassword",
+        "parameters": [],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/ChangePasswordDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UserResponseDto"
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Authentication"
+        ],
+        "security": [
+          {
+            "bearer": []
+          }
+        ]
+      }
+    },
     "/auth/logout": {
       "post": {
         "operationId": "logout",
@@ -3258,6 +3294,23 @@
           "authStatus"
         ]
       },
+      "ChangePasswordDto": {
+        "type": "object",
+        "properties": {
+          "password": {
+            "type": "string",
+            "example": "password"
+          },
+          "newPassword": {
+            "type": "string",
+            "example": "password"
+          }
+        },
+        "required": [
+          "password",
+          "newPassword"
+        ]
+      },
       "LogoutResponseDto": {
         "type": "object",
         "properties": {

+ 89 - 1
web/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.38.2
+ * The version of the OpenAPI document: 1.39.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -481,6 +481,25 @@ export const AssetTypeEnum = {
 export type AssetTypeEnum = typeof AssetTypeEnum[keyof typeof AssetTypeEnum];
 
 
+/**
+ * 
+ * @export
+ * @interface ChangePasswordDto
+ */
+export interface ChangePasswordDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof ChangePasswordDto
+     */
+    'password': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ChangePasswordDto
+     */
+    'newPassword': string;
+}
 /**
  * 
  * @export
@@ -4171,6 +4190,45 @@ export const AuthenticationApiAxiosParamCreator = function (configuration?: Conf
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {ChangePasswordDto} changePasswordDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        changePassword: async (changePasswordDto: ChangePasswordDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'changePasswordDto' is not null or undefined
+            assertParamExists('changePassword', 'changePasswordDto', changePasswordDto)
+            const localVarPath = `/auth/change-password`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(changePasswordDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {LoginCredentialDto} loginCredentialDto 
@@ -4288,6 +4346,16 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.adminSignUp(signUpDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {ChangePasswordDto} changePasswordDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async changePassword(changePasswordDto: ChangePasswordDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.changePassword(changePasswordDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {LoginCredentialDto} loginCredentialDto 
@@ -4335,6 +4403,15 @@ export const AuthenticationApiFactory = function (configuration?: Configuration,
         adminSignUp(signUpDto: SignUpDto, options?: any): AxiosPromise<AdminSignupResponseDto> {
             return localVarFp.adminSignUp(signUpDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {ChangePasswordDto} changePasswordDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        changePassword(changePasswordDto: ChangePasswordDto, options?: any): AxiosPromise<UserResponseDto> {
+            return localVarFp.changePassword(changePasswordDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {LoginCredentialDto} loginCredentialDto 
@@ -4381,6 +4458,17 @@ export class AuthenticationApi extends BaseAPI {
         return AuthenticationApiFp(this.configuration).adminSignUp(signUpDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {ChangePasswordDto} changePasswordDto 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuthenticationApi
+     */
+    public changePassword(changePasswordDto: ChangePasswordDto, options?: AxiosRequestConfig) {
+        return AuthenticationApiFp(this.configuration).changePassword(changePasswordDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {LoginCredentialDto} loginCredentialDto 

+ 1 - 1
web/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.38.2
+ * The version of the OpenAPI document: 1.39.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.38.2
+ * The version of the OpenAPI document: 1.39.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.38.2
+ * The version of the OpenAPI document: 1.39.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.38.2
+ * The version of the OpenAPI document: 1.39.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 142 - 15
web/src/lib/components/user-settings-page/user-settings-list.svelte

@@ -1,27 +1,154 @@
 <script lang="ts">
-	import { UserResponseDto } from '@api';
+	import {
+		notificationController,
+		NotificationType
+	} from '$lib/components/shared-components/notification/notification';
+	import { api, UserResponseDto } from '@api';
+	import { AxiosError } from 'axios';
+	import { fade } from 'svelte/transition';
 	import SettingAccordion from '../admin-page/settings/setting-accordion.svelte';
 	import SettingInputField, {
 		SettingInputFieldType
 	} from '../admin-page/settings/setting-input-field.svelte';
 
 	export let user: UserResponseDto;
+
+	const handleSaveProfile = async () => {
+		try {
+			const { data } = await api.userApi.updateUser({
+				id: user.id,
+				firstName: user.firstName,
+				lastName: user.lastName
+			});
+
+			Object.assign(user, data);
+
+			notificationController.show({
+				message: 'Saved profile',
+				type: NotificationType.Info
+			});
+		} catch (error) {
+			console.error('Error [user-profile] [updateProfile]', error);
+			notificationController.show({
+				message: 'Unable to save profile',
+				type: NotificationType.Error
+			});
+		}
+	};
+
+	let password = '';
+	let newPassword = '';
+	let confirmPassword = '';
+
+	const handleChangePassword = async () => {
+		try {
+			await api.authenticationApi.changePassword({
+				password,
+				newPassword
+			});
+
+			notificationController.show({
+				message: 'Updated password',
+				type: NotificationType.Info
+			});
+
+			password = '';
+			newPassword = '';
+			confirmPassword = '';
+		} catch (error: AxiosError | any) {
+			console.error('Error [user-profile] [changePassword]', error);
+			notificationController.show({
+				message: error?.response?.data?.message || 'Unable to change password',
+				type: NotificationType.Error
+			});
+		}
+	};
 </script>
 
-<SettingAccordion title="User profile" subtitle="Manage the user information">
+<SettingAccordion title="User Profile" subtitle="View and manage your profile">
+	<section class="my-4">
+		<div in:fade={{ duration: 500 }}>
+			<form autocomplete="off" on:submit|preventDefault>
+				<div class="flex flex-col gap-4 ml-4 mt-4">
+					<SettingInputField
+						inputType={SettingInputFieldType.TEXT}
+						label="User ID"
+						bind:value={user.id}
+						disabled={true}
+					/>
+
+					<SettingInputField
+						inputType={SettingInputFieldType.TEXT}
+						label="Email"
+						bind:value={user.email}
+						disabled={true}
+					/>
+
+					<SettingInputField
+						inputType={SettingInputFieldType.TEXT}
+						label="First name"
+						bind:value={user.firstName}
+						required={true}
+					/>
+
+					<SettingInputField
+						inputType={SettingInputFieldType.TEXT}
+						label="Last name"
+						bind:value={user.lastName}
+						required={true}
+					/>
+
+					<div class="flex justify-end">
+						<button
+							type="submit"
+							on:click={() => handleSaveProfile()}
+							class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
+							>Save
+						</button>
+					</div>
+				</div>
+			</form>
+		</div>
+	</section>
+</SettingAccordion>
+
+<SettingAccordion title="Password" subtitle="Change your password">
 	<section class="my-4">
-		<SettingInputField
-			inputType={SettingInputFieldType.TEXT}
-			label="First name"
-			bind:value={user.firstName}
-			required={true}
-		/>
-
-		<SettingInputField
-			inputType={SettingInputFieldType.TEXT}
-			label="Last name"
-			bind:value={user.lastName}
-			required={true}
-		/>
+		<div in:fade={{ duration: 500 }}>
+			<form autocomplete="off" on:submit|preventDefault>
+				<div class="flex flex-col gap-4 ml-4 mt-4">
+					<SettingInputField
+						inputType={SettingInputFieldType.PASSWORD}
+						label="Password"
+						bind:value={password}
+						required={true}
+					/>
+
+					<SettingInputField
+						inputType={SettingInputFieldType.PASSWORD}
+						label="New password"
+						bind:value={newPassword}
+						required={true}
+					/>
+
+					<SettingInputField
+						inputType={SettingInputFieldType.PASSWORD}
+						label="Confirm password"
+						bind:value={confirmPassword}
+						required={true}
+					/>
+
+					<div class="flex justify-end">
+						<button
+							type="submit"
+							disabled={!(password && newPassword && newPassword === confirmPassword)}
+							on:click={() => handleChangePassword()}
+							class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
+							>Save
+						</button>
+					</div>
+				</div>
+			</form>
+		</div>
 	</section>
 </SettingAccordion>