mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(web): user profile (#1148)
* fix: allow updateUser for admin account * feat: update user first/last name * feat(web): change password
This commit is contained in:
parent
723a7c563f
commit
14db7a09e3
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@ -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
|
||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AuthenticationApi.md
generated
BIN
mobile/openapi/doc/AuthenticationApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ChangePasswordDto.md
generated
Normal file
BIN
mobile/openapi/doc/ChangePasswordDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/change_password_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/change_password_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/authentication_api_test.dart
generated
BIN
mobile/openapi/test/authentication_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/change_password_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/change_password_dto_test.dart
generated
Normal file
Binary file not shown.
@ -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];
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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": {
|
||||
|
90
web/src/api/open-api/api.ts
generated
90
web/src/api/open-api/api.ts
generated
@ -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
|
||||
|
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@ -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).
|
||||
|
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@ -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).
|
||||
|
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@ -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).
|
||||
|
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@ -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,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">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="First name"
|
||||
bind:value={user.firstName}
|
||||
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.TEXT}
|
||||
label="User ID"
|
||||
bind:value={user.id}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Last name"
|
||||
bind:value={user.lastName}
|
||||
required={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">
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user