From daffaa1d6acf35a3c50888d0ddec44dcaa6f9e6c Mon Sep 17 00:00:00 2001 From: dfamilia33 Date: Tue, 22 Mar 2022 17:19:37 -0400 Subject: [PATCH 1/2] Profile Pic Selector Component --- .../tutorComponents/ProfilePicModal.tsx | 81 +++++++++++++++++++ .../components/tutorComponents/Settings.tsx | 38 +++++++-- 2 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 client/src/components/tutorComponents/ProfilePicModal.tsx diff --git a/client/src/components/tutorComponents/ProfilePicModal.tsx b/client/src/components/tutorComponents/ProfilePicModal.tsx new file mode 100644 index 0000000..ae80464 --- /dev/null +++ b/client/src/components/tutorComponents/ProfilePicModal.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { api } from "../../services/api"; +import { Name } from "../../services/api.types"; +import { selectClientData } from "../../store/ClientData/selectors"; +import { actions as clientDataActions } from "../../store/ClientData/slice"; +import { Container, Row, ListGroup, ListGroupItem, Modal, ModalHeader, ModalBody, InputGroup, Input, ModalFooter, Button } from "reactstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import Avatar from "react-avatar-edit"; +import "../../containers/DashboardPage/ClientSettings.css"; +import defaultUser from "../../assets/default_user.png"; +import { CompressAndSaveImg } from "../../services/tools"; + + + export interface IProfilePicModalProps { + clientImg:string, + imgModalOpen:boolean, + croppedImg:string, + setImgModalOpen: (arg:boolean) => void, + cancelImgChange: () => void, + setCroppedImg: (arg:string) =>void, + setClientImg: (arg:string) =>void, +} + +export const ProfilePicModal = (props:IProfilePicModalProps) => { + let clientData = useSelector(selectClientData); + let dispatch = useDispatch(); + + const {clientImg,imgModalOpen,croppedImg} = props; + const {setImgModalOpen,cancelImgChange,setCroppedImg,setClientImg} = props; + + const handleImageSave = async (img: string) => { + await api.SetClientProfileImage(img, clientData.clientId); + setClientImg(img); + dispatch(clientDataActions.setProfileImage(img)); + } + + const saveImgChange = async () => { + if(croppedImg.toString() !== "") { + CompressAndSaveImg(croppedImg, clientData.first_name + clientData.last_name + "-photo", handleImageSave); + } else { + handleImageSave(croppedImg); + } + + setImgModalOpen(false); + } + return ( + <> + + setImgModalOpen(true)}> + + + {setImgModalOpen(!imgModalOpen)}} className="img-modal"> + {cancelImgChange()}}>Edit Profile Photo + + Change your profile photo here. +
+ setCroppedImg(img)} + onClose={() => {setCroppedImg("")}} + onBeforeFileLoad={() => {}} + src={clientImg === "" ? defaultUser : clientImg} + /> +
+ + + + +
+ + + + ); + + +} \ No newline at end of file diff --git a/client/src/components/tutorComponents/Settings.tsx b/client/src/components/tutorComponents/Settings.tsx index 11d96bd..b19d785 100644 --- a/client/src/components/tutorComponents/Settings.tsx +++ b/client/src/components/tutorComponents/Settings.tsx @@ -3,8 +3,11 @@ import classNames from "classnames"; import { Container, Row, Col, ListGroup, ListGroupItem, Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, InputGroup, Input, Alert, Nav, NavItem, NavLink, TabContent, TabPane } from "reactstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEdit, faBan, faPlus, faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; +import {ProfilePicModal,IProfilePicModalProps} from './ProfilePicModal' import Slider from 'react-rangeslider'; import Autocomplete from 'react-autocomplete'; +import Avatar from "react-avatar-edit"; +import defaultUser from "../../assets/default_user.png"; import TimePicker from 'rc-time-picker'; import moment from 'moment'; import Cookies from "js-cookie"; @@ -49,7 +52,9 @@ interface iSettingsState { interval_modal: boolean, schedule_modal: boolean, add_time_err: boolean, - add_time_err_msg: string + add_time_err_msg: string, + imgModalOpen:boolean, + croppedImg:string } @@ -65,7 +70,7 @@ class Settings extends Component { temp_firstn: "", temp_lastn: "", email: "test2@gmail.com", - obj_id: userid !== undefined ? userid : "61a5a9bbc73a5d336d8d0b74", + obj_id: "61a5a9bbc73a5d336d8d0b74", profile_pic: "", description: "Typescript port", temp_description: "", @@ -114,7 +119,9 @@ class Settings extends Component { interval_modal: false, schedule_modal: false, add_time_err: false, - add_time_err_msg: "" + add_time_err_msg: "", + imgModalOpen:false, + croppedImg:"" } as iSettingsState; } @@ -139,7 +146,7 @@ class Settings extends Component { temp_lastn: tutor.last_name, meeting_interval: parseInt(tutor.interval), temp_meeting_interval: parseInt(tutor.interval), - profile_pic:tutor.profile_img, + profile_pic:tutor.profile_img || defaultUser, description: tutor.description || "", temp_description: tutor.description || "" @@ -590,8 +597,24 @@ class Settings extends Component { return hrs + ":" + mins + meridiem; } + + + render(): JSX.Element{ let schedule_days = [{day: "Sunday", abbr: "SUN"}, {day: "Monday", abbr: "MON"}, {day: "Tuesday", abbr: "TUE"}, {day: "Wednesday", abbr: "WED"}, {day: "Thursday", abbr: "THU"}, {day: "Friday", abbr: "FRI"}, {day: "Saturday", abbr: "SAT"}]; + + + + const profilePicProps = { + clientImg:this.state.profile_pic, + imgModalOpen:this.state.imgModalOpen, + croppedImg:this.state.croppedImg, + setImgModalOpen: (arg:boolean) => this.setState({...this.state,imgModalOpen:arg}), + setCroppedImg: (arg:string) =>this.setState({...this.state,croppedImg:arg}), + setClientImg: (arg:string) =>this.setState({...this.state,profile_pic:arg}), + cancelImgChange: () => this.setState({...this.state,croppedImg:"",imgModalOpen:false}), + + } as IProfilePicModalProps; return ( @@ -601,9 +624,14 @@ class Settings extends Component { - + {/* {'profile + */} + + + + {this.state.first_name + " " + this.state.last_name} From 2c9bf8029003f0c63b98758bc0f2e4dd56ecfff0 Mon Sep 17 00:00:00 2001 From: dfamilia33 Date: Mon, 11 Apr 2022 23:42:55 -0400 Subject: [PATCH 2/2] Tutor Profile Final + Route Permissions --- .../tutorComponents/ProfilePicModal.tsx | 35 +++++++++------- .../components/tutorComponents/Settings.tsx | 27 +++++++++---- .../containers/DashboardPage/Dashboard.tsx | 2 - client/src/services/api.ts | 15 ++++++- server/routes/api/tutors.js | 40 +++++++++++++++++++ server/routes/api/users.js | 22 +++++++++- 6 files changed, 115 insertions(+), 26 deletions(-) diff --git a/client/src/components/tutorComponents/ProfilePicModal.tsx b/client/src/components/tutorComponents/ProfilePicModal.tsx index ae80464..9617a14 100644 --- a/client/src/components/tutorComponents/ProfilePicModal.tsx +++ b/client/src/components/tutorComponents/ProfilePicModal.tsx @@ -1,10 +1,6 @@ -import React, { useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; +import React from "react"; import { api } from "../../services/api"; -import { Name } from "../../services/api.types"; -import { selectClientData } from "../../store/ClientData/selectors"; -import { actions as clientDataActions } from "../../store/ClientData/slice"; -import { Container, Row, ListGroup, ListGroupItem, Modal, ModalHeader, ModalBody, InputGroup, Input, ModalFooter, Button } from "reactstrap"; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from "reactstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEdit } from "@fortawesome/free-solid-svg-icons"; import Avatar from "react-avatar-edit"; @@ -14,31 +10,41 @@ import { CompressAndSaveImg } from "../../services/tools"; export interface IProfilePicModalProps { + isTutor:boolean, + firstName:string, + lastName:string, clientImg:string, imgModalOpen:boolean, croppedImg:string, setImgModalOpen: (arg:boolean) => void, cancelImgChange: () => void, setCroppedImg: (arg:string) =>void, - setClientImg: (arg:string) =>void, + setClientImg: (arg:string) =>void, + userid:string, } export const ProfilePicModal = (props:IProfilePicModalProps) => { - let clientData = useSelector(selectClientData); - let dispatch = useDispatch(); - const {clientImg,imgModalOpen,croppedImg} = props; + const {clientImg,imgModalOpen,croppedImg,userid} = props; const {setImgModalOpen,cancelImgChange,setCroppedImg,setClientImg} = props; const handleImageSave = async (img: string) => { - await api.SetClientProfileImage(img, clientData.clientId); + + if (props.isTutor === true){ + console.log(userid) + await api.SetTutorProfileImage(img, userid); + } + else{ + + await api.SetClientProfileImage(img, userid); + + } setClientImg(img); - dispatch(clientDataActions.setProfileImage(img)); } const saveImgChange = async () => { if(croppedImg.toString() !== "") { - CompressAndSaveImg(croppedImg, clientData.first_name + clientData.last_name + "-photo", handleImageSave); + CompressAndSaveImg(croppedImg, props.firstName + props.lastName + "-photo", handleImageSave); } else { handleImageSave(croppedImg); } @@ -59,12 +65,13 @@ export const ProfilePicModal = (props:IProfilePicModalProps) => { setCroppedImg(img)} onClose={() => {setCroppedImg("")}} onBeforeFileLoad={() => {}} - src={clientImg === "" ? defaultUser : clientImg} + src={clientImg} /> diff --git a/client/src/components/tutorComponents/Settings.tsx b/client/src/components/tutorComponents/Settings.tsx index b19d785..d0f9396 100644 --- a/client/src/components/tutorComponents/Settings.tsx +++ b/client/src/components/tutorComponents/Settings.tsx @@ -4,9 +4,11 @@ import { Container, Row, Col, ListGroup, ListGroupItem, Button, Modal, ModalHead import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEdit, faBan, faPlus, faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import {ProfilePicModal,IProfilePicModalProps} from './ProfilePicModal' +import { TutorDataSlice } from "../../store/TutorData/types"; +import { ClientDataSlice } from "../../store/ClientData/types"; +import { connect } from 'react-redux'; import Slider from 'react-rangeslider'; import Autocomplete from 'react-autocomplete'; -import Avatar from "react-avatar-edit"; import defaultUser from "../../assets/default_user.png"; import TimePicker from 'rc-time-picker'; import moment from 'moment'; @@ -18,7 +20,8 @@ import "./settings.css"; import 'rc-time-picker/assets/index.css'; interface iSettingsProps { - + clientData:ClientDataSlice, + tutorData: TutorDataSlice } interface iSettingsState { @@ -59,6 +62,7 @@ interface iSettingsState { } +//to-do: connect to Redux store Client + Tutor Data slice class Settings extends Component { constructor(props: iSettingsProps) { @@ -70,7 +74,7 @@ class Settings extends Component { temp_firstn: "", temp_lastn: "", email: "test2@gmail.com", - obj_id: "61a5a9bbc73a5d336d8d0b74", + obj_id: userid || "61a5a9bbc73a5d336d8d0b74", profile_pic: "", description: "Typescript port", temp_description: "", @@ -602,17 +606,19 @@ class Settings extends Component { render(): JSX.Element{ let schedule_days = [{day: "Sunday", abbr: "SUN"}, {day: "Monday", abbr: "MON"}, {day: "Tuesday", abbr: "TUE"}, {day: "Wednesday", abbr: "WED"}, {day: "Thursday", abbr: "THU"}, {day: "Friday", abbr: "FRI"}, {day: "Saturday", abbr: "SAT"}]; - - const profilePicProps = { + isTutor:true, + firstName:this.state.first_name, + lastName:this.state.last_name, clientImg:this.state.profile_pic, imgModalOpen:this.state.imgModalOpen, croppedImg:this.state.croppedImg, setImgModalOpen: (arg:boolean) => this.setState({...this.state,imgModalOpen:arg}), setCroppedImg: (arg:string) =>this.setState({...this.state,croppedImg:arg}), setClientImg: (arg:string) =>this.setState({...this.state,profile_pic:arg}), - cancelImgChange: () => this.setState({...this.state,croppedImg:"",imgModalOpen:false}), + cancelImgChange: () => this.setState({...this.state,croppedImg:"",imgModalOpen:false}), + userid:this.state.obj_id } as IProfilePicModalProps; return ( @@ -901,4 +907,11 @@ class Settings extends Component { } -export default Settings; \ No newline at end of file +function mapStateToProps(state: any) { + return { + tutorData: state.tutorData, + clientData: state.clientData + } +} + +export default connect(mapStateToProps)(Settings); \ No newline at end of file diff --git a/client/src/containers/DashboardPage/Dashboard.tsx b/client/src/containers/DashboardPage/Dashboard.tsx index 00bee2b..8245a42 100644 --- a/client/src/containers/DashboardPage/Dashboard.tsx +++ b/client/src/containers/DashboardPage/Dashboard.tsx @@ -9,9 +9,7 @@ import { actions as clientDataActions } from "../../store/ClientData/slice"; import { selectClientData } from "../../store/ClientData/selectors"; import {useDispatch, useSelector} from 'react-redux' import {selectSidebarToggled} from "../../store/ClientFlowData/selectors"; -import TutorPanelBlank from "./TutorPanelSignup"; import { api } from "../../services/api"; -import { Spinner } from "reactstrap"; import TutorPanelSignup from "./TutorPanelSignup"; export interface IParams { diff --git a/client/src/services/api.ts b/client/src/services/api.ts index 40c251e..6b8d4a5 100644 --- a/client/src/services/api.ts +++ b/client/src/services/api.ts @@ -41,7 +41,7 @@ export class ApiService { public async GetTutorById(id: String) { console.log("Fetching Tutor"); - let url = this.tutorsEndpoint + 'tutor/' + id; + let url = this.tutorsEndpoint + id; let response = await axios.get(url); let tutor: TutorsResponse = {data: []} tutor.data = response.data; @@ -197,6 +197,19 @@ export class ApiService { return await axios.put(url, body, {withCredentials: true}); } + + public async SetTutorProfileImage(img: String, id: String) { + let url = this.tutorsEndpoint + 'tutor'; + let body = { + userid: id, + profile_img: img + } + + return await axios.put(url, body, {withCredentials: true}); + } + + + public async SetMeetingLink(id: String, link: String) { let url = this.appointmentsEndpoint + 'link'; let body = { diff --git a/server/routes/api/tutors.js b/server/routes/api/tutors.js index 30c97af..946678a 100644 --- a/server/routes/api/tutors.js +++ b/server/routes/api/tutors.js @@ -21,6 +21,9 @@ let router = express.Router(); //Email notify admins of new tutor signup applications var emailsender = require("../../lib/emailsender.js"); +//dev mode +const developer = process.env.NODE_ENV !== 'production'; + //Models const mongoose = require('mongoose'); const Subject = require('../../models/Subject'); @@ -31,6 +34,10 @@ const TutorApplication = require('../../models/TutorApplication'); // Middleware const withAuth = require('../../middleware/token_auth') +const jwt = require('jsonwebtoken'); +const parseCookies = require("../../lib/parseCookies"); +const secret = require("../../config/secret"); +const passport = require('passport'); //mongoose.set('useFindAndModify', false); @@ -130,6 +137,23 @@ router.post("/", withAuth, (req, res) => { // PUT /api/tutors/tutor // Update a tutor router.put("/tutor", withAuth, (req, res) => { + + const token = parseCookies(req.headers.cookie).token + + //protect route + if(!!!developer){ + jwt.verify(token, secret, function (err, decoded) { + if (err) { + + res.status(401).send('Unauthorized: Invalid token'); + } else if (req.body.userid !== decoded.userid) { + res.status(403).send(`Forbidden: Access denied ${req.body.userid} ${decoded.userid}`); + + } else{ + true; + }}); + } + const entries = Object.keys(req.body) const updates = {} @@ -159,6 +183,22 @@ router.put("/tutor", withAuth, (req, res) => { // Update a tutor router.delete("/:tutor_id", withAuth, (req, res) => { + const token = parseCookies(req.headers.cookie).token + + //protect route + if(!!!developer){ + jwt.verify(token, secret, function (err, decoded) { + if (err) { + + res.status(401).send('Unauthorized: Invalid token'); + } else if (req.body.userid !== decoded.userid) { + res.status(403).send('Forbidden: Access denied'); + + } else{ + true; + }}); + } + Tutor.deleteOne( { _id: req.params.tutor_id } ) diff --git a/server/routes/api/users.js b/server/routes/api/users.js index 5c7c7b0..4df13db 100644 --- a/server/routes/api/users.js +++ b/server/routes/api/users.js @@ -20,10 +20,11 @@ const secret = require('../../config/secret'); * @namespace userRouter */ let router = express.Router(); - +//dev mode +const developer = process.env.NODE_ENV !== 'production'; // Middleware const withAuth = require('../../middleware/token_auth'); - +const parseCookies = require("../../lib/parseCookies"); /** * Route serving subjects form. * @name get/api/users @@ -143,6 +144,23 @@ router.post('/', async (req, res) => { // PUT /api/users/user // Update a user router.put("/user", withAuth, (req, res) => { + + const token = parseCookies(req.headers.cookie).token + + if(!!!developer){ + jwt.verify(token, secret, function (err, decoded) { + console.log(req.body.userid) + console.log(decoded.userid) + if (err) { + + res.status(401).send('Unauthorized: Invalid token'); + } else if (req.body.userid !== decoded.userid) { + res.status(403).send('Forbidden: Access denied'); + + } else{ + true; + }}); + } const entries = Object.keys(req.body) const updates = {}