diff --git a/backend/src/file/file.service.ts b/backend/src/file/file.service.ts index e2a8e62..868c121 100644 --- a/backend/src/file/file.service.ts +++ b/backend/src/file/file.service.ts @@ -25,6 +25,12 @@ export class FileService { return newFile; } + async deleteDatabaseFile(id: string) { + const file = await this.fileRepository.findOne({ id }); + + await this.fileRepository.remove(file); + } + async getFileById(fileId: string) { const file = await this.fileRepository.findOne(fileId); if (!file) { diff --git a/backend/src/graphql.ts b/backend/src/graphql.ts index b0120de..a8bfb1d 100644 --- a/backend/src/graphql.ts +++ b/backend/src/graphql.ts @@ -11,6 +11,18 @@ export interface SendMessageInput { message: string; } +export interface UpdateInput { + email: string; + firstName: string; + lastName: string; +} + +export interface UpdatePasswordInput { + password: string; + newPassword: string; + repeatedPassword: string; +} + export interface SignUpInput { email: string; password: string; @@ -42,6 +54,8 @@ export interface IMutation { createChat(user_to?: string): Chat | Promise; setMessagesRead(message_ids?: string[], chat_id?: string): boolean | Promise; sendMessage(input?: SendMessageInput): Message | Promise; + updatePassword(input?: UpdatePasswordInput): string | Promise; + updateUser(input?: UpdateInput, file?: Upload): string | Promise; signUp(input?: SignUpInput, file?: Upload): string | Promise; signIn(input?: SignInInput): string | Promise; } diff --git a/backend/src/message/message.service.ts b/backend/src/message/message.service.ts index dd3d2e3..5ab9a09 100644 --- a/backend/src/message/message.service.ts +++ b/backend/src/message/message.service.ts @@ -32,8 +32,7 @@ export class MessageService { async setMessagesRead(message_ids: string[]) { await this.messageRepo .createQueryBuilder('message') - .update(MessageEntity) - .set({ read: true }) + .update(MessageEntity, { read: true }) .where('id IN (:...id)', { id: message_ids }) .execute(); } diff --git a/backend/src/user/dto/update.inputs.ts b/backend/src/user/dto/update.inputs.ts new file mode 100644 index 0000000..28953b2 --- /dev/null +++ b/backend/src/user/dto/update.inputs.ts @@ -0,0 +1,25 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class UpdateInput { + @Field() + email: string; + + @Field() + firstName: string; + + @Field() + lastName: string; +} + +@InputType() +export class UpdatePasswordInput { + @Field() + password: string; + + @Field() + newPassword: string; + + @Field() + repeatedPassword: string; +} diff --git a/backend/src/user/user.graphql b/backend/src/user/user.graphql index 3da5d87..ad6dcdb 100644 --- a/backend/src/user/user.graphql +++ b/backend/src/user/user.graphql @@ -17,10 +17,24 @@ type Query { } type Mutation { + updatePassword(input: UpdatePasswordInput): String + updateUser(input: UpdateInput, file: Upload): String signUp(input: SignUpInput, file: Upload): String signIn(input: SignInInput): String } +input UpdateInput { + email: String! + firstName: String! + lastName: String! +} + +input UpdatePasswordInput { + password: String! + newPassword: String! + repeatedPassword: String! +} + input SignUpInput { email: String! password: String! diff --git a/backend/src/user/user.resolver.ts b/backend/src/user/user.resolver.ts index db6ef19..41605b0 100644 --- a/backend/src/user/user.resolver.ts +++ b/backend/src/user/user.resolver.ts @@ -14,6 +14,8 @@ import { AuthGuard } from '../guards/auth.guard'; import { UserEntity } from './user.entity'; import { CurrentUser } from './user.decorator'; import { pubsub } from '../lib/pubsub'; +import { FileUpload } from 'graphql-upload'; +import { UpdateInput, UpdatePasswordInput } from './dto/update.inputs'; @Resolver('User') export class UserResolver { @@ -39,19 +41,13 @@ export class UserResolver { @Mutation() async signIn(@Args('input') signInInput: SignInInput) { - const user = await this.userService.signIn({ ...signInInput }); - - if (user) { - return this.userService.createToken(user); - } else { - throw new UnprocessableEntityException('Invalid credentials 🤥'); - } + return await this.userService.signIn({ ...signInInput }); } @Mutation() async signUp( @Args('input') signUpInput: SignUpInput, - @Args('file') file: any, + @Args('file') file: { file: FileUpload }, ) { const _file = await file; const user = await this.userService.createUser( @@ -62,6 +58,30 @@ export class UserResolver { return this.userService.createToken(user); } + @Mutation() + @UseGuards(new AuthGuard()) + async updateUser( + @Args('input') input: UpdateInput, + @Args('file') file: { file: FileUpload }, + @CurrentUser() user: UserEntity, + ) { + const _file = await file; + return await this.userService.updateUser( + user.id, + { ...input }, + _file?.file, + ); + } + + @Mutation() + @UseGuards(new AuthGuard()) + async updatePassword( + @Args('input') input: UpdatePasswordInput, + @CurrentUser() user: UserEntity, + ) { + return await this.userService.updatePassword(user.id, { ...input }); + } + @Subscription(() => UserEntity) userRegistred() { return pubsub.asyncIterator('userRegistred'); diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index c3965c6..34cf04b 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { UserEntity } from './user.entity'; import { ILike, Repository } from 'typeorm'; @@ -7,6 +11,7 @@ import * as bcrypt from 'bcryptjs'; import { SignInInput, SignUpInput } from './dto/auth.inputs'; import { FileService } from 'src/file/file.service'; import { FileUpload } from 'graphql-upload'; +import { UpdateInput, UpdatePasswordInput } from './dto/update.inputs'; @Injectable() export class UserService { @@ -49,7 +54,7 @@ export class UserService { const avatar = await this.fileService.uploadDatabaseFile(file); return this.userRepo - .create({ ...user, password, avatar }) + .create({ ...user, email: user.email.toLowerCase(), password, avatar }) .save() .catch((e) => { if (/(email)[\s\S]+(already exists)/.test(e.detail)) { @@ -61,16 +66,64 @@ export class UserService { }); } + async updateUser(user_id: string, input: UpdateInput, file: FileUpload) { + const avatar = file && (await this.fileService.uploadDatabaseFile(file)); + let oldUser; + if (avatar) { + oldUser = await this.userRepo.findOne(user_id, { + relations: ['avatar'], + }); + } + + await this.userRepo + .createQueryBuilder('user') + .update(UserEntity, avatar ? { ...input, avatar } : { ...input }) + .where('id = :id', { id: user_id }) + .execute(); + + if (avatar) { + await this.fileService.deleteDatabaseFile(oldUser.avatar.id); + } + + const user = await this.userRepo.findOne(user_id, { + relations: ['avatar'], + }); + return await this.createToken(user); + } + + async updatePassword( + user_id: string, + { password, newPassword, repeatedPassword }: UpdatePasswordInput, + ) { + const user = await this.userRepo.findOne({ id: user_id }); + const isCorrectPassword = await bcrypt.compare(password, user.password); + if (!isCorrectPassword) + throw new UnprocessableEntityException('Invalid current password 🤥'); + if (newPassword !== repeatedPassword) + throw new UnprocessableEntityException( + 'Check if the input is correct 🤥', + ); + + await this.userRepo + .createQueryBuilder('user') + .update(UserEntity, { password: await bcrypt.hash(newPassword, 10) }) + .where('id = :id', { id: user_id }) + .execute(); + + return true; + } + async signIn({ email, password }: SignInInput) { const user = await this.userRepo.findOne({ relations: ['avatar'], where: { email: email.toLowerCase() }, }); - console.log(user); - if (!user) return false; + if (!user) throw new UnprocessableEntityException('Invalid credentials 🤥'); const isCorrectPassword = await bcrypt.compare(password, user.password); + if (!isCorrectPassword) + throw new UnprocessableEntityException('Invalid credentials 🤥'); - return isCorrectPassword ? user : false; + return this.createToken(user); } async newUsers() { diff --git a/frontend/src/assets/svg/eye-hidden.svg b/frontend/src/assets/svg/eye-hidden.svg new file mode 100644 index 0000000..cfad3be --- /dev/null +++ b/frontend/src/assets/svg/eye-hidden.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/svg/eye.svg b/frontend/src/assets/svg/eye.svg new file mode 100644 index 0000000..38f0681 --- /dev/null +++ b/frontend/src/assets/svg/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/svg/upload.svg b/frontend/src/assets/svg/upload.svg new file mode 100644 index 0000000..6e5c1de --- /dev/null +++ b/frontend/src/assets/svg/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/sidebar/chats-list/chats-list.tsx b/frontend/src/components/sidebar/chats-list/chats-list.tsx index 6006559..61763c7 100644 --- a/frontend/src/components/sidebar/chats-list/chats-list.tsx +++ b/frontend/src/components/sidebar/chats-list/chats-list.tsx @@ -63,7 +63,9 @@ export const ChatsList = () => { )} {chats?.map((chat: TChat) => { const data = { - user: chat.users.filter((user: TUser) => user.id !== me?.id)[0], + user: chat.users.filter( + (user: Omit) => user.id !== me?.id + )[0], date: chat.messages[0]?.createdAt, message: chat.messages[0]?.message, isMessageFromMe: chat.messages[0]?.user_from.id === me?.id, diff --git a/frontend/src/features/constants/index.ts b/frontend/src/features/constants/index.ts index 0e31a2b..b9ece1f 100644 --- a/frontend/src/features/constants/index.ts +++ b/frontend/src/features/constants/index.ts @@ -1,2 +1,3 @@ export const AUTH_TOKEN = "auth-token"; export const API = "localhost:5000"; +export const ALERT_SECONDS = 5; diff --git a/frontend/src/features/enum.ts b/frontend/src/features/enum.ts new file mode 100644 index 0000000..80e3ef7 --- /dev/null +++ b/frontend/src/features/enum.ts @@ -0,0 +1,4 @@ +export enum AlertTypes { + SUCCESS = "success", + DANGER = "danger", +} diff --git a/frontend/src/features/helpers/getImage.ts b/frontend/src/features/helpers/getImage.ts new file mode 100644 index 0000000..5b3c1cf --- /dev/null +++ b/frontend/src/features/helpers/getImage.ts @@ -0,0 +1,4 @@ +import { API } from "@features/constants"; + +export const getImage = (id?: string) => + id ? `http://${API}/file/${id}` : null; diff --git a/frontend/src/features/models.ts b/frontend/src/features/models.ts index 76abda3..6f9bbe0 100644 --- a/frontend/src/features/models.ts +++ b/frontend/src/features/models.ts @@ -6,8 +6,9 @@ export const loginModel = { export const registerModel = { email: "email", password: "password", + newPassword: "newPassword", + repeatedPassword: "repeatedPassword", firstName: "firstName", lastName: "lastName", avatar: "avatar", - description: "description", }; diff --git a/frontend/src/features/types.ts b/frontend/src/features/types.ts index b2b471d..b9e92ae 100644 --- a/frontend/src/features/types.ts +++ b/frontend/src/features/types.ts @@ -2,6 +2,7 @@ export type TUser = { id: string; firstName: string; lastName: string; + email: string; message?: string; avatar?: { filename: string; diff --git a/frontend/src/pages/Auth/login-block/login-block.tsx b/frontend/src/pages/Auth/login-block/login-block.tsx index 18678a5..dc086ec 100644 --- a/frontend/src/pages/Auth/login-block/login-block.tsx +++ b/frontend/src/pages/Auth/login-block/login-block.tsx @@ -1,14 +1,15 @@ import { BaseForm } from "@components"; -import { Button, ButtonType, ErrorMessage, Input } from "@ui"; +import { AlertMessage, Button, ButtonType, Input } from "@ui"; import styles from "./styles.scss"; import { Link, useHistory } from "react-router-dom"; -import { useLazyQuery, useMutation } from "@apollo/client"; +import { useMutation } from "@apollo/client"; import { SEND_LOGIN } from "@schemas"; import { useForm, UseFormOptions } from "react-hook-form"; import { loginModel } from "@features/models"; import { AUTH_TOKEN } from "@features/constants"; import { routePath } from "@pages/routes"; import { useLocalStorage } from "@features/hooks"; +import { AlertTypes } from "@features/enum"; export default function LoginBlock() { const history = useHistory(); @@ -48,9 +49,14 @@ export default function LoginBlock() { name={loginModel.password} type={"password"} /> - +
-
diff --git a/frontend/src/pages/Auth/register-block/register-block.tsx b/frontend/src/pages/Auth/register-block/register-block.tsx index 984360e..bb07249 100644 --- a/frontend/src/pages/Auth/register-block/register-block.tsx +++ b/frontend/src/pages/Auth/register-block/register-block.tsx @@ -3,7 +3,7 @@ import { Button, ButtonStyle, ButtonType, - ErrorMessage, + AlertMessage, IconDirection, Input, InputGroup, @@ -17,15 +17,17 @@ import { useMutation } from "@apollo/client"; import { AUTH_TOKEN } from "@features/constants"; import { routePath } from "@pages/routes"; import { useHistory } from "react-router-dom"; +import { useLocalStorage } from "@features/hooks"; +import { AlertTypes } from "@features/enum"; export const RegisterBlock = () => { const history = useHistory(); - + const { setItem } = useLocalStorage(); const [sendRegister, { loading, error }] = useMutation(SEND_REGISTER, { notifyOnNetworkStatusChange: true, fetchPolicy: "network-only", onCompleted: ({ signUp }) => { - localStorage.setItem(AUTH_TOKEN, signUp); + setItem(AUTH_TOKEN, signUp); history.push(routePath.main.path); }, }); @@ -68,7 +70,11 @@ export const RegisterBlock = () => { name={registerModel.password} type={"password"} /> - +