From 14db7a09e32669a693ff1ec0e7b860a859bc5825 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Dec 2022 09:43:35 -0500 Subject: [PATCH] feat(web): user profile (#1148) * fix: allow updateUser for admin account * feat: update user first/last name * feat(web): change password --- mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | Bin 11896 -> 12067 bytes mobile/openapi/doc/AuthenticationApi.md | Bin 4450 -> 5939 bytes mobile/openapi/doc/ChangePasswordDto.md | Bin 0 -> 450 bytes mobile/openapi/lib/api.dart | Bin 4132 -> 4171 bytes .../openapi/lib/api/authentication_api.dart | Bin 6483 -> 8162 bytes mobile/openapi/lib/api_client.dart | Bin 15221 -> 15307 bytes .../lib/model/change_password_dto.dart | Bin 0 -> 3678 bytes .../openapi/test/authentication_api_test.dart | Bin 1006 -> 1160 bytes .../test/change_password_dto_test.dart | Bin 0 -> 682 bytes .../immich/src/api-v1/auth/auth.controller.ts | 9 + .../immich/src/api-v1/auth/auth.service.ts | 28 +++- .../api-v1/auth/dto/change-password.dto.ts | 15 ++ .../immich/src/api-v1/user/user-repository.ts | 2 +- server/immich-openapi-specs.json | 53 ++++++ web/src/api/open-api/api.ts | 90 +++++++++- web/src/api/open-api/base.ts | 2 +- web/src/api/open-api/common.ts | 2 +- web/src/api/open-api/configuration.ts | 2 +- web/src/api/open-api/index.ts | 2 +- .../user-settings-list.svelte | 155 ++++++++++++++++-- 21 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 mobile/openapi/doc/ChangePasswordDto.md create mode 100644 mobile/openapi/lib/model/change_password_dto.dart create mode 100644 mobile/openapi/test/change_password_dto_test.dart create mode 100644 server/apps/immich/src/api-v1/auth/dto/change-password.dto.ts diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index a596cc0d83..7b27be8b20 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e29aec936e16092b74066318623bb5d61c8c866d..49b02442005a40d6f287a70baf34b2ef66e8aeae 100644 GIT binary patch delta 136 zcmewnvp8 diff --git a/mobile/openapi/doc/AuthenticationApi.md b/mobile/openapi/doc/AuthenticationApi.md index 01f106f73ffc83467422f6a3e67b61e70d8e7227..ffcece086bf1a2a29ad17b96cd6943882a5858cf 100644 GIT binary patch delta 386 zcmaE)v{`S1DI;HUMq*xiYCvLfae01G%H;Emk_Hf=0;rItLXCo!R)BwSh?bUueqw1! zhCW1rE=r*pgche31*H}j zEi*4M2Td_ZyRCvVierId8k*J$`ub?%lRq%&$)XyLZWkka47$%N_!TB^XHK<2)d;i$ x=(b$bwOS**8G*svM5!EPobBSrAzo2)BV5j2Y2jhEeOiCw;wq_Eds^Ec)1wJ5*BnYN^F+Ga5ZLNtc+z==-%%=MF zB)KTEg)qA*)KRI4xf$7E5*srhyu;I5C|`}Ki!QJupV5XmGt|Y)#S5g=Usy;PCKpm> zvXonn&i5n8IQKoCK8^lP5xd=OwhBzJF6qG0CEo9^v|c=}mi2n22U}U{UI+AS%IWHv hq8dX=bNHHGHuwLW&5VV9EpSOthun9$^Y z4rO~(q43O-43CnM0?)j(d<_f((DVb1)R}yr&j-T=hWyqd3bvT4C!Z5pHu*C1f1;cw z=@?p^S`?I8T#%nvoC>td-VV(~f)1Q)$f~J{$AhR+n?G{vGD)FXs;;A{kXWn$5}n*A L9JpDI!&DFe=tquX delta 12 TcmaE4f7xh*9LMIhe5Qf`B(nsu diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0fd1705933797f079eb752c0be11e7fde8149a40..13f5a3e02d073ad58db1444245051610147cc9ca 100644 GIT binary patch delta 46 qcmexbcDj5+gFH`SUV3UkVsUYKeo@NgiSm-D0*o5Uo9pDiO9KGSd=icT delta 12 TcmX?I{Dh0S+sWO_`^gntzkWN3;c5!E(|dR~y?TB9 z*9nS|Er5|EVW^!Nkxth#Zqu()%8(lg($gB3t6sE%&b|N z{_=C0t(nrj9=bCimmn)%vPvZQf7$D01=AW{q3DYLj*nZZ@+%|Zp$9O;3fys1t5O4l zl5b(w5uAgt0cJcwjm~yNb^v_lVAet>RpTcM$vTffoE6+v*v05qcOOuW~_)Pve@r@{_LJW9IMUKr#5gW z(q@D9+W_IQNDw1Q1+SfuMQLEg4Xl}7zmhq}Ag!|&jD*3GXIyKhc7!2>#hPoOtytts zlz0OJ_{r@AUJsic|4pThbPtiOQoqyJnfEg~^-pn_*XT5GH}6|F?^yM#vl>Ib!x<3S z7>vBpvP3ryC`(bY0@4q-1|#T*KkP4E>7B?-^}YB09OeDXmqI}-RxWsVqROJ&8Kspy zf$qoP53J0=@qh+lG<;%3%{45Q+%(2!b_pkDi2{9^sTtN}j)kiW#3_MtE3_S?pxu?? zOPpp)1gg#u$vP#Br^pLNWGPa}TISS#x6mEJ=Qapi=cev1IA3&(C$gat+%Y1*4&d6+ zbi;}y8PlfIm&lDNjXFj?DRr$l&N?&r!C}KH!0-)2s;~frkn^naPxk8oAj~3GFP8@dixei0T$YFw%n)cS2AM!N`8o zKp@*c*l3d3+kl#IsQ~S~H{oey}IaO35u-6v)}IWQFGO z-s8i~k>a{84Dnjm{A}*ZO0!5d2?i7io+Lo;iO@J5lTBp;(9twRjcvIlM3bm@N@a7K zuEnD+GbDtt8`Oy~^q|`ZMre=~+N61)L=u+gjyD2LS6*UJa{s8V|BR``2;6P*N#6^!pfuPe=WT2(8x9BYlKKl=0*J` zjZKYP3l4Z`(ilI@5_1RJ0YLU9)hfRs-IgH650IJGD!wYVTZuQ=7EB;QUUIU_MIJvAV)xVSvOC`H2=g$q=HrT{3S hsgPJ)nU~C^00bqe#U&c*AQDa8WJ6}7&G}6Ai~!^?ELs2n delta 12 TcmeC+e8;{apJ_8Eb3G#f9hw8@ diff --git a/mobile/openapi/test/change_password_dto_test.dart b/mobile/openapi/test/change_password_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..5095250fc6b33b04ca9aab037dacf1adc66f04f1 GIT binary patch literal 682 zcma)&&uhXk6vyxRE8b33aCY-F6k)Q~A#8SR-C?H~+SeM^CRvhBWca`DwJHb-+o5S5 ze7-+k!jOd_%-+-F`Y~R{ud_6cV7_>Y2M{H&Oj3AGqQ(5F?1xGPmxs zJk!c~p*)v41{VWJ3?SY_9;pnYlBJ{8PYdw zw+<#}h)3tc_BaAc%dVTm|0E8D(i3()5MTRgv zSM~Z6Mr>(yV|qbrf6fF0I9PV3Z{A8(f;mK!8oFL2*WCsD8o^lF(fTc{j^JwVBivSH iUBOmJCDSBII+NYL%V~(h&2hy)b57X*MUi6{xnsYrN!^zK literal 0 HcmV?d00001 diff --git a/server/apps/immich/src/api-v1/auth/auth.controller.ts b/server/apps/immich/src/api-v1/auth/auth.controller.ts index a2dbce89ed..53b6b02b97 100644 --- a/server/apps/immich/src/api-v1/auth/auth.controller.ts +++ b/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 { + return this.authService.changePassword(authUser, dto); + } + @Post('/logout') async logout(@Req() req: Request, @Res({ passthrough: true }) response: Response): Promise { const authType: AuthType = req.cookies[IMMICH_AUTH_TYPE_COOKIE]; diff --git a/server/apps/immich/src/api-v1/auth/auth.service.ts b/server/apps/immich/src/api-v1/auth/auth.service.ts index cfcaf58937..090b8c86aa 100644 --- a/server/apps/immich/src/api-v1/auth/auth.service.ts +++ b/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 { const adminUser = await this.userRepository.getAdmin(); diff --git a/server/apps/immich/src/api-v1/auth/dto/change-password.dto.ts b/server/apps/immich/src/api-v1/auth/dto/change-password.dto.ts new file mode 100644 index 0000000000..9c5ce479ea --- /dev/null +++ b/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; +} diff --git a/server/apps/immich/src/api-v1/user/user-repository.ts b/server/apps/immich/src/api-v1/user/user-repository.ts index e4dd2e6465..2c11607907 100644 --- a/server/apps/immich/src/api-v1/user/user-repository.ts +++ b/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'); } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index fb8bce007e..d01a7d1a79 100644 --- a/server/immich-openapi-specs.json +++ b/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": { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 156fd87f6c..38b5be8cd0 100644 --- a/web/src/api/open-api/api.ts +++ b/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 => { + // 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> { + 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 { 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 { + 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 diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index f00f196d8b..5cb76e4478 100644 --- a/web/src/api/open-api/base.ts +++ b/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). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index a946fabb54..79a5fd9335 100644 --- a/web/src/api/open-api/common.ts +++ b/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). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index 48794159a3..3d13f6f92c 100644 --- a/web/src/api/open-api/configuration.ts +++ b/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). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index f0b9d9c785..c9ec6d14c1 100644 --- a/web/src/api/open-api/index.ts +++ b/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). diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index a7a9677150..0f2589dfdd 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -1,27 +1,154 @@ - +
- +
+
+
+ - + + + + + + +
+ +
+
+
+
+
+
+ + +
+
+
+
+ + + + + + +
+ +
+
+
+