Compare commits
40 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
f878fe314d | |
|
|
37181d714a | |
|
|
6d1072cc43 | |
|
|
b1a1590c1e | |
|
|
e17ac1f88f | |
|
|
5e552a439f | |
|
|
dfea794acf | |
|
|
0397efba8a | |
|
|
dd07580943 | |
|
|
829474b200 | |
|
|
ff532359f5 | |
|
|
7b97687e78 | |
|
|
55bae65444 | |
|
|
5889a81358 | |
|
|
39b9f05cff | |
|
|
28c1832001 | |
|
|
9df7b8bc4f | |
|
|
919ac7012a | |
|
|
ddfdc29c1f | |
|
|
0b8d60fa46 | |
|
|
eb286154c4 | |
|
|
125ce7aa7a | |
|
|
b6bda6e436 | |
|
|
d69f31addc | |
|
|
ae743c074d | |
|
|
c927f3aced | |
|
|
83c1b5685a | |
|
|
e420e01e4f | |
|
|
3e1a2f6b5b | |
|
|
f01baf4c2e | |
|
|
5684e19582 | |
|
|
9d189bfebd | |
|
|
d08c0a38fb | |
|
|
37d4ee82be | |
|
|
6ac28c7357 | |
|
|
63588e0500 | |
|
|
85a3231c50 | |
|
|
bc1f1731d2 | |
|
|
b9d2f0a156 | |
|
|
f994e2ca5d |
24
README.md
24
README.md
|
|
@ -8,9 +8,29 @@
|
|||
|
||||
3. Pour compiler les fichiers, éxecutez `npm run build`.
|
||||
|
||||
4. Enfin, pour démarrer le serveur, éxecutez `npm start`.
|
||||
4. Il faut ensuite générer un fichier `.env` à la racine du projet. Vous pouvez vous aider du fichier `.env.example` pour cela. Pour la base de données, vous pouvez utiliser MySQL ou MariaDB. Si vous désirez utiliser mariadb ou mondodb par exemple, alors il faut aussi modifier le 'provider' dans le fichier `prisma/schema.prisma`.
|
||||
|
||||
5. Puis générer la base de données en éxécutant `npm run db:build` et enfin synchroniser les tables avec `npm run db:push`.
|
||||
|
||||
6. Enfin, pour démarrer le serveur, éxecutez `npm start`.
|
||||
|
||||
7. Pour accéder à l'application, rendez-vous sur `http://localhost:5000`.
|
||||
|
||||
## Programme exécutable
|
||||
|
||||
1. Téléchargez le programme exécutable pour Windows en cliquant [ici](https://github.com/polynux/groupomania-openclassrooms/releases/download/1.1/groupomania-release-1.1.zip).
|
||||
|
||||
2. Décompressez le fichier `groupomania-release-1.1.zip`.
|
||||
|
||||
3. Lancez le programme `groupomania.exe`.
|
||||
|
||||
Si vous souhaitez compiler le programme exécutable, vous pouvez suivre les instructions suivantes :
|
||||
|
||||
Executez `npm run build:neu`.
|
||||
|
||||
Le programme exécutable se trouve dans le dossier `groupomania-openclassrooms/client/dist`.
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Régler un problème avec prisma. Il demande un roleId pour la création de l'utilisateur mais ce n'est pas présent dans le model.
|
||||
- [ ] Utiliser NeutralinoJS pour faire une application desktop
|
||||
- [x] Utiliser NeutralinoJS pour faire une application desktop
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=https://localhost:3000/api
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/icon" href="/favicon.ico" />
|
||||
<title>Groupomania</title>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
"cli": {
|
||||
"binaryName": "groupomania",
|
||||
"resourcesPath": "/dist-vite/",
|
||||
"clientLibrary": "/public/js/neutralino.js",
|
||||
"extensionsPath": "/extensions/",
|
||||
"binaryVersion": "4.7.0",
|
||||
"clientVersion": "3.6.0"
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -25,7 +25,7 @@ const AppHeader = () => {
|
|||
<header className="sticky top-0 z-100 w-full shadow-sm shadow-slate-900">
|
||||
<div className="min-h-80 flex items-center justify-between bg-grey-dark p-2">
|
||||
<div className="app-header__logo flex items-center gap-3">
|
||||
<img src={logo} alt="logo" className="h-14" />
|
||||
<img src={logo} alt="Logo du site" className="h-14" />
|
||||
<span className="w-0 transition-all duration-500 sm:w-full flex-shrink overflow-hidden text-red font-bold text-4xl">Groupomania</span>
|
||||
</div>
|
||||
<div className="right">
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
import gravatar from 'gravatar';
|
||||
import { useState } from 'react';
|
||||
import Image from './Image';
|
||||
|
||||
const Avatar = ({ user }: any) => {
|
||||
// console.log(user);
|
||||
|
||||
const initials = user.firstName[0] + user.lastName[0];
|
||||
const gravatarUrl = gravatar.url(user.email, { s: '64', r: 'x', d: '404' }, true);
|
||||
const avatarUi = `https://ui-avatars.com/api/?name=${initials}&background=0D8ABC&color=fff&size=64`;
|
||||
// const avatarUi = `https://ui-avatars.com/api/?name=GD&background=0D8ABC&color=fff&size=64`;
|
||||
const [avatar, setAvatar] = useState(avatarUi);
|
||||
const [firstLoad, setFirstLoad] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
if (firstLoad) {
|
||||
fetch(gravatarUrl).then((response) => {
|
||||
if (response.status === 200) {
|
||||
setAvatar(gravatarUrl);
|
||||
}
|
||||
setFirstLoad(false);
|
||||
});
|
||||
const handleError = (err: any) => {
|
||||
if (!error) {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="avatar shrink-0">
|
||||
<img src={avatar} alt="avatar" className="rounded-full w-12 h-12 md:w-16 md:h-16 transition-all cursor-pointer" />
|
||||
<Image
|
||||
src={!error ? gravatarUrl : avatarUi}
|
||||
alt={`avatar ${user.firstName} ${user.lastName}`}
|
||||
className="rounded-full w-12 h-12 md:w-16 md:h-16 transition-all cursor-pointer"
|
||||
onError={handleError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { useState } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import Modal from './Modal';
|
||||
|
||||
const Image = ({ src, alt, className, onError }: { src: string; alt?: string; className?: string, onError?: (err: any) => void }) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
const handleError = (err: any) => {
|
||||
if (!error) {
|
||||
setError(true);
|
||||
}
|
||||
if (onError) {
|
||||
onError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
setModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal && (
|
||||
<div onClick={() => setModal(false)}>
|
||||
<Modal show={modal} className="w-auto flex flex-col items-end max-w-[80%]">
|
||||
<FaTimes
|
||||
className="fill-grey-light hover:fill-grey-dark hover:bg-grey-light cursor-pointer transition-all text-xl w-10 h-10 p-2.5 rounded-md"
|
||||
onClick={() => setModal(!modal)}
|
||||
/>
|
||||
<img
|
||||
src={(error && onError === undefined) ? 'https://via.placeholder.com/150' : src}
|
||||
alt={alt}
|
||||
onError={handleError}
|
||||
className={className}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={(error && onError === undefined) ? 'https://via.placeholder.com/150' : src}
|
||||
alt={(error && onError === undefined) ? 'Image absente' : alt}
|
||||
onError={handleError}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
||||
|
|
@ -1,15 +1,39 @@
|
|||
import { FaThumbsUp } from 'react-icons/fa';
|
||||
import { likePost, unlikePost } from '@controllers/LikeController';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toastError, toastSuccess } from '@controllers/Toasts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMeInfo } from '@controllers/UserController';
|
||||
|
||||
const Like = ({ messageId, isLiked }: { messageId: string; isLiked: boolean }) => {
|
||||
const Like = ({ message }: { message: any }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [liked, setLiked] = useState(false);
|
||||
const me = useQuery(['me'], getMeInfo, {
|
||||
onSuccess: (data) => {
|
||||
return data;
|
||||
},
|
||||
onError: (error) => {
|
||||
toastError(error as string);
|
||||
},
|
||||
});
|
||||
|
||||
const mutateLike = useMutation(isLiked ? unlikePost : likePost, {
|
||||
useEffect(() => {
|
||||
if (message.likedBy.some((like: any) => like.userId === me.data?.id)) {
|
||||
setLiked(true);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const mutateLike = useMutation(liked ? unlikePost : likePost, {
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries(['messages']);
|
||||
isLiked ? toastSuccess('Message aimé') : null;
|
||||
if (data.message === 'Post liked') {
|
||||
setLiked(true);
|
||||
toastSuccess('Message liké');
|
||||
} else if (data.message === 'Post unliked') {
|
||||
setLiked(false);
|
||||
} else {
|
||||
toastError(data.message);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toastError(error as string);
|
||||
|
|
@ -17,15 +41,22 @@ const Like = ({ messageId, isLiked }: { messageId: string; isLiked: boolean }) =
|
|||
});
|
||||
|
||||
const like = () => {
|
||||
mutateLike.mutate(messageId);
|
||||
mutateLike.mutate(message.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="absolute -bottom-10 right-0 mb-2 rounded-full bg-grey-dark shadow-lg shadow-slate-900 cursor-pointer"
|
||||
className="absolute -bottom-10 -right-2 mb-2 rounded-full bg-grey-dark shadow-lg shadow-slate-900 cursor-pointer"
|
||||
onClick={like}
|
||||
name="like"
|
||||
>
|
||||
<FaThumbsUp className={'fill-red-light text-xl w-10 h-10 p-2.5' + (isLiked ? ' fill-red' : '')} />
|
||||
<FaThumbsUp className={'fill-red-light text-xl w-10 h-10 p-2.5' + (liked ? ' fill-red' : '')} />
|
||||
{message.likes > 0 && (
|
||||
<span className="absolute -top-2 right-0 text-white rounded-full bg-red w-5 h-5 text-xs text-center p-0">
|
||||
{message.likes}
|
||||
</span>
|
||||
)}
|
||||
<span className="sr-only">Aimer</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,18 +5,8 @@ import { getMeInfo } from '@controllers/UserController';
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { toastError } from '@controllers/Toasts';
|
||||
import User from './User';
|
||||
|
||||
const Image = ({ image }: { image: string }) => {
|
||||
if (image === '' || image === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center ">
|
||||
<img src={image} alt="image" className="w-full rounded-lg cursor-pointer" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import Image from './Image';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Text = ({ text }: { text: string }) => {
|
||||
if (text === '') {
|
||||
|
|
@ -50,7 +40,8 @@ const Message = ({ message }: any) => {
|
|||
) : null}
|
||||
</div>
|
||||
<Text text={message.content} />
|
||||
<Image image={message.image} />
|
||||
{message.image && <Image src={message.image} alt="image" className="w-fit rounded-lg cursor-pointer" />}
|
||||
<div className="flex justify-between gap-3 flex-wrap">
|
||||
<div className="text-grey-light date">
|
||||
{new Date(message.createdAt).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
|
|
@ -60,14 +51,10 @@ const Message = ({ message }: any) => {
|
|||
minute: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
{me.data?.id === message.author.id ? null : (
|
||||
<Like
|
||||
messageId={message.id}
|
||||
isLiked={
|
||||
message.likes > 0 && message.likedBy.find((like: any) => like.userId === me.data?.id) ? true : false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{message.edited && <div className="text-grey-light italic flex-grow">Modifié</div>}
|
||||
{me.data?.id === message.author.id && message.likes > 0 && <div className="text-white">{message.likes} likes</div>}
|
||||
</div>
|
||||
{me.data?.id !== message.author.id && <Like message={message} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getMessages } from '@controllers/MessageController';
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { toastError } from '@controllers/Toasts';
|
||||
import ScrollToBottom from './ScrollToBottom';
|
||||
import { api } from '../main';
|
||||
|
||||
const MessageWrapper = () => {
|
||||
const {
|
||||
|
|
@ -10,6 +11,14 @@ const MessageWrapper = () => {
|
|||
isLoading,
|
||||
isError,
|
||||
} = useQuery(['messages'], getMessages, {
|
||||
onSuccess: (data) => {
|
||||
data.map((message: any) => {
|
||||
if (message.image) {
|
||||
message.image = api.slice(0, -4) + message.image;
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onError: (error) => {
|
||||
toastError(error as string);
|
||||
},
|
||||
|
|
@ -21,7 +30,7 @@ const MessageWrapper = () => {
|
|||
|
||||
return (
|
||||
<main className="messages-wrapper rounded-md w-full max-w-3xl flex flex-col flex-shrink relative overflow-y-hidden">
|
||||
<ScrollToBottom className="message-container flex flex-col gap-4 w-full max-w-3xl overflow-scroll pt-4 pb-6 px-2 md:px-0">
|
||||
<ScrollToBottom className="message-container flex flex-col gap-4 w-full max-w-3xl overflow-y-scroll overflow-x-hidden pt-4 pb-6 px-2 md:px-0">
|
||||
{isLoading
|
||||
? ''
|
||||
: isError
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import {newMessage} from '@controllers/MessageController';
|
||||
import { useRef, useState } from 'react';
|
||||
import { FaPlus, FaTimes } from 'react-icons/fa';
|
||||
import { newMessage } from '@controllers/MessageController';
|
||||
|
||||
const NewMessage = () => {
|
||||
const [message, setMessage] = useState('');
|
||||
const [image, setImage] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
const [preview, setPreview] = useState('');
|
||||
const imageRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleImageSelect = (e: any) => {
|
||||
setImage(e.target.value);
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const src = URL.createObjectURL(file);
|
||||
setPreview(src);
|
||||
}
|
||||
};
|
||||
|
||||
const { mutate: send } = useMutation(newMessage, {
|
||||
onSuccess: () => {
|
||||
|
|
@ -26,29 +37,65 @@ const NewMessage = () => {
|
|||
send(data);
|
||||
setMessage('');
|
||||
setImage('');
|
||||
setPreview('');
|
||||
if (imageRef.current) {
|
||||
imageRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
setImage('');
|
||||
setPreview('');
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="new-message z-10 flex justify-center w-full">
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 bg-grey-dark rounded-xl w-full max-w-3xl p-2 mx-2 sm:p-3 md:mx-0 shadow-md shadow-grey-dark">
|
||||
<div className="w-full max-w-3xl">
|
||||
{preview !== '' && (
|
||||
<div className="image-preview w-fit rounded-xl bg-grey-dark p-3 absolute bottom-20 shadow-md shadow-grey-dark">
|
||||
<img src={preview} alt="Image" className="rounded-lg max-w-xs max-h-48" />
|
||||
<span className="rounded-full text-grey-light bg-grey-dark text-lg p-2 block absolute -top-4 -right-4 cursor-pointer shadow-md shadow-slate-800 hover:text-grey-dark hover:bg-grey-light transition-all" onClick={handleRemoveImage}>
|
||||
<FaTimes className="" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex gap-2 bg-grey-dark rounded-xl p-2 mx-2 sm:p-3 md:mx-0 shadow-md shadow-grey-dark"
|
||||
>
|
||||
<div className="file">
|
||||
<label htmlFor="image" className="cursor-pointer block p-2">
|
||||
<div className="rounded-full text-grey-dark bg-red-light text-lg p-2">
|
||||
<span className="rounded-full text-grey-dark bg-red-light text-lg p-2 block">
|
||||
<FaPlus className="" />
|
||||
</div>
|
||||
</span>
|
||||
<span className="sr-only">Ajouter une image</span>
|
||||
</label>
|
||||
<input type="file" name="image" id="image" accept="image/*" className="hidden" value={image} onChange={e => setImage(e.target.value)} />
|
||||
<input
|
||||
type="file"
|
||||
name="image"
|
||||
id="image"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
{...(image !== '' && { value: image })}
|
||||
onChange={handleImageSelect}
|
||||
ref={imageRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="new-message flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Message"
|
||||
className="bg-grey-dark text-white rounded-xl p-2.5 w-full placeholder-red-light"
|
||||
id='content'
|
||||
name='content'
|
||||
id="content"
|
||||
name="content"
|
||||
/>
|
||||
<label htmlFor="content" className="sr-only">
|
||||
Message
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md border border-red bg-red py-2 px-4 text-sm font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
|
||||
|
|
@ -56,6 +103,7 @@ const NewMessage = () => {
|
|||
Envoyer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaChevronDown } from 'react-icons/fa';
|
||||
|
||||
const ScrollToBottom = ({ children, className = '' }: { children: ReactNode; className?: string }) => {
|
||||
|
|
@ -12,21 +12,37 @@ const ScrollToBottom = ({ children, className = '' }: { children: ReactNode; cla
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useMemo(() => {
|
||||
if (node) {
|
||||
const container = document.querySelector('main > div');
|
||||
container?.addEventListener('scroll', () => {
|
||||
if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) {
|
||||
setShow(true);
|
||||
} else {
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
container?.addEventListener('DOMNodeInserted', (e) => {
|
||||
if (!(e.target as Element).classList.contains('message')) return;
|
||||
if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) {
|
||||
node?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
container?.querySelectorAll('.message > div > img').forEach((img) => {
|
||||
img.addEventListener('load', () => {
|
||||
if (node?.getBoundingClientRect() && node.getBoundingClientRect().y >= window.innerHeight) {
|
||||
node?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setShow(false);
|
||||
} else {
|
||||
setShow(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 1 }
|
||||
);
|
||||
observer.observe(node);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [node]);
|
||||
|
||||
return (
|
||||
|
|
@ -37,11 +53,13 @@ const ScrollToBottom = ({ children, className = '' }: { children: ReactNode; cla
|
|||
</div>
|
||||
<button
|
||||
onClick={() => node?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className={'absolute right-3 transition-all' + (show ? ' bottom-3' : ' -bottom-10')}
|
||||
className={'absolute right-3 ' + (show ? 'animate-show bottom-3 block' : 'animate-hide hidden bottom-0 -mb-10')}
|
||||
name="bottom"
|
||||
>
|
||||
<div className="popup-btn cursor-pointer rounded-full shadow-lg shadow-slate-900 bg-grey-dark hover:bg-grey-light transition-all">
|
||||
<span className="popup-btn block cursor-pointer rounded-full shadow-lg shadow-slate-900 bg-grey-dark hover:bg-grey-light transition-all">
|
||||
<FaChevronDown className="fill-grey-light hover:fill-grey-dark transition-all text-xl w-10 h-10 p-2.5" />
|
||||
</div>
|
||||
</span>
|
||||
<span className="sr-only">Descendre en bas de la page</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { FormEvent, useState } from 'react';
|
||||
import Modal from './Modal';
|
||||
import { getMeInfo, giveUserRights } from '@controllers/UserController';
|
||||
import { getMeInfo, giveUserRights, changeUserInfo } from '@controllers/UserController';
|
||||
import { toastError, toastSuccess } from '@controllers/Toasts';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
|
|
@ -20,9 +20,8 @@ const User = ({ author }: any) => {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
function handleContextMenu(e: any) {
|
||||
|
||||
if (messageId!== '0' && e.target.closest('.message')?.id.slice(9) !== messageId) {
|
||||
setPopupPos({ posX: 0, posY: 0});
|
||||
if (messageId !== '0' && e.target.closest('.message')?.id.slice(9) !== messageId) {
|
||||
setPopupPos({ posX: 0, posY: 0 });
|
||||
setMessageId('0');
|
||||
document.removeEventListener('contextmenu', handleContextMenu);
|
||||
}
|
||||
|
|
@ -56,6 +55,22 @@ const User = ({ author }: any) => {
|
|||
queryClient.invalidateQueries(['messages']);
|
||||
}
|
||||
|
||||
const handleChangeInfo = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
changeUserInfo(author.id, data).then((response) => {
|
||||
if (response.error) {
|
||||
return toastError(response.error);
|
||||
}
|
||||
toastSuccess('Infos personelles changées');
|
||||
queryClient.invalidateQueries(['messages']);
|
||||
setShow(false);
|
||||
}).catch((error) => {
|
||||
toastError(error.error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="user">
|
||||
<button
|
||||
|
|
@ -90,6 +105,7 @@ const User = ({ author }: any) => {
|
|||
)}
|
||||
<Modal show={show}>
|
||||
<div className="flex flex-col gap-5">
|
||||
{me.data?.id !== author.id ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-2xl text-white">Info utilisateur</div>
|
||||
<div className="text-white">Nom: {author.lastName}</div>
|
||||
|
|
@ -97,7 +113,6 @@ const User = ({ author }: any) => {
|
|||
<div className="text-white">
|
||||
Adresse mail: <a href={'mailto:' + author.email}>{author.email}</a>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-md border border-red bg-red py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
|
||||
onClick={() => setShow(false)}
|
||||
|
|
@ -105,6 +120,83 @@ const User = ({ author }: any) => {
|
|||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-2xl text-white">Modifier ses infos personnelles</div>
|
||||
<div className="text-white">
|
||||
{' '}
|
||||
Tous les champs avec <span className="text-red">*</span> sont obligatoires
|
||||
</div>
|
||||
<form className="flex flex-col gap-2" onSubmit={handleChangeInfo}>
|
||||
<label htmlFor="firstName" className="text-white">
|
||||
Prenom <span className="text-red">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
id="firstName"
|
||||
className="rounded-lg p-2"
|
||||
defaultValue={author.firstName}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="lastName" className="text-white">
|
||||
Nom <span className="text-red">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
id="lastName"
|
||||
className="rounded-lg p-2"
|
||||
defaultValue={author.lastName}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="email" className="text-white">
|
||||
Adresse mail (ne sera pas modifié)
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
className="rounded-lg p-2"
|
||||
defaultValue={author.email}
|
||||
required
|
||||
disabled
|
||||
/>
|
||||
<label htmlFor="password" className="text-white">
|
||||
Mot de passe <span className="text-red">*</span>
|
||||
</label>
|
||||
<input type="password" name="password" id="password" className="rounded-lg p-2" required />
|
||||
<div>
|
||||
<div className="text-white text-xl">Changer son mot de passe:</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="newPassword" className="text-white">
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input type="password" name="newPassword" id="newPassword" className="rounded-lg p-2" />
|
||||
<label htmlFor="confirmPassword" className="text-white">
|
||||
Confirmer le nouveau mot de passe
|
||||
</label>
|
||||
<input type="password" name="confirmPassword" id="confirmPassword" className="rounded-lg p-2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
className="rounded-md border py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent hover:text-red cursor-pointer"
|
||||
onClick={(e) => setShow(false)}
|
||||
>
|
||||
Annuler
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md border border-red bg-red py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Cookies } from "react-cookie";
|
||||
import { api } from "../main";
|
||||
|
||||
const likePost = async (id: string) => {
|
||||
const token = new Cookies().get('token');
|
||||
const response = await fetch(`/api/posts/like/${id}`, {
|
||||
const response = await fetch(`${api}/posts/like/${id}`, {
|
||||
method: 'PUT',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
|
|
@ -19,7 +20,7 @@ const likePost = async (id: string) => {
|
|||
|
||||
const unlikePost = async (id: string) => {
|
||||
const token = new Cookies().get('token');
|
||||
const response = await fetch(`/api/posts/unlike/${id}`, {
|
||||
const response = await fetch(`${api}/posts/unlike/${id}`, {
|
||||
method: 'PUT',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Cookies } from 'react-cookie';
|
||||
import { api } from '../main';
|
||||
|
||||
const getMessages = async () => {
|
||||
const token = new Cookies().get('token');
|
||||
const response = await fetch('/api/posts', {
|
||||
const response = await fetch(api + '/posts', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
|
@ -17,7 +18,7 @@ const getMessages = async () => {
|
|||
|
||||
const newMessage = async (data: FormData) => {
|
||||
const token = new Cookies().get('token');
|
||||
const response = await fetch('/api/posts/new', {
|
||||
const response = await fetch(api + '/posts/new', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
mode: 'cors',
|
||||
|
|
@ -30,7 +31,7 @@ const newMessage = async (data: FormData) => {
|
|||
|
||||
const deleteMessage = async (id: string) => {
|
||||
const token = new Cookies().get('token');
|
||||
const response = await fetch(`/api/posts/delete/${id}`, {
|
||||
const response = await fetch(`${api}/posts/delete/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
|
@ -41,7 +42,7 @@ const deleteMessage = async (id: string) => {
|
|||
|
||||
const editMessage = async (id: string, data: FormData) => {
|
||||
const token = new Cookies().get('token');
|
||||
const response = await fetch(`/api/posts/edit/${id}`, {
|
||||
const response = await fetch(`${api}/posts/edit/${id}`, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
mode: 'cors',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Cookies } from 'react-cookie';
|
||||
import { api } from '../main';
|
||||
|
||||
const getMeInfo = async () => {
|
||||
const token = new Cookies().get('token');
|
||||
|
||||
const response = await fetch('/api/me', {
|
||||
const response = await fetch(api + '/me', {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
|
|
@ -21,7 +22,7 @@ const getMeInfo = async () => {
|
|||
const login = async ({ email, password }: { email: string; password: string }) => {
|
||||
const token = new Cookies().get('token');
|
||||
|
||||
const response = await fetch('/api/auth/login', {
|
||||
const response = await fetch(api + '/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
mode: 'cors',
|
||||
|
|
@ -51,7 +52,7 @@ const signup = async (formData: FormData) => {
|
|||
throw 'Passwords do not match';
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/signup', {
|
||||
const response = await fetch(api + '/auth/signup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(form),
|
||||
mode: 'cors',
|
||||
|
|
@ -69,7 +70,7 @@ const signup = async (formData: FormData) => {
|
|||
export const giveUserRights = async (userId: string, role: string) => {
|
||||
const token = new Cookies().get('token');
|
||||
|
||||
const response = await fetch(`/api/users/${userId}/roles`, {
|
||||
const response = await fetch(`${api}/users/${userId}/roles`, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
|
|
@ -88,4 +89,55 @@ export const giveUserRights = async (userId: string, role: string) => {
|
|||
return data;
|
||||
};
|
||||
|
||||
export const changeUserInfo = async (userId: string, formData: FormData) => {
|
||||
const token = new Cookies().get('token');
|
||||
|
||||
const firstName = formData.get('firstName');
|
||||
const lastName = formData.get('lastName');
|
||||
const password = formData.get('password');
|
||||
|
||||
if (!firstName || !lastName || !password) {
|
||||
throw {error: 'Les champs ne peuvent pas être vides'};
|
||||
}
|
||||
|
||||
const newPassword = formData.get('newPassword');
|
||||
const confirmPassword = formData.get('confirmPassword');
|
||||
|
||||
if (newPassword) {
|
||||
if (newPassword === password) {
|
||||
throw {error: 'Le nouveau mot de passe doit être différent de l\'ancien'};
|
||||
}
|
||||
// regex to check if password is strong enough
|
||||
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
|
||||
if (!regex.test(newPassword as string)) {
|
||||
throw {error: 'Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule, un chiffre et un caractère spécial'};
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
throw {error: 'Les mots de passe ne correspondent pas'};
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${api}/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
newPassword,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
return {error: data.error};
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
export { getMeInfo, login, signup };
|
||||
|
|
|
|||
|
|
@ -3,3 +3,10 @@ import App from './App';
|
|||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);
|
||||
|
||||
let api = '/api';
|
||||
if (import.meta.env.VITE_API_URL) {
|
||||
api = import.meta.env.VITE_API_URL;
|
||||
}
|
||||
|
||||
export { api };
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'react-toastify/dist/ReactToastify.css';
|
|||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="min-h-full max-h-full bg-grey flex flex-col items-center overflow-y-hidden">
|
||||
<div className="min-h-full max-h-full bg-grey flex flex-col items-center justify-between overflow-y-hidden">
|
||||
<AppHeader />
|
||||
<MessageWrapper />
|
||||
<NewMessage />
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ const Login = () => {
|
|||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [cookies, setCookie] = useCookies(['token']);
|
||||
const [keepMeLoggedIn, setKeepMeLoggedIn] = useState(false);
|
||||
|
||||
const mutation = useMutation(login, {
|
||||
onSuccess: (data: Token) => {
|
||||
setCookie('token', data.token, { path: '/', expires: new Date(data.expiresAt) });
|
||||
setCookie('token', data.token, { path: '/', expires: keepMeLoggedIn ? new Date(data.expiresAt) : undefined });
|
||||
},
|
||||
onError: (error) => {
|
||||
toastError(error as string);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
|
@ -68,6 +69,19 @@ const Login = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center mb-2">
|
||||
<input
|
||||
id="remember_me"
|
||||
name="remember_me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-red focus:ring-red border-gray-300 rounded"
|
||||
checked={keepMeLoggedIn}
|
||||
onChange={(e) => setKeepMeLoggedIn(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="remember_me" className="ml-2 block text-sm text-white">
|
||||
Se souvenir de moi
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -2,15 +2,38 @@ import { Token } from '@/types';
|
|||
import logo from '@assets/images/logo.svg';
|
||||
import { toastError, toastSuccess } from '@controllers/Toasts';
|
||||
import { signup } from '@controllers/UserController';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const checkPasswordComplexity = (password: string) => {
|
||||
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$/;
|
||||
return regex.test(password);
|
||||
};
|
||||
|
||||
const checkEmailValidity = (email: string) => {
|
||||
const regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
|
||||
return regex.test(email);
|
||||
};
|
||||
|
||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
const email = data.get('email') as string;
|
||||
const password = data.get('password') as string;
|
||||
const passwordConfirmation = data.get('password-confirm') as string;
|
||||
const firstName = data.get('firstName') as string;
|
||||
const lastName = data.get('lastName') as string;
|
||||
|
||||
if (!/^[a-zA-Z]+$/.test(firstName)) return toastError('Le prénom ne doit contenir que des lettres.');
|
||||
if (!/^[a-zA-Z]+$/.test(lastName)) return toastError('Le nom ne doit contenir que des lettres.');
|
||||
if (!checkEmailValidity(email)) return toastError('L\'adresse e-mail n\'est pas valide.');
|
||||
if (password !== passwordConfirmation) return toastError('Les mots de passe ne correspondent pas.');
|
||||
if (!checkPasswordComplexity(password)) return toastError('Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule, un chiffre et un caractère spécial.');
|
||||
|
||||
signup(data).then((data: Token) => {
|
||||
toastSuccess('You have successfully signed up!');
|
||||
toastSuccess('Vous êtes inscrit !');
|
||||
navigate('/login');
|
||||
}).catch((err) => {
|
||||
toastError(err as string);
|
||||
|
|
@ -23,8 +46,8 @@ export default () => {
|
|||
<div>
|
||||
<img className="mx-auto h-20 pb-2 w-auto" src={logo} alt="Groupomania" />
|
||||
</div>
|
||||
<div className="w-full max-w-md space-y-8 bg-grey rounded-lg p-5">
|
||||
<form className="m-6 " action="#" method="POST" onSubmit={(e) => onSubmit(e)}>
|
||||
<div className="w-full max-w-md space-y-4 bg-grey rounded-lg p-5">
|
||||
<form className="m-6 mb-0" action="#" method="POST" onSubmit={(e) => onSubmit(e)}>
|
||||
<div className="-space-y-px rounded-md shadow-sm">
|
||||
<div>
|
||||
<label htmlFor="lastName" className="sr-only">
|
||||
|
|
@ -106,6 +129,11 @@ export default () => {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-center text-sm text-grey-dark">
|
||||
<Link to="/login" className="font-medium text-red-light hover:text-red">
|
||||
Retournez à la page de connexion
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,22 @@ module.exports = {
|
|||
padding: {
|
||||
'2.5': '0.625rem',
|
||||
},
|
||||
keyframes: {
|
||||
'hide': {
|
||||
'0%': { opacity: 1, display: 'block' },
|
||||
'99%': { opacity: 0, display: 'block' },
|
||||
'100%': { opacity: 0, display: 'none' },
|
||||
},
|
||||
'show': {
|
||||
'0%': { opacity: 0, display: 'none' },
|
||||
'1%': { opacity: 0, display: 'block' },
|
||||
'100%': { opacity: 1, display: 'block' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'hide': 'hide 0.2s ease-in-out',
|
||||
'show': 'show 0.2s ease-in-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -5,10 +5,15 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch src -e js,ts,json --exec 'ts-node src/index.ts'",
|
||||
"build": "cd client && pnpm build",
|
||||
"start": "ts-node src/index.ts",
|
||||
"build": "npm run build:server && npm run build:client",
|
||||
"build:neu": "cd client && npm run build:neu",
|
||||
"build:server": "tsc && tsc-alias",
|
||||
"build:client": "cd client && npm run build",
|
||||
"start": "node dist/index.js",
|
||||
"db": "prisma studio",
|
||||
"db-build": "prisma generate; prisma db push"
|
||||
"db:build": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"postinstall": "cd client && npm install"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ model Post {
|
|||
updatedAt DateTime @updatedAt
|
||||
likes Int @default(0)
|
||||
likedBy Like[]
|
||||
edited Boolean @default(false)
|
||||
}
|
||||
|
||||
model Like {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ export default async (req: Request, res: Response) => {
|
|||
}
|
||||
return res.status(200).send({ message: 'Post unliked' });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
return res.status(500).send(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Roles from "./roles";
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { verifyToken } from "@/controller/AuthController";
|
||||
import { changeUserInfo } from "@/controller/UserController";
|
||||
|
||||
const users = Router();
|
||||
|
||||
|
|
@ -28,4 +29,26 @@ users.use(checkAuth);
|
|||
|
||||
users.post('/:id/roles', Roles);
|
||||
|
||||
users.put('/:id', (req: Request, res: Response) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
const token = getToken(req);
|
||||
if (token === undefined) {
|
||||
return res.status(401).send({ error: 'No token provided' });
|
||||
}
|
||||
if (req.userId !== userId) {
|
||||
return res.status(401).send({ error: 'Unauthorized' });
|
||||
}
|
||||
return changeUserInfo(userId, req.body)
|
||||
.then((data) => {
|
||||
if (data instanceof Error) {
|
||||
return res.status(400).send({ error: data.message });
|
||||
}
|
||||
return res.status(200).send(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
return res.status(400).send({ error: error.message });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
export default users;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import fs from 'fs';
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: path.join(__dirname, '../../public/uploads'),
|
||||
|
|
@ -10,3 +11,12 @@ const storage = multer.diskStorage({
|
|||
});
|
||||
|
||||
export const upload = multer({ storage });
|
||||
|
||||
export const deleteFile = (filename: string) => {
|
||||
const filePath = path.join(__dirname, '../../public/uploads', filename);
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { PrismaClient, Post as PrismaPost, Like } from '@prisma/client';
|
|||
import { Post } from '@/models/PostModel';
|
||||
import { exclude } from '@/lib/utils';
|
||||
import { getUserById } from './UserController';
|
||||
import { deleteFile } from './FileController';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ const editPost = async (post: Post): Promise<PrismaPost | null | Error> => {
|
|||
data: {
|
||||
content: post.content,
|
||||
image: post.image,
|
||||
edited: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -94,6 +96,9 @@ const deletePost = async (id: number, userId: number): Promise<PrismaPost | Erro
|
|||
if (post.authorId !== userId && user.role === 'USER') {
|
||||
return new Error('User is not the author of this post');
|
||||
}
|
||||
if (post.image) {
|
||||
deleteFile(post.image);
|
||||
}
|
||||
return prisma.post.delete({
|
||||
where: {
|
||||
id,
|
||||
|
|
@ -185,11 +190,6 @@ const likePost = async (id: number, userId: number): Promise<PrismaPost | Error>
|
|||
id,
|
||||
},
|
||||
data: {
|
||||
likedBy: {
|
||||
connect: {
|
||||
id: newLike.id,
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
increment: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { PrismaClient, Role } from '@prisma/client';
|
||||
import { User } from '@/models/UserModel';
|
||||
import { exclude } from '@/lib/utils';
|
||||
import { comparePassword } from './AuthController';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
|
@ -96,4 +97,57 @@ export const changeUserRoles = async (id: number, role: Role) => {
|
|||
return exclude(updatedUser, 'password');
|
||||
};
|
||||
|
||||
export const changeUserInfo = async (
|
||||
id: number,
|
||||
userInfo: { firstName: string; lastName: string; password: string; newPassword?: string; confirmPassword: string }
|
||||
) => {
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentUser) {
|
||||
return new Error('User not found');
|
||||
}
|
||||
|
||||
const isPasswordCorrect = await comparePassword(userInfo.password, currentUser.password);
|
||||
if (!isPasswordCorrect) {
|
||||
return new Error('Password is incorrect');
|
||||
}
|
||||
|
||||
if (userInfo.newPassword) {
|
||||
if (userInfo.newPassword !== userInfo.confirmPassword) {
|
||||
return new Error('New password and confirm password do not match');
|
||||
}
|
||||
|
||||
const isPasswordSame = await comparePassword(userInfo.password, currentUser.password);
|
||||
if (isPasswordSame) {
|
||||
return new Error('Password are the same');
|
||||
}
|
||||
|
||||
if (userInfo.newPassword !== userInfo.confirmPassword) {
|
||||
return new Error('New password and confirm password are not the same');
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
firstName: userInfo.firstName,
|
||||
lastName: userInfo.lastName,
|
||||
};
|
||||
if (userInfo.newPassword) Object.setPrototypeOf(data, { password: userInfo.newPassword });
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (!updatedUser) {
|
||||
return new Error('User not found');
|
||||
}
|
||||
return exclude(updatedUser, 'password');
|
||||
};
|
||||
|
||||
export { getUser, newUser, isUserExist, getUserById };
|
||||
|
|
|
|||
10
src/index.ts
10
src/index.ts
|
|
@ -5,6 +5,7 @@ import { config as envConfig } from 'dotenv';
|
|||
import { deleteExpiredTokens } from '@/controller/AuthController';
|
||||
import ms from 'ms';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
envConfig();
|
||||
|
||||
|
|
@ -42,10 +43,17 @@ app.use(json({ limit: '50mb' }));
|
|||
app.use(urlencoded({ extended: true, limit: '50mb' }));
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
app.use(express.static(path.join(__dirname, '../client/dist')));
|
||||
|
||||
app.use('/api', api);
|
||||
|
||||
const staticDist = fs.existsSync(path.join(__dirname, '../client/dist-vite')) ? express.static(path.join(__dirname, '../client/dist-vite')) : express.static(path.join(__dirname, '../client/dist'));
|
||||
|
||||
app.use(staticDist);
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../client/dist/index.html'));
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface Post {
|
|||
authorId: number;
|
||||
likes?: number | undefined;
|
||||
likedBy?: Like[] | undefined;
|
||||
edited?: boolean | undefined;
|
||||
}
|
||||
|
||||
const Post: z.ZodType<Post> = z.object({
|
||||
|
|
@ -17,6 +18,7 @@ const Post: z.ZodType<Post> = z.object({
|
|||
authorId: z.number(),
|
||||
likes: z.number().optional(),
|
||||
likedBy: z.array(Like).optional(),
|
||||
edited: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export { Post };
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "es2017",
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"esnext"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"module": "CommonJS",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue