1
0
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:
Jason Rasmussen 2022-12-21 09:43:35 -05:00 committed by GitHub
parent 723a7c563f
commit 14db7a09e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 342 additions and 21 deletions

View File

@ -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

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/ChangePasswordDto.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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];

View File

@ -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();

View File

@ -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;
}

View File

@ -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');
}

View File

@ -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": {

View File

@ -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

View File

@ -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).

View File

@ -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).

View File

@ -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).

View File

@ -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).

View File

@ -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>