├── .gitignore ├── LICENSE ├── README.md ├── client ├── .gitignore ├── .netlify │ └── state.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.png │ ├── index.html │ ├── logo256.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── backendUrl.js │ ├── components │ ├── AddContactModal.js │ ├── ContactCard.js │ ├── ContactsDisplay.js │ ├── ContactsLoader.js │ ├── DeleteModal.js │ ├── DemoCredsInfo.js │ ├── DisplayPictureModal.js │ ├── EditContactModal.js │ ├── FormError.js │ ├── LinkFormModal.js │ ├── LoginForm.js │ ├── NavBar.js │ ├── RegisterForm.js │ ├── Routes.js │ └── Search.js │ ├── data │ └── demoCreds.js │ ├── index.css │ ├── index.js │ ├── services │ ├── auth.js │ └── contacts.js │ └── utils │ ├── arraysAndFuncs.js │ └── localStorageHelpers.js ├── screenshots ├── auth-forms.png ├── desktop-tablet.png ├── mobile-ui-1.png ├── mobile-ui-2.png └── modals.png └── server ├── .eslintrc.json ├── app.js ├── controllers ├── auth.js └── contact.js ├── db.js ├── index.js ├── models ├── contact.js └── user.js ├── package-lock.json ├── package.json ├── routes ├── auth.js └── contact.js └── utils ├── config.js └── middleware.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | build 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Amandeep Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Profile Store | MERN 2 | 3 | A MERN stack app for storing profile links of people you admire, at one place. 4 | 5 | ## Demo 6 | 7 | [Deployed on Netlify (front-end) & Heroku (back-end)](https://profile-store.netlify.app) 8 | 9 | ## Built using 10 | 11 | #### Front-end 12 | 13 | - [ReactJS](https://reactjs.org/) - Frontend framework 14 | - [useState hook & props](https://reactjs.org/docs/hooks-state.html) - For state management 15 | - [React Router](https://reactrouter.com/) - For general routing & navigation 16 | - [Semantic-UI w/ normal CSS for customisations](https://react.semantic-ui.com/) - UI library 17 | - [React toast notifications](https://jossmac.github.io/react-toast-notifications/) - For toast notifications (duh :P) 18 | 19 | #### Back-end 20 | 21 | - [Node.js](https://nodejs.org/en/) - Runtime environment for JS 22 | - [Express.js](https://expressjs.com/) - Node.js framework, makes process of building APIs easier & faster 23 | - [MongoDB](https://www.mongodb.com/) - Database to store document-based data 24 | - [Mongoose](https://mongoosejs.com/) - MongoDB object modeling for Node.js 25 | - [Cloudinary](https://cloudinary.com/) - For image uploading & related API 26 | - [JSON Web Token](https://jwt.io/) - A standard to secure/authenticate HTTP requests 27 | - [Bcrypt.js](https://www.npmjs.com/package/bcryptjs) - For hashing passwords 28 | - [Validator.js](https://www.npmjs.com/package/validator) - For validation of JSON data 29 | - [Mongoose Unique Validator](https://www.npmjs.com/package/mongoose-unique-validator) - Plugin for better error handling of unique fields within Mongoose schema 30 | - [Dotenv](https://www.npmjs.com/package/dotenv) - To load environment variables from a .env file 31 | 32 | ## Features 33 | 34 | - Authentication (login/register with email-password) 35 | - Upload images for display pictures of contacts 36 | - Add/update/delete contacts & change display picture 37 | - Add/update/delete profile links of individual contacts 38 | - Search contacts by name or profile links 39 | - Toast notifications for actions - adding/updating/deleting contact, or welcome message etc. 40 | - Dark mode toggle w/ local storage save 41 | - Responsive UI for all screens 42 | 43 | ## Screenshots 44 | 45 | #### Desktop/Tablet Home 46 | 47 | ![Home](https://github.com/amand33p/profile-store-mern/blob/master/screenshots/desktop-tablet.png) 48 | 49 | #### Auth Forms 50 | 51 | ![Auth Forms](https://github.com/amand33p/profile-store-mern/blob/master/screenshots/auth-forms.png) 52 | 53 | #### Pop-up windows (modals) 54 | 55 | ![Pop-up windows](https://github.com/amand33p/profile-store-mern/blob/master/screenshots/modals.png) 56 | 57 | #### Mobile UI 58 | 59 | ![Mobile UI - 1](https://github.com/amand33p/profile-store-mern/blob/master/screenshots/mobile-ui-1.png) 60 | 61 | ![Mobile UI - 2](https://github.com/amand33p/profile-store-mern/blob/master/screenshots/mobile-ui-2.png) 62 | 63 | ## Usage 64 | 65 | Notes: 66 | 67 | - For image API, make account at cloudinary.com & get API keys from account dashboard. 68 | - For upload preset usage, if you want to organize images separately at cloudinary.com, you have to create it from account settings first. If you don't want to, just don't put anything or use .env key - `UPLOAD_PRESET`. 69 | 70 | #### Env variable: 71 | 72 | Create .env file in server directory and add the following: 73 | 74 | ``` 75 | MONGODB_URI = "Your Mongo URI" 76 | PORT = 3005 77 | SECRET = "Your JWT secret" 78 | CLOUDINARY_NAME = "From your cloudinary dashboard" 79 | CLOUDINARY_API_KEY = "From your cloudinary dashboard" 80 | CLOUDINARY_API_SECRET = "From your cloudinary dashboard" 81 | UPLOAD_PRESET = "Folder/preset name from your cloudinary account" (OPTIONAL) 82 | ``` 83 | 84 | #### Client: 85 | 86 | Open client/src/backendUrl.js & change "backend" variable to `"http://localhost:3005"` 87 | 88 | ``` 89 | cd client 90 | npm install 91 | npm start 92 | ``` 93 | 94 | #### Server: 95 | 96 | Note: Make sure that you have installed 'nodemon' as global package. 97 | 98 | ``` 99 | cd server 100 | npm install 101 | npm run dev 102 | ``` 103 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /client/.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "0b69803b-6b3d-4f71-9619-e3f961f6b99c" 3 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "profile-store-by-amand33p", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.21.1", 7 | "react": "^16.13.1", 8 | "react-dom": "^16.13.1", 9 | "react-responsive": "^8.1.0", 10 | "react-router-dom": "^5.2.0", 11 | "react-scripts": "3.4.1", 12 | "react-toast-notifications": "^2.4.0", 13 | "semantic-ui-react": "^1.0.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "predeploy": "rm -rf build && npm run build" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/client/public/favicon.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 20 | 29 | Profile Store | amand33p 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /client/public/logo256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/client/public/logo256.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Profile Store", 3 | "name": "Profile Store by amand33p", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "logo256.png", 12 | "type": "image/png", 13 | "sizes": "256x256" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#9edacf", 24 | "background_color": "#e5f5f2" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import NavBar from './components/NavBar'; 3 | import Routes from './components/Routes'; 4 | import contactService from './services/contacts'; 5 | import { optionsArray } from './utils/arraysAndFuncs'; 6 | import storageService from './utils/localStorageHelpers'; 7 | import { useToasts } from 'react-toast-notifications'; 8 | import { Container, Segment } from 'semantic-ui-react'; 9 | 10 | const App = () => { 11 | const [contacts, setContacts] = useState([]); 12 | const [user, setUser] = useState(null); 13 | const [options, setOptions] = useState(optionsArray); 14 | const [isLoading, setIsLoading] = useState(false); 15 | const [search, setSearch] = useState(''); 16 | const [isDarkMode, setIsDarkMode] = useState(false); 17 | 18 | const { addToast: notify } = useToasts(); 19 | 20 | useEffect(() => { 21 | const loggedUser = storageService.loadUser(); 22 | 23 | if (loggedUser) { 24 | setUser(loggedUser); 25 | contactService.setToken(loggedUser.token); 26 | } 27 | }, []); 28 | 29 | useEffect(() => { 30 | const getAllContacts = async () => { 31 | try { 32 | setIsLoading(true); 33 | const contacts = await contactService.getAll(); 34 | setContacts(contacts); 35 | setIsLoading(false); 36 | } catch (err) { 37 | setIsLoading(false); 38 | notify(err.message, { 39 | appearance: 'error', 40 | }); 41 | } 42 | }; 43 | 44 | if (user) { 45 | getAllContacts(); 46 | } 47 | // eslint-disable-next-line react-hooks/exhaustive-deps 48 | }, [user]); 49 | 50 | useEffect(() => { 51 | const darkMode = storageService.loadDarkMode(); 52 | if (darkMode === 'true') { 53 | setIsDarkMode(true); 54 | } 55 | }, []); 56 | 57 | const handleOptionAddition = (e, data) => { 58 | setOptions((prevState) => [ 59 | { 60 | key: data.value, 61 | text: data.value, 62 | value: data.value, 63 | icon: 'linkify', 64 | }, 65 | ...prevState, 66 | ]); 67 | }; 68 | 69 | return ( 70 | 71 | 72 | 78 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | export default App; 97 | -------------------------------------------------------------------------------- /client/src/backendUrl.js: -------------------------------------------------------------------------------- 1 | const backendUrl = 'https://profile-store.herokuapp.com'; 2 | 3 | export default backendUrl; 4 | -------------------------------------------------------------------------------- /client/src/components/AddContactModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FormError from './FormError'; 3 | import contactService from '../services/contacts'; 4 | import { generateBase64Encode } from '../utils/arraysAndFuncs'; 5 | import { useMediaQuery } from 'react-responsive'; 6 | import { Modal, Header, Form, Button, Icon, Image } from 'semantic-ui-react'; 7 | 8 | const AddContactModal = ({ 9 | setContacts, 10 | options, 11 | handleOptionAddition, 12 | notify, 13 | isDarkMode, 14 | }) => { 15 | const [modalOpen, setModalOpen] = useState(false); 16 | const [name, setName] = useState(''); 17 | const [url, setUrl] = useState(''); 18 | const [site, setSite] = useState(''); 19 | const [displayPicture, setDisplayPicture] = useState(''); 20 | const [fileName, setFileName] = useState(''); 21 | const [error, setError] = useState(null); 22 | const [isLoading, setIsLoading] = useState(false); 23 | 24 | const isMobile = useMediaQuery({ maxWidth: 767 }); 25 | 26 | const handleOpen = () => { 27 | setModalOpen(true); 28 | }; 29 | 30 | const handleClose = () => { 31 | setModalOpen(false); 32 | }; 33 | 34 | const addNewContact = async (e) => { 35 | e.preventDefault(); 36 | 37 | const contactObject = { 38 | name, 39 | contacts: { 40 | url, 41 | site, 42 | }, 43 | displayPicture, 44 | }; 45 | 46 | try { 47 | setIsLoading(true); 48 | const returnedObject = await contactService.addNew(contactObject); 49 | setContacts((prevState) => prevState.concat(returnedObject)); 50 | setIsLoading(false); 51 | setError(null); 52 | 53 | notify(`Added new contact named as "${returnedObject.name}"`, { 54 | appearance: 'success', 55 | }); 56 | handleClose(); 57 | 58 | setName(''); 59 | setUrl(''); 60 | setSite(''); 61 | setDisplayPicture(''); 62 | setFileName(''); 63 | } catch (err) { 64 | setIsLoading(false); 65 | const errRes = err?.response?.data; 66 | 67 | if (errRes?.error) { 68 | return setError(errRes.error); 69 | } else { 70 | return setError(err.message); 71 | } 72 | } 73 | }; 74 | 75 | const fileInputOnChange = (e) => { 76 | const file = e.target.files[0]; 77 | setFileName(file.name); 78 | generateBase64Encode(file, setDisplayPicture); 79 | }; 80 | 81 | const clearfileSelection = () => { 82 | setDisplayPicture(''); 83 | setFileName(''); 84 | }; 85 | 86 | return ( 87 | 98 | 99 | Add New Contact 100 | 101 | } 102 | onOpen={handleOpen} 103 | onClose={handleClose} 104 | className={isDarkMode ? 'dark-mode-modal modal' : 'modal'} 105 | > 106 |
107 | {error && } 108 | 109 |
110 | setName(e.target.value)} 117 | icon="user secret" 118 | iconPosition="left" 119 | /> 120 | setUrl(e.target.value)} 127 | icon="linkify" 128 | iconPosition="left" 129 | /> 130 | setSite(data.value)} 141 | /> 142 | 143 | 176 | )} 177 | {displayPicture && ( 178 | 184 | )} 185 | 186 | 195 | 196 | 197 |
198 | 199 | ); 200 | }; 201 | 202 | export default AddContactModal; 203 | -------------------------------------------------------------------------------- /client/src/components/ContactCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LinkFormModal from './LinkFormModal'; 3 | import DeleteModal from './DeleteModal'; 4 | import DisplayPictureModal from './DisplayPictureModal'; 5 | import EditContactModal from './EditContactModal'; 6 | import { siteIconsArray, randomColor } from '../utils/arraysAndFuncs'; 7 | import { useMediaQuery } from 'react-responsive'; 8 | import { Header, Card, List, Label } from 'semantic-ui-react'; 9 | 10 | const ContactCard = ({ 11 | contact, 12 | contacts, 13 | setContacts, 14 | options, 15 | handleOptionAddition, 16 | notify, 17 | isDarkMode, 18 | }) => { 19 | const isMobile = useMediaQuery({ maxWidth: 767 }); 20 | 21 | const linkCharCount = isMobile ? '30' : '70'; 22 | 23 | const formattedLink = (link) => { 24 | return link.length > linkCharCount 25 | ? link.slice(0, linkCharCount).concat('...') 26 | : link; 27 | }; 28 | 29 | return ( 30 | 31 | 32 |
33 |
34 | {contact.displayPicture.exists ? ( 35 | 40 | ) : ( 41 | 49 | )} 50 | {contact.name} 51 |
52 |
53 | 60 | 69 |
70 |
71 |
72 | 73 | 74 | {contact.contacts.map((c) => ( 75 | 76 | 85 | 86 | 87 | 94 | {formattedLink(c.url).startsWith('http') 95 | ? formattedLink(c.url).split('//')[1] 96 | : formattedLink(c.url)} 97 | 98 | 110 | 123 | 124 | {c.site} 125 | 126 | 127 | ))} 128 | 129 | 139 | 140 |
141 | ); 142 | }; 143 | 144 | export default ContactCard; 145 | -------------------------------------------------------------------------------- /client/src/components/ContactsDisplay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContactCard from './ContactCard'; 3 | import ContactsLoader from './ContactsLoader'; 4 | import { Header, Icon } from 'semantic-ui-react'; 5 | 6 | const ContactsDisplay = ({ 7 | contacts, 8 | setContacts, 9 | search, 10 | options, 11 | handleOptionAddition, 12 | notify, 13 | isLoading, 14 | isDarkMode, 15 | }) => { 16 | const filterByName = (contact, search) => { 17 | return contact.name.toLowerCase().includes(search.toLowerCase()); 18 | }; 19 | 20 | const filterByProfileLinks = (contact, search) => { 21 | const urlsArray = contact.contacts.map((c) => c.url); 22 | 23 | return urlsArray.find((u) => 24 | u.toLowerCase().includes(search.toLowerCase()) 25 | ); 26 | }; 27 | 28 | const contactsToDisplay = contacts.filter( 29 | (c) => filterByName(c, search) || filterByProfileLinks(c, search) 30 | ); 31 | 32 | return ( 33 |
34 | {search !== '' && contactsToDisplay.length !== 0 && ( 35 |
36 | 37 | Showing search results for query "{search}" 38 |
39 | )} 40 | {search !== '' && contactsToDisplay.length === 0 && ( 41 |
46 | 47 | Search: No matches found for "{search}" 48 |
49 | )} 50 | {!isLoading && search === '' && contactsToDisplay.length === 0 && ( 51 |
56 | 57 | No contacts added yet. 58 |
59 | )} 60 | {isLoading ? ( 61 | 62 | ) : ( 63 | contactsToDisplay.map((contact) => ( 64 | 74 | )) 75 | )} 76 |
77 | ); 78 | }; 79 | 80 | export default ContactsDisplay; 81 | -------------------------------------------------------------------------------- /client/src/components/ContactsLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Placeholder, Segment } from 'semantic-ui-react'; 3 | 4 | const ContactsLoader = ({ isDarkMode }) => { 5 | return ( 6 |
7 | {Array.from(new Array(3)).map((_, i) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ))} 29 |
30 | ); 31 | }; 32 | 33 | export default ContactsLoader; 34 | -------------------------------------------------------------------------------- /client/src/components/DeleteModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FormError from './FormError'; 3 | import contactService from '../services/contacts'; 4 | import { useMediaQuery } from 'react-responsive'; 5 | import { Button, Header, Icon, Modal } from 'semantic-ui-react'; 6 | 7 | const DeleteModal = ({ 8 | type, 9 | contacts, 10 | setContacts, 11 | contact, 12 | id, 13 | urlId, 14 | urlLink, 15 | urlName, 16 | notify, 17 | isDarkMode, 18 | }) => { 19 | const [open, setOpen] = useState(false); 20 | const [error, setError] = useState(null); 21 | const [isLoading, setIsLoading] = useState(false); 22 | 23 | const isMobile = useMediaQuery({ maxWidth: 767 }); 24 | 25 | const handleContactDelete = async () => { 26 | try { 27 | setIsLoading(true); 28 | await contactService.deleteContact(id); 29 | setContacts(contacts.filter((c) => c.id !== id)); 30 | setIsLoading(false); 31 | setError(null); 32 | 33 | notify(`Deleted contact "${contact.name}"`, { 34 | appearance: 'success', 35 | }); 36 | } catch (err) { 37 | setIsLoading(false); 38 | const errRes = err.response.data; 39 | 40 | if (errRes && errRes.error) { 41 | return setError(errRes.error); 42 | } else { 43 | return setError(err.message); 44 | } 45 | } 46 | }; 47 | 48 | const handleLinkDelete = async () => { 49 | const targetContact = contacts.find((c) => c.id === id); 50 | const updatedContactsKey = targetContact.contacts.filter( 51 | (t) => t.id !== urlId 52 | ); 53 | const updatedContact = { ...targetContact, contacts: updatedContactsKey }; 54 | 55 | try { 56 | setIsLoading(true); 57 | await contactService.deleteLink(id, urlId); 58 | setContacts(contacts.map((c) => (c.id !== id ? c : updatedContact))); 59 | setIsLoading(false); 60 | setError(null); 61 | 62 | notify(`Deleted ${urlName} link "${urlLink}"`, { 63 | appearance: 'success', 64 | }); 65 | } catch (err) { 66 | setIsLoading(false); 67 | const errRes = err.response.data; 68 | 69 | if (errRes && errRes.error) { 70 | return setError(errRes.error); 71 | } else { 72 | return setError(err.message); 73 | } 74 | } 75 | }; 76 | 77 | const isTypeContact = type === 'contact'; 78 | 79 | return ( 80 | 95 | } 96 | onClose={() => setOpen(false)} 97 | onOpen={() => setOpen(true)} 98 | className={isDarkMode ? 'dark-mode-modal' : ''} 99 | > 100 |
101 | {error && } 102 | 103 |

104 | {isTypeContact 105 | ? `Are you sure you want to delete contact named as '${contact.name}'?` 106 | : `Are you sure you want to delete ${urlName} link '${urlLink}'?`} 107 |

108 |
109 | 110 | 113 | 121 | 122 | 123 | ); 124 | }; 125 | 126 | export default DeleteModal; 127 | -------------------------------------------------------------------------------- /client/src/components/DemoCredsInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Message } from 'semantic-ui-react'; 3 | import demoCredentials from '../data/demoCreds'; 4 | 5 | const DemoCredsInfo = () => { 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default DemoCredsInfo; 12 | -------------------------------------------------------------------------------- /client/src/components/DisplayPictureModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { getCircularAvatar } from '../utils/arraysAndFuncs'; 3 | 4 | import { Header, Modal, Image } from 'semantic-ui-react'; 5 | 6 | const DisplayPictureModal = ({ imageLink, contactName, isDarkMode }) => { 7 | const [open, setOpen] = useState(false); 8 | 9 | return ( 10 | 20 | } 21 | onClose={() => setOpen(false)} 22 | onOpen={() => setOpen(true)} 23 | className={isDarkMode ? 'dark-mode-modal' : ''} 24 | > 25 |
26 | 27 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default DisplayPictureModal; 39 | -------------------------------------------------------------------------------- /client/src/components/EditContactModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FormError from './FormError'; 3 | import contactService from '../services/contacts'; 4 | import { generateBase64Encode } from '../utils/arraysAndFuncs'; 5 | import { useMediaQuery } from 'react-responsive'; 6 | import { Modal, Header, Form, Button, Icon, Image } from 'semantic-ui-react'; 7 | 8 | const EditContactModal = ({ oldName, setContacts, id, notify, isDarkMode }) => { 9 | const [modalOpen, setModalOpen] = useState(false); 10 | const [name, setName] = useState(oldName); 11 | const [displayPicture, setDisplayPicture] = useState(''); 12 | const [fileName, setFileName] = useState(''); 13 | const [error, setError] = useState(null); 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | const isMobile = useMediaQuery({ maxWidth: 767 }); 17 | 18 | const handleOpen = () => { 19 | setModalOpen(true); 20 | }; 21 | 22 | const handleClose = () => { 23 | setModalOpen(false); 24 | }; 25 | 26 | const addNewContact = async (e) => { 27 | e.preventDefault(); 28 | 29 | const contactObject = { 30 | name, 31 | displayPicture, 32 | }; 33 | 34 | try { 35 | setIsLoading(true); 36 | const returnedObject = await contactService.editContact( 37 | id, 38 | contactObject 39 | ); 40 | setContacts((prevState) => 41 | prevState.map((p) => (p.id !== id ? p : returnedObject)) 42 | ); 43 | setIsLoading(false); 44 | setError(null); 45 | 46 | let message = `Updated contact "${returnedObject.name}"`; 47 | 48 | if (oldName !== returnedObject.name && displayPicture !== '') { 49 | message = `Updated contact name from "${oldName}" to "${returnedObject.name}" & changed DP`; 50 | } else if (oldName === returnedObject.name && displayPicture !== '') { 51 | message = `Updated DP of contact "${returnedObject.name}"`; 52 | } else if (oldName !== returnedObject.name && displayPicture === '') { 53 | message = `Updated contact name from "${oldName}" to "${returnedObject.name}"`; 54 | } 55 | 56 | notify(message, { 57 | appearance: 'success', 58 | }); 59 | handleClose(); 60 | setDisplayPicture(''); 61 | setFileName(''); 62 | } catch (err) { 63 | setIsLoading(false); 64 | const errRes = err?.response?.data; 65 | 66 | if (errRes?.error) { 67 | return setError(errRes.error); 68 | } else { 69 | return setError(err.message); 70 | } 71 | } 72 | }; 73 | 74 | const fileInputOnChange = (e) => { 75 | const file = e.target.files[0]; 76 | setFileName(file.name); 77 | generateBase64Encode(file, setDisplayPicture); 78 | }; 79 | 80 | const clearfileSelection = () => { 81 | setDisplayPicture(''); 82 | setFileName(''); 83 | }; 84 | 85 | return ( 86 | 98 | } 99 | onOpen={handleOpen} 100 | onClose={handleClose} 101 | className={isDarkMode ? 'dark-mode-modal modal' : 'modal'} 102 | > 103 |
108 | {error && } 109 | 110 |
111 | setName(e.target.value)} 118 | icon="user secret" 119 | iconPosition="left" 120 | /> 121 | 150 | )} 151 | {displayPicture && ( 152 | 158 | )} 159 | 160 | 169 | 170 | 171 |
172 | 173 | ); 174 | }; 175 | 176 | export default EditContactModal; 177 | -------------------------------------------------------------------------------- /client/src/components/FormError.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Message, Button } from 'semantic-ui-react'; 3 | 4 | const FormError = ({ message, setError, title, positive }) => { 5 | return ( 6 | 7 | 8 | {title || 'Error'} 9 | 170 | 171 | 172 | 173 | ); 174 | }; 175 | 176 | export default LinkFormModal; 177 | -------------------------------------------------------------------------------- /client/src/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useHistory } from 'react-router-dom'; 3 | import FormError from './FormError'; 4 | import DemoCredsInfo from './DemoCredsInfo'; 5 | import contactService from '../services/contacts'; 6 | import authService from '../services/auth'; 7 | import storageService from '../utils/localStorageHelpers'; 8 | import { useMediaQuery } from 'react-responsive'; 9 | import { Segment, Form, Button, Icon, Header } from 'semantic-ui-react'; 10 | 11 | const LoginForm = ({ setUser, notify, isDarkMode }) => { 12 | const [credentials, setCredentials] = useState({ 13 | email: '', 14 | password: '', 15 | }); 16 | const [error, setError] = useState(null); 17 | const [isLoading, setIsLoading] = useState(false); 18 | const [showPass, setShowPass] = useState(false); 19 | const history = useHistory(); 20 | const isMobile = useMediaQuery({ maxWidth: 767 }); 21 | const { email, password } = credentials; 22 | 23 | const handleOnChange = (e) => { 24 | setCredentials({ ...credentials, [e.target.name]: e.target.value }); 25 | }; 26 | 27 | const handleLogin = async (e) => { 28 | e.preventDefault(); 29 | try { 30 | setIsLoading(true); 31 | const user = await authService.login(credentials); 32 | setUser(user); 33 | contactService.setToken(user.token); 34 | storageService.saveUser(user); 35 | setIsLoading(false); 36 | setError(null); 37 | 38 | notify(`Welcome ${user.displayName}, you're logged in!`, { 39 | appearance: 'success', 40 | }); 41 | history.push('/'); 42 | } catch (err) { 43 | setIsLoading(false); 44 | const errRes = err?.response?.data; 45 | 46 | if (errRes?.error) { 47 | return setError({ message: errRes.error }); 48 | } else { 49 | return setError({ message: err.message }); 50 | } 51 | } 52 | }; 53 | 54 | return ( 55 | 61 |
62 | 63 | Login to your account 64 |
65 |
69 | 80 | setShowPass(!showPass), 94 | } 95 | } 96 | /> 97 | 98 | 112 |
117 | Don't have an account? Register. 118 |
119 | 120 | {error && ( 121 | 127 | )} 128 | 129 |
130 | ); 131 | }; 132 | 133 | export default LoginForm; 134 | -------------------------------------------------------------------------------- /client/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import storageService from '../utils/localStorageHelpers'; 4 | import { useMediaQuery } from 'react-responsive'; 5 | import { Menu, Icon, Dropdown } from 'semantic-ui-react'; 6 | 7 | const NavBar = ({ user, setUser, isDarkMode, setIsDarkMode }) => { 8 | const [iconLoading, setIconLoading] = useState(false); 9 | const location = useLocation(); 10 | 11 | const isMobile = useMediaQuery({ maxWidth: 767 }); 12 | 13 | const handleLogout = () => { 14 | setUser(null); 15 | storageService.logoutUser(); 16 | }; 17 | 18 | const handleDarkModeToggle = () => { 19 | setIsDarkMode(!isDarkMode); 20 | setIconLoading(true); 21 | storageService.saveDarkMode(!isDarkMode); 22 | setTimeout(() => setIconLoading(false), 2150); 23 | }; 24 | 25 | const logoutMenu = () => { 26 | return isMobile ? ( 27 | 28 | 29 | 30 | 31 | {`Hi, ${user.displayName}`} 32 | 33 | 34 | 35 | Logout 36 | 37 | 38 | 42 | Dark Mode: {isDarkMode ? 'ON' : 'OFF'} 43 | 44 | 45 | 46 | ) : ( 47 | <> 48 | 49 | 50 | {`Hi, ${user.displayName}`} 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | const loginRegisterMenu = () => { 58 | return isMobile ? ( 59 | 60 | 61 | 62 | 63 | Register 64 | 65 | 66 | 67 | Login 68 | 69 | 70 | 74 | Dark Mode: {isDarkMode ? 'ON' : 'OFF'} 75 | 76 | 77 | 78 | ) : ( 79 | <> 80 | 87 | 94 | 95 | ); 96 | }; 97 | 98 | return ( 99 | 106 | 107 |
108 | 109 | Profile Store 110 |
111 | 112 | Made with by{' '} 113 | 119 | amand33p 120 | 121 | 122 |
123 | 124 | {user ? <>{logoutMenu()} : <>{loginRegisterMenu()}} 125 | {!isMobile && ( 126 | 127 | 136 | 137 | )} 138 | 139 |
140 | ); 141 | }; 142 | 143 | export default NavBar; 144 | -------------------------------------------------------------------------------- /client/src/components/RegisterForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useHistory } from 'react-router-dom'; 3 | import FormError from './FormError'; 4 | import DemoCredsInfo from './DemoCredsInfo'; 5 | import contactService from '../services/contacts'; 6 | import authService from '../services/auth'; 7 | import storageService from '../utils/localStorageHelpers'; 8 | import { useMediaQuery } from 'react-responsive'; 9 | import { Segment, Form, Button, Icon, Header } from 'semantic-ui-react'; 10 | 11 | const RegisterForm = ({ setUser, notify, isDarkMode }) => { 12 | const [userDetails, setUserDetails] = useState({ 13 | displayName: '', 14 | email: '', 15 | password: '', 16 | }); 17 | const [confirmPassword, setConfirmPassword] = useState(''); 18 | const [error, setError] = useState(null); 19 | const [isLoading, setIsLoading] = useState(false); 20 | const [showPass, setShowPass] = useState(false); 21 | const [showConfirmPass, setShowConfirmPass] = useState(false); 22 | const history = useHistory(); 23 | const isMobile = useMediaQuery({ maxWidth: 767 }); 24 | const { displayName, email, password } = userDetails; 25 | 26 | const handleOnChange = (e) => { 27 | setUserDetails({ ...userDetails, [e.target.name]: e.target.value }); 28 | }; 29 | 30 | const handleRegister = async (e) => { 31 | if (password !== confirmPassword) 32 | return setError(`Confirm password failed. Both passwords need to match.`); 33 | e.preventDefault(); 34 | try { 35 | setIsLoading(true); 36 | const user = await authService.register(userDetails); 37 | setUser(user); 38 | contactService.setToken(user.token); 39 | storageService.saveUser(user); 40 | setIsLoading(false); 41 | setError(null); 42 | 43 | notify(`Welcome ${user.displayName}, you have successfully registered!`, { 44 | appearance: 'success', 45 | }); 46 | history.push('/'); 47 | } catch (err) { 48 | setIsLoading(false); 49 | const errRes = err?.response?.data; 50 | 51 | if (errRes?.error) { 52 | return setError(errRes.error); 53 | } else { 54 | return setError(err.message); 55 | } 56 | } 57 | }; 58 | 59 | return ( 60 | 66 |
67 | 68 | Create an account 69 |
70 |
74 | 85 | 96 | setShowPass(!showPass), 110 | } 111 | } 112 | /> 113 | setConfirmPassword(target.value)} 120 | icon="lock" 121 | iconPosition="left" 122 | action={ 123 | confirmPassword !== '' && { 124 | icon: showConfirmPass ? 'eye slash' : 'eye', 125 | onClick: () => setShowConfirmPass(!showConfirmPass), 126 | } 127 | } 128 | /> 129 | 130 | 144 |
149 | Already have an account? Login. 150 |
151 | 152 | {error && } 153 | 154 |
155 | ); 156 | }; 157 | 158 | export default RegisterForm; 159 | -------------------------------------------------------------------------------- /client/src/components/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | import Search from './Search'; 4 | import AddContactModal from '../components/AddContactModal'; 5 | import ContactsDisplay from '../components/ContactsDisplay'; 6 | import RegisterForm from '../components/RegisterForm'; 7 | import LoginForm from '../components/LoginForm'; 8 | import storageService from '../utils/localStorageHelpers'; 9 | 10 | const Routes = ({ 11 | contacts, 12 | setContacts, 13 | user, 14 | setUser, 15 | search, 16 | setSearch, 17 | options, 18 | handleOptionAddition, 19 | notify, 20 | isLoading, 21 | isDarkMode, 22 | }) => { 23 | return ( 24 | 25 | 26 | {storageService.loadUser() || user ? ( 27 | <> 28 | 33 | 40 | 50 | 51 | ) : ( 52 | 53 | )} 54 | 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default Routes; 70 | -------------------------------------------------------------------------------- /client/src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input } from 'semantic-ui-react'; 3 | 4 | const Search = ({ search, setSearch, isDarkMode }) => { 5 | const handleSearch = (e) => { 6 | setSearch(e.target.value); 7 | }; 8 | 9 | return ( 10 |
11 | setSearch(''), 26 | } 27 | } 28 | /> 29 |
30 | ); 31 | }; 32 | 33 | export default Search; 34 | -------------------------------------------------------------------------------- /client/src/data/demoCreds.js: -------------------------------------------------------------------------------- 1 | const demoCredentials = "Email: 'test@test.com' & password: 'password'"; 2 | 3 | export default demoCredentials; 4 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | width: calc(100vw - 34px); 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | .container { 17 | margin-bottom: 2em; 18 | } 19 | 20 | .main-segment { 21 | width: 100vw; 22 | display: flex; 23 | flex-direction: column; 24 | flex: 1; 25 | min-height: 100vh; 26 | padding: 0 !important; 27 | } 28 | 29 | a { 30 | color: #009c95; 31 | } 32 | 33 | a:hover { 34 | color: #06746e; 35 | filter: brightness(60%); 36 | } 37 | 38 | .nav-bar { 39 | height: 4em !important; 40 | position: sticky; 41 | top: 1px; 42 | z-index: 10; 43 | } 44 | 45 | .nav-bar .menu .item { 46 | font-size: 1.2em !important; 47 | } 48 | 49 | .nav-bar .menu a:hover { 50 | color: rgb(7, 63, 63) !important; 51 | } 52 | 53 | .nav-title { 54 | display: flex; 55 | flex-direction: column; 56 | } 57 | 58 | .nav-logo { 59 | height: 25px !important; 60 | font-size: 1.3em; 61 | margin-bottom: 0.2em; 62 | align-self: flex-start; 63 | } 64 | 65 | .nav-link { 66 | color: rgb(9, 85, 85); 67 | } 68 | 69 | .nav-link:hover { 70 | color: rgba(9, 85, 85, 50); 71 | } 72 | 73 | .name-header { 74 | margin-left: 8px; 75 | } 76 | 77 | .search-card { 78 | margin-bottom: 1em !important; 79 | } 80 | 81 | .contacts-display { 82 | margin-top: 20px; 83 | } 84 | 85 | .modal { 86 | padding: 10px; 87 | } 88 | 89 | .avatar-label { 90 | margin-top: 3px !important; 91 | margin-left: 0px !important; 92 | } 93 | 94 | .avatar-image:hover { 95 | cursor: pointer; 96 | } 97 | 98 | .avatar-preview { 99 | margin: 0 auto; 100 | } 101 | 102 | .upload-preview { 103 | display: block; 104 | margin: 1.5em auto; 105 | } 106 | 107 | .clear-preview-btn { 108 | margin-top: 0.3em !important; 109 | } 110 | 111 | .card-header { 112 | display: flex !important; 113 | justify-content: space-between !important; 114 | align-items: center !important; 115 | } 116 | 117 | .contact-edit-btn { 118 | margin-right: 1em !important; 119 | } 120 | 121 | .edit-btn .icon { 122 | color: #49769c !important; 123 | } 124 | 125 | .delete-btn .icon { 126 | color: #db2828 !important; 127 | } 128 | 129 | .login-reg-card { 130 | padding-left: 14em !important; 131 | padding-right: 14em !important; 132 | padding-bottom: 2em !important; 133 | } 134 | 135 | .main-text { 136 | margin-top: 3em !important; 137 | } 138 | 139 | .auth-form { 140 | margin-bottom: 2em; 141 | } 142 | 143 | .dark-mode-input input { 144 | background: #1b1c1d !important; 145 | color: rgb(233, 230, 230) !important; 146 | border: 1px solid rgb(233, 230, 230) !important; 147 | } 148 | 149 | .dark-mode-card .content { 150 | background: #1b1c1d !important; 151 | color: rgb(233, 230, 230) !important; 152 | } 153 | 154 | .dark-mode-card .content strong { 155 | color: rgb(233, 230, 230) !important; 156 | } 157 | 158 | .dark-mode-modal { 159 | background: #1b1c1d !important; 160 | color: rgb(233, 230, 230) !important; 161 | } 162 | 163 | .dark-mode-modal .header { 164 | background: #1b1c1d !important; 165 | color: rgb(233, 230, 230) !important; 166 | } 167 | 168 | .dark-mode-modal .content { 169 | background: #1b1c1d !important; 170 | color: rgb(233, 230, 230) !important; 171 | } 172 | 173 | .dark-mode-modal label { 174 | background: #1b1c1d !important; 175 | color: rgb(233, 230, 230) !important; 176 | } 177 | 178 | .dark-mode-modal input { 179 | background: #1b1c1d !important; 180 | color: rgb(233, 230, 230) !important; 181 | border: 1px solid white !important; 182 | } 183 | 184 | .dark-mode-modal .field .dropdown { 185 | background: #1b1c1d !important; 186 | color: rgb(233, 230, 230) !important; 187 | border: 1px solid white !important; 188 | } 189 | 190 | .dark-mode-modal .field .dropdown i { 191 | color: rgb(233, 230, 230) !important; 192 | border-width: 0 !important; 193 | border-bottom-width: 1px !important; 194 | } 195 | 196 | .dark-mode-modal .field .dropdown .menu .item { 197 | background: #1b1c1d !important; 198 | color: rgb(233, 230, 230) !important; 199 | } 200 | 201 | .dark-mode-modal .field .dropdown .menu .selected { 202 | color: rgb(194, 229, 229) !important; 203 | background-color: rgb(70, 70, 63) !important; 204 | } 205 | 206 | .dark-mode-modal .field .dropdown .menu .item:hover { 207 | background-color: rgb(95, 95, 91) !important; 208 | } 209 | 210 | .dark-mode-modal i { 211 | color: rgb(233, 230, 230) !important; 212 | } 213 | 214 | .dark-mode-modal label i { 215 | color: black !important; 216 | } 217 | 218 | .dark-mode-modal .actions { 219 | background: #1b1c1d !important; 220 | } 221 | 222 | .dark-mode-auth-form label { 223 | color: rgb(233, 230, 230) !important; 224 | } 225 | 226 | .dark-mode-auth-form .header { 227 | color: rgb(233, 230, 230) !important; 228 | } 229 | 230 | .dark-mode-auth-form input { 231 | background: #1b1c1d !important; 232 | color: rgb(233, 230, 230) !important; 233 | border: 1px solid rgb(233, 230, 230) !important; 234 | } 235 | 236 | .dark-mode-auth-form .input i { 237 | color: rgb(233, 230, 230) !important; 238 | } 239 | 240 | .dark-mode-auth-form input:autofill { 241 | color: #1b1c1d !important; 242 | border: 1px solid rgb(233, 230, 230) !important; 243 | } 244 | 245 | .dark-mode-auth-form .input .button i { 246 | color: black !important; 247 | } 248 | 249 | .dark-mode-clear-btn i { 250 | color: black !important; 251 | } 252 | 253 | .dark-mode-info-text { 254 | color: rgb(233, 230, 230) !important; 255 | } 256 | 257 | .dark-mode-menu { 258 | background: #24282c !important; 259 | } 260 | 261 | .dark-mode-menu .item * { 262 | color: rgb(233, 230, 230) !important; 263 | } 264 | 265 | .dark-mode-segment { 266 | border: 1px solid rgb(233, 230, 230) !important; 267 | } 268 | 269 | @media only screen and (max-width: 768px) { 270 | body { 271 | width: 100vw; 272 | } 273 | 274 | .edit-btn { 275 | padding: 5px !important; 276 | } 277 | 278 | .delete-btn { 279 | padding: 5px !important; 280 | } 281 | 282 | .contact-edit-btn { 283 | padding: 8px !important; 284 | } 285 | 286 | .contact-del-btn { 287 | padding: 8px !important; 288 | } 289 | 290 | .login-reg-card { 291 | padding-left: 1em !important; 292 | padding-right: 1em !important; 293 | padding-bottom: 1em !important; 294 | } 295 | 296 | .login-reg-bottom-text { 297 | margin-top: 4em !important; 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | import { ToastProvider } from 'react-toast-notifications'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /client/src/services/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import backendUrl from '../backendUrl'; 3 | 4 | const login = async (credentials) => { 5 | const response = await axios.post(`${backendUrl}/api/login`, credentials); 6 | return response.data; 7 | }; 8 | 9 | const register = async (enteredData) => { 10 | const response = await axios.post(`${backendUrl}/api/register`, enteredData); 11 | return response.data; 12 | }; 13 | 14 | export default { login, register }; 15 | -------------------------------------------------------------------------------- /client/src/services/contacts.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import backendUrl from '../backendUrl'; 3 | 4 | const baseUrl = `${backendUrl}/api/contacts`; 5 | 6 | let token = null; 7 | 8 | const setToken = (newToken) => { 9 | token = newToken; 10 | }; 11 | 12 | const setConfig = () => { 13 | return { 14 | headers: { 'x-auth-token': token }, 15 | }; 16 | }; 17 | 18 | const getAll = async () => { 19 | const response = await axios.get(baseUrl, setConfig()); 20 | return response.data; 21 | }; 22 | 23 | const addNew = async (contactObj) => { 24 | const response = await axios.post(baseUrl, contactObj, setConfig()); 25 | return response.data; 26 | }; 27 | 28 | const deleteContact = async (id) => { 29 | const response = await axios.delete(`${baseUrl}/${id}`, setConfig()); 30 | return response.data; 31 | }; 32 | 33 | const editContact = async (id, contactObj) => { 34 | const response = await axios.patch( 35 | `${baseUrl}/${id}/name_dp`, 36 | contactObj, 37 | setConfig() 38 | ); 39 | return response.data; 40 | }; 41 | 42 | const addLink = async (id, linkObj) => { 43 | const response = await axios.post( 44 | `${baseUrl}/${id}/url`, 45 | linkObj, 46 | setConfig() 47 | ); 48 | return response.data; 49 | }; 50 | 51 | const editLink = async (id, urlId, linkObj) => { 52 | const response = await axios.patch( 53 | `${baseUrl}/${id}/url/${urlId}`, 54 | linkObj, 55 | setConfig() 56 | ); 57 | return response.data; 58 | }; 59 | 60 | const deleteLink = async (id, urlId) => { 61 | const response = await axios.delete( 62 | `${baseUrl}/${id}/url/${urlId}`, 63 | setConfig() 64 | ); 65 | return response.data; 66 | }; 67 | 68 | export default { 69 | setToken, 70 | getAll, 71 | addNew, 72 | deleteContact, 73 | editContact, 74 | addLink, 75 | editLink, 76 | deleteLink, 77 | }; 78 | -------------------------------------------------------------------------------- /client/src/utils/arraysAndFuncs.js: -------------------------------------------------------------------------------- 1 | export const optionsArray = [ 2 | { key: 'fb', text: 'Facebook', value: 'Facebook', icon: 'facebook' }, 3 | { key: 'ig', text: 'Instagram', value: 'Instagram', icon: 'instagram' }, 4 | { key: 'tw', text: 'Twitter', value: 'Twitter', icon: 'twitter' }, 5 | { key: 'gh', text: 'Github', value: 'Github', icon: 'github' }, 6 | { key: 'yt', text: 'Youtube', value: 'Youtube', icon: 'youtube' }, 7 | ]; 8 | 9 | export const siteIconsArray = [ 10 | 'facebook', 11 | 'github', 12 | 'youtube', 13 | 'twitter', 14 | 'instagram', 15 | 'blogger', 16 | 'linkedin', 17 | 'reddit', 18 | 'medium', 19 | 'telegram', 20 | 'stack overflow', 21 | 'spotify', 22 | ]; 23 | 24 | const labelColors = [ 25 | 'red', 26 | 'orange', 27 | 'yellow', 28 | 'olive', 29 | 'green', 30 | 'teal', 31 | 'blue', 32 | 'violet', 33 | 'purple', 34 | 'pink', 35 | 'brown', 36 | 'grey', 37 | 'black', 38 | ]; 39 | 40 | export const randomColor = () => { 41 | return labelColors[Math.floor(Math.random() * labelColors.length)]; 42 | }; 43 | 44 | export const getCircularAvatar = (imageLink) => { 45 | const firstPart = imageLink.split('image/upload/')[0]; 46 | const secondPart = imageLink.split('image/upload/')[1]; 47 | const transformApi = 'w_200,h_200,c_fill,r_max/e_trim/'; 48 | 49 | return [firstPart, transformApi, secondPart].join(''); 50 | }; 51 | 52 | export const generateBase64Encode = (file, setState) => { 53 | const reader = new FileReader(); 54 | reader.readAsDataURL(file); 55 | reader.onloadend = () => { 56 | setState(reader.result); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /client/src/utils/localStorageHelpers.js: -------------------------------------------------------------------------------- 1 | const storageKeyToken = 'profileStoreUserKey'; 2 | const storageKeyDarkMode = 'profileStoreDarkMode'; 3 | 4 | const saveUser = (user) => 5 | localStorage.setItem(storageKeyToken, JSON.stringify(user)); 6 | 7 | const loadUser = () => JSON.parse(localStorage.getItem(storageKeyToken)); 8 | 9 | const logoutUser = () => localStorage.removeItem(storageKeyToken); 10 | 11 | const saveDarkMode = (boolean) => 12 | localStorage.setItem(storageKeyDarkMode, boolean); 13 | 14 | const loadDarkMode = () => localStorage.getItem(storageKeyDarkMode); 15 | 16 | export default { 17 | saveUser, 18 | loadUser, 19 | logoutUser, 20 | saveDarkMode, 21 | loadDarkMode, 22 | }; 23 | -------------------------------------------------------------------------------- /screenshots/auth-forms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/auth-forms.png -------------------------------------------------------------------------------- /screenshots/desktop-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/desktop-tablet.png -------------------------------------------------------------------------------- /screenshots/mobile-ui-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/mobile-ui-1.png -------------------------------------------------------------------------------- /screenshots/mobile-ui-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/mobile-ui-2.png -------------------------------------------------------------------------------- /screenshots/modals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amand33p/profile-store/6fb31f5b5cb5ab39c420ef1945385acd1d0f8a9a/screenshots/modals.png -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": 12 11 | }, 12 | "rules": {} 13 | } 14 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | require('express-async-errors'); 3 | const cors = require('cors'); 4 | const middleware = require('./utils/middleware'); 5 | const authRoutes = require('./routes/auth'); 6 | const contactRoutes = require('./routes/contact'); 7 | 8 | const app = express(); 9 | 10 | app.use(cors()); 11 | app.use(express.json({ limit: '10mb' })); 12 | app.use(express.urlencoded({ limit: '10mb', extended: true })); 13 | 14 | app.use('/api', authRoutes); 15 | app.use('/api/contacts', contactRoutes); 16 | 17 | app.use(middleware.unknownEndpointHandler); 18 | app.use(middleware.errorHandler); 19 | 20 | module.exports = app; 21 | -------------------------------------------------------------------------------- /server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const bcrypt = require('bcrypt'); 3 | const User = require('../models/user'); 4 | const validator = require('validator'); 5 | const { SECRET } = require('../utils/config'); 6 | 7 | const loginUser = async (req, res) => { 8 | const { email, password } = req.body; 9 | 10 | const user = await User.findOne({ email }); 11 | 12 | if (!user) { 13 | return res 14 | .status(400) 15 | .send({ error: 'No account with this email has been registered.' }); 16 | } 17 | 18 | const credentialsValid = await bcrypt.compare(password, user.passwordHash); 19 | 20 | if (!credentialsValid) { 21 | return res.status(401).send({ error: 'Invalid credentials.' }); 22 | } 23 | 24 | const payloadForToken = { 25 | id: user._id, 26 | }; 27 | 28 | const token = jwt.sign(payloadForToken, SECRET); 29 | 30 | res 31 | .status(200) 32 | .send({ token, displayName: user.displayName, email: user.email }); 33 | }; 34 | 35 | const registerUser = async (req, res) => { 36 | const { displayName, email, password } = req.body; 37 | 38 | if (!password || password.length < 6) { 39 | return res 40 | .status(400) 41 | .send({ error: 'Password needs to be atleast 6 characters long.' }); 42 | } 43 | 44 | if (!email || !validator.isEmail(email)) { 45 | return res.status(400).send({ error: 'Valid email address is required.' }); 46 | } 47 | 48 | const existingUser = await User.findOne({ email }); 49 | 50 | if (existingUser) { 51 | return res 52 | .status(400) 53 | .send({ error: 'An account with this email already exists.' }); 54 | } 55 | 56 | const saltRounds = 10; 57 | const passwordHash = await bcrypt.hash(password, saltRounds); 58 | 59 | const user = new User({ 60 | displayName, 61 | email, 62 | passwordHash, 63 | }); 64 | 65 | const savedUser = await user.save(); 66 | 67 | const payloadForToken = { 68 | id: savedUser._id, 69 | }; 70 | 71 | const token = jwt.sign(payloadForToken, SECRET); 72 | res.status(200).send({ 73 | token, 74 | displayName: savedUser.displayName, 75 | email: savedUser.email, 76 | }); 77 | }; 78 | 79 | module.exports = { loginUser, registerUser }; 80 | -------------------------------------------------------------------------------- /server/controllers/contact.js: -------------------------------------------------------------------------------- 1 | const Contact = require('../models/contact'); 2 | const User = require('../models/user'); 3 | const validator = require('validator'); 4 | const { cloudinary, UPLOAD_PRESET } = require('../utils/config'); 5 | 6 | const getContacts = async (req, res) => { 7 | const allContacts = await Contact.find({ user: req.user }); 8 | res.json(allContacts); 9 | }; 10 | 11 | const createNewContact = async (req, res) => { 12 | const { name, contacts, displayPicture } = req.body; 13 | 14 | if (!contacts.url || !validator.isURL(contacts.url)) { 15 | return res 16 | .status(401) 17 | .send({ error: 'Valid URL is required for link field.' }); 18 | } 19 | 20 | if (!contacts.site) { 21 | return res.status(401).send({ error: 'Site name is required.' }); 22 | } 23 | 24 | const user = await User.findById(req.user); 25 | 26 | if (!user) { 27 | return res.status(404).send({ error: 'User does not exist in database.' }); 28 | } 29 | 30 | const newPerson = new Contact({ 31 | name, 32 | contacts, 33 | user: user._id, 34 | }); 35 | 36 | if (displayPicture) { 37 | const uploadedImage = await cloudinary.uploader.upload( 38 | displayPicture, 39 | { 40 | upload_preset: UPLOAD_PRESET, 41 | }, 42 | (error) => { 43 | if (error) return res.status(401).send({ error: error.message }); 44 | } 45 | ); 46 | 47 | newPerson.displayPicture = { 48 | exists: true, 49 | link: uploadedImage.url, 50 | public_id: uploadedImage.public_id, 51 | }; 52 | } 53 | 54 | const savedPerson = await newPerson.save(); 55 | return res.status(201).json(savedPerson); 56 | }; 57 | 58 | const deleteContact = async (req, res) => { 59 | const { id } = req.params; 60 | 61 | const user = await User.findById(req.user); 62 | const person = await Contact.findById(id); 63 | 64 | if (!user) { 65 | return res.status(404).send({ error: 'User does not exist in database.' }); 66 | } 67 | 68 | if (!person) { 69 | return res 70 | .status(404) 71 | .send({ error: `Contact with ID: ${id} does not exist in database.` }); 72 | } 73 | 74 | if (person.user.toString() !== user._id.toString()) { 75 | return res.status(401).send({ error: 'Access is denied.' }); 76 | } 77 | 78 | if (person.displayPicture.exists === true) { 79 | await cloudinary.uploader.destroy( 80 | person.displayPicture.public_id, 81 | (error) => { 82 | if (error) res.status(401).send({ error: error.message }); 83 | } 84 | ); 85 | } 86 | 87 | await Contact.findByIdAndDelete(id); 88 | res.status(204).end(); 89 | }; 90 | 91 | const updateContactNameDP = async (req, res) => { 92 | const { id } = req.params; 93 | const { name, displayPicture } = req.body; 94 | 95 | if (!name) { 96 | return res.status(401).send({ error: 'Name field is required.' }); 97 | } 98 | 99 | const user = await User.findById(req.user); 100 | const person = await Contact.findById(id); 101 | 102 | if (!user) { 103 | return res.status(404).send({ error: 'User does not exist in database.' }); 104 | } 105 | 106 | if (!person) { 107 | return res 108 | .status(404) 109 | .send({ error: `Contact with ID: ${id} does not exist in database.` }); 110 | } 111 | 112 | if (person.user.toString() !== user._id.toString()) { 113 | return res.status(401).send({ error: 'Access is denied.' }); 114 | } 115 | 116 | if (displayPicture) { 117 | const uploadedImage = await cloudinary.uploader.upload( 118 | displayPicture, 119 | { 120 | upload_preset: UPLOAD_PRESET, 121 | }, 122 | (error) => { 123 | if (error) return res.status(401).send({ error: error.message }); 124 | } 125 | ); 126 | 127 | await cloudinary.uploader.destroy( 128 | person.displayPicture.public_id, 129 | (error) => { 130 | if (error) res.status(401).send({ error: error.message }); 131 | } 132 | ); 133 | 134 | person.displayPicture = { 135 | exists: true, 136 | link: uploadedImage.url, 137 | public_id: uploadedImage.public_id, 138 | }; 139 | } 140 | 141 | person.name = name; 142 | 143 | await person.save(); 144 | res.status(202).json(person); 145 | }; 146 | 147 | const addProfileUrl = async (req, res) => { 148 | const { id } = req.params; 149 | const { url, site } = req.body; 150 | 151 | if (!site) { 152 | return res.status(401).send({ error: 'Site name is required.' }); 153 | } 154 | 155 | if (!url || !validator.isURL(url)) { 156 | return res 157 | .status(401) 158 | .send({ error: 'Valid URL is required for link field.' }); 159 | } 160 | 161 | const user = await User.findById(req.user); 162 | const person = await Contact.findById(id); 163 | 164 | if (!user) { 165 | return res.status(404).send({ error: 'User does not exist in database.' }); 166 | } 167 | 168 | if (!person) { 169 | return res 170 | .status(404) 171 | .send({ error: `Contact with ID: ${id} does not exist in database.` }); 172 | } 173 | 174 | if (person.user.toString() !== user._id.toString()) { 175 | return res.status(401).send({ error: 'Access is denied.' }); 176 | } 177 | 178 | const newContact = { 179 | url, 180 | site, 181 | }; 182 | 183 | person.contacts = [...person.contacts, newContact]; 184 | const savedPerson = await person.save(); 185 | 186 | res.status(201).json(savedPerson); 187 | }; 188 | 189 | const updateProfileUrl = async (req, res) => { 190 | const { id } = req.params; 191 | const { urlId } = req.params; 192 | const { url, site } = req.body; 193 | 194 | if (!site) { 195 | return res.status(401).send({ error: 'Site name is required.' }); 196 | } 197 | 198 | if (!url || !validator.isURL(url)) { 199 | return res 200 | .status(401) 201 | .send({ error: 'Valid URL is required for link field.' }); 202 | } 203 | 204 | const user = await User.findById(req.user); 205 | const person = await Contact.findById(id); 206 | 207 | if (!user) { 208 | return res.status(404).send({ error: 'User does not exist in database.' }); 209 | } 210 | 211 | if (!person) { 212 | return res 213 | .status(404) 214 | .send({ error: `Contact with ID: ${id} does not exist in database.` }); 215 | } 216 | 217 | if (person.user.toString() !== user._id.toString()) { 218 | return res.status(401).send({ error: 'Access is denied.' }); 219 | } 220 | 221 | const urlToUpdate = person.contacts.find((c) => c.id === urlId); 222 | 223 | if (!urlToUpdate) { 224 | return res 225 | .status(404) 226 | .send({ error: `URL with ID: ${urlId} does not exist in database.` }); 227 | } 228 | 229 | urlToUpdate.url = url; 230 | urlToUpdate.site = site; 231 | 232 | person.contacts = person.contacts.map((c) => 233 | c.id !== urlId ? c : urlToUpdate 234 | ); 235 | 236 | await person.save(); 237 | res.status(202).json(urlToUpdate); 238 | }; 239 | 240 | const deleteProfileUrl = async (req, res) => { 241 | const { id } = req.params; 242 | const { urlId } = req.params; 243 | 244 | const user = await User.findById(req.user); 245 | const person = await Contact.findById(id); 246 | 247 | if (!user) { 248 | return res.status(404).send({ error: 'User does not exist in database.' }); 249 | } 250 | 251 | if (!person) { 252 | return res 253 | .status(404) 254 | .send({ error: `Contact with ID: ${id} does not exist in database.` }); 255 | } 256 | 257 | if (person.user.toString() !== user._id.toString()) { 258 | return res.status(401).send({ error: 'Access is denied.' }); 259 | } 260 | 261 | person.contacts = person.contacts.filter((c) => c.id !== urlId); 262 | 263 | await person.save(); 264 | res.status(204).end(); 265 | }; 266 | 267 | module.exports = { 268 | getContacts, 269 | createNewContact, 270 | deleteContact, 271 | updateContactNameDP, 272 | addProfileUrl, 273 | deleteProfileUrl, 274 | updateProfileUrl, 275 | }; 276 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { MONGODB_URI: url } = require('./utils/config'); 3 | 4 | const connectToDB = async () => { 5 | try { 6 | await mongoose.connect(url, { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | useCreateIndex: true, 10 | useFindAndModify: false, 11 | }); 12 | 13 | console.log('Connected to MongoDB!'); 14 | } catch (error) { 15 | console.error(`Error while connecting to MongoDB: `, error.message); 16 | } 17 | }; 18 | 19 | module.exports = connectToDB; 20 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const http = require('http'); 3 | const { PORT } = require('./utils/config'); 4 | const connectToDB = require('./db'); 5 | 6 | connectToDB(); 7 | 8 | const server = http.createServer(app); 9 | 10 | server.listen(PORT, () => { 11 | console.log(`Server running on port ${PORT}`); 12 | }); 13 | -------------------------------------------------------------------------------- /server/models/contact.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const uniqueValidator = require('mongoose-unique-validator'); 3 | 4 | const urlSchema = new mongoose.Schema({ 5 | url: { 6 | type: String, 7 | required: true, 8 | trim: true, 9 | }, 10 | site: { 11 | type: String, 12 | required: true, 13 | trim: true, 14 | }, 15 | }); 16 | 17 | const contactSchema = new mongoose.Schema({ 18 | name: { 19 | type: String, 20 | required: true, 21 | trim: true, 22 | }, 23 | contacts: [urlSchema], 24 | user: { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: 'User', 27 | }, 28 | displayPicture: { 29 | exists: { 30 | type: Boolean, 31 | required: true, 32 | default: 'false', 33 | }, 34 | link: { 35 | type: String, 36 | required: true, 37 | default: 'null', 38 | }, 39 | public_id: { 40 | type: String, 41 | required: true, 42 | default: 'null', 43 | }, 44 | }, 45 | }); 46 | 47 | contactSchema.plugin(uniqueValidator); 48 | 49 | // replaces _id with id, convert id to string from ObjectID and deletes __v 50 | contactSchema.set('toJSON', { 51 | transform: (_document, returnedObject) => { 52 | returnedObject.id = returnedObject._id.toString(); 53 | delete returnedObject._id; 54 | delete returnedObject.__v; 55 | }, 56 | }); 57 | 58 | urlSchema.set('toJSON', { 59 | transform: (_document, returnedObject) => { 60 | returnedObject.id = returnedObject._id.toString(); 61 | delete returnedObject._id; 62 | }, 63 | }); 64 | 65 | module.exports = mongoose.model('Contact', contactSchema); 66 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const uniqueValidator = require('mongoose-unique-validator'); 3 | 4 | const userSchema = new mongoose.Schema({ 5 | displayName: { 6 | type: String, 7 | minlength: 3, 8 | maxlength: 15, 9 | required: true, 10 | trim: true, 11 | }, 12 | email: { 13 | type: String, 14 | required: true, 15 | unique: true, 16 | trim: true, 17 | }, 18 | passwordHash: { 19 | type: String, 20 | required: true, 21 | }, 22 | }); 23 | 24 | userSchema.plugin(uniqueValidator); 25 | 26 | // replaces _id with id, convert id to string from ObjectID and deletes __v 27 | userSchema.set('toJSON', { 28 | transform: (_document, returnedObject) => { 29 | returnedObject.id = returnedObject._id.toString(); 30 | delete returnedObject._id; 31 | delete returnedObject.__v; 32 | delete returnedObject.passwordHash; 33 | }, 34 | }); 35 | 36 | module.exports = mongoose.model('User', userSchema); 37 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "profile_project_repo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "bcrypt": "^5.0.0", 8 | "cloudinary": "^1.23.0", 9 | "cors": "^2.8.5", 10 | "dotenv": "^8.2.0", 11 | "express": "^4.17.1", 12 | "express-async-errors": "^3.1.1", 13 | "jsonwebtoken": "^8.5.1", 14 | "mongoose": "^5.9.25", 15 | "mongoose-unique-validator": "^2.0.3", 16 | "validator": "^13.1.1" 17 | }, 18 | "scripts": { 19 | "start": "node index.js", 20 | "dev": "nodemon index.js", 21 | "build:ui": "rm -rf build && cd ../client && rm -rf build && npm run build --prod && cp -r build ../server" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "devDependencies": { 27 | "eslint": "^7.18.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { loginUser, registerUser } = require('../controllers/auth'); 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/register', registerUser); 7 | router.post('/login', loginUser); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /server/routes/contact.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | getContacts, 4 | createNewContact, 5 | deleteContact, 6 | updateContactNameDP, 7 | addProfileUrl, 8 | deleteProfileUrl, 9 | updateProfileUrl, 10 | } = require('../controllers/contact'); 11 | const { auth } = require('../utils/middleware'); 12 | 13 | const router = express.Router(); 14 | 15 | router.get('/', auth, getContacts); 16 | router.post('/', auth, createNewContact); 17 | router.delete('/:id', auth, deleteContact); 18 | router.patch('/:id/name_dp', auth, updateContactNameDP); 19 | router.post('/:id/url', auth, addProfileUrl); 20 | router.patch('/:id/url/:urlId', auth, updateProfileUrl); 21 | router.delete('/:id/url/:urlId', auth, deleteProfileUrl); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /server/utils/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const cloudinary = require('cloudinary').v2; 3 | 4 | const PORT = process.env.PORT; 5 | const MONGODB_URI = process.env.MONGODB_URI; 6 | const SECRET = process.env.SECRET; 7 | const UPLOAD_PRESET = process.env.UPLOAD_PRESET || 'ml_default'; 8 | 9 | cloudinary.config({ 10 | cloud_name: process.env.CLOUDINARY_NAME, 11 | api_key: process.env.CLOUDINARY_API_KEY, 12 | api_secret: process.env.CLOUDINARY_API_SECRET, 13 | }); 14 | 15 | module.exports = { 16 | PORT, 17 | MONGODB_URI, 18 | SECRET, 19 | cloudinary, 20 | UPLOAD_PRESET, 21 | }; 22 | -------------------------------------------------------------------------------- /server/utils/middleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { SECRET } = require('../utils/config'); 3 | 4 | const auth = (req, res, next) => { 5 | try { 6 | const token = req.header('x-auth-token'); 7 | 8 | if (!token) { 9 | return res 10 | .status(401) 11 | .send({ error: 'No auth token found. Authorization denied.' }); 12 | } 13 | 14 | const decodedToken = jwt.verify(token, SECRET); 15 | 16 | if (!decodedToken.id) { 17 | return res 18 | .status(401) 19 | .send({ error: 'Token verification failed. Authorization denied.' }); 20 | } 21 | 22 | req.user = decodedToken.id; 23 | 24 | next(); 25 | } catch (error) { 26 | res.status(500).send({ error: error.message }); 27 | } 28 | }; 29 | 30 | const unknownEndpointHandler = (_req, res) => { 31 | res.status(404).send({ error: 'Unknown endpoint.' }); 32 | }; 33 | 34 | const errorHandler = (error, _req, res, next) => { 35 | console.error(error.message); 36 | 37 | if (error.name === 'CastError' && error.kind === 'ObjectId') { 38 | return res.status(400).send({ error: 'Malformatted ID.' }); 39 | } else if (error.name === 'ValidationError') { 40 | return res.status(400).send({ error: error.message }); 41 | } else if (error.name === 'JsonWebTokenError') { 42 | return res.status(401).send({ error: 'Invalid token.' }); 43 | } 44 | 45 | next(error); 46 | }; 47 | 48 | module.exports = { 49 | auth, 50 | unknownEndpointHandler, 51 | errorHandler, 52 | }; 53 | --------------------------------------------------------------------------------