From 44d3450b1040b31442d1933e48b6fb663f6e0728 Mon Sep 17 00:00:00 2001 From: Clayton Wilson Date: Sun, 27 Oct 2019 18:06:47 -0400 Subject: [PATCH] Profile image upload frontend and backend finished --- functions/handlers/users.js | 53 +++++++- functions/index.js | 8 +- functions/package.json | 1 + functions/util/fbAuth.js | 12 +- twistter-frontend/src/pages/editProfile.js | 121 +++++++++++++++++- .../src/redux/actions/userActions.js | 23 +++- .../src/redux/reducers/userReducer.js | 8 +- 7 files changed, 210 insertions(+), 16 deletions(-) diff --git a/functions/handlers/users.js b/functions/handlers/users.js index 1af539d..5fc219c 100644 --- a/functions/handlers/users.js +++ b/functions/handlers/users.js @@ -53,6 +53,8 @@ exports.signup = (req, res) => { return res.status(400).json(errors); } + const noImg = 'no-img.png'; + let token, userId; db.doc(`/users/${newUser.handle}`) @@ -77,6 +79,7 @@ exports.signup = (req, res) => { email: newUser.email, handle: newUser.handle, createdAt: newUser.createdAt, + imageUrl: `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${noImg}?alt=media`, userId }; handle2Email.set(userCred.handle, userCred.email); @@ -207,7 +210,7 @@ exports.updateProfileInfo = (req, res) => { // Update the database entry for this user db.collection("users") .doc(req.user.handle) - .set(profileData, { merge: true }) + .set(profileData) .then(() => { console.log(`${req.user.handle}'s profile info has been updated.`); return res @@ -241,6 +244,7 @@ exports.getUserDetails = (req, res) => { }); }; +// Returns all data stored for a user exports.getAuthenticatedUser = (req, res) => { let credentials = {}; db.doc(`/users/${req.user.handle}`) @@ -258,4 +262,51 @@ exports.getAuthenticatedUser = (req, res) => { }); }; +// Uploads a profile image +exports.uploadProfileImage = (req, res) => { + const BusBoy = require("busboy"); + const path = require("path"); + const os = require("os"); + const fs = require("fs"); + const busboy = new BusBoy({ headers: req.headers }); + + let imageFileName; + let imageToBeUploaded = {}; + + busboy.on("file", (fieldname, file, filename, encoding, mimetype) => { + if (mimetype !== 'image/jpeg' && mimetype !== 'image/png') { + return res.status(400).json({ error: "Wrong filetype submitted" }); + } + // console.log(fieldname); + // console.log(filename); + // console.log(mimetype); + const imageExtension = filename.split(".")[filename.split(".").length - 1]; // Get the image file extension + imageFileName = `${Math.round(Math.random() * 100000000000)}.${imageExtension}`; // Get a random filename + const filepath = path.join(os.tmpdir(), imageFileName); + imageToBeUploaded = { filepath, mimetype }; + file.pipe(fs.createWriteStream(filepath)); + }); + busboy.on("finish", () => { + admin.storage().bucket().upload(imageToBeUploaded.filepath, { + resumable: false, + metadata: { + metadata: { + contentType: imageToBeUploaded.mimetype + } + } + }) + .then(() => { + const imageUrl = `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media`; + return db.doc(`/users/${req.user.handle}`).update({ imageUrl }); + }) + .then(() => { + return res.status(201).json({ message: "Image uploaded successfully"}); + }) + .catch((err) => { + console.error(err); + return res.status(500).json({ error: err.code}) + }) + }); + busboy.end(req.rawBody); +} diff --git a/functions/index.js b/functions/index.js index 737d44f..47dda28 100644 --- a/functions/index.js +++ b/functions/index.js @@ -16,7 +16,8 @@ const { login, signup, deleteUser, - updateProfileInfo + updateProfileInfo, + uploadProfileImage } = require("./handlers/users"); // Adds a user to the database and registers them in firebase with @@ -39,8 +40,13 @@ app.get("/getProfileInfo", fbAuth, getProfileInfo); // Updates the currently logged in user's profile information app.post("/updateProfileInfo", fbAuth, updateProfileInfo); +// Returns all user data for the logged in user. +// Used when setting the state in Redux. app.get("/user", fbAuth, getAuthenticatedUser); +// Uploads a profile image +app.post("/user/image", fbAuth, uploadProfileImage); + /*------------------------------------------------------------------* * handlers/post.js * *------------------------------------------------------------------*/ diff --git a/functions/package.json b/functions/package.json index 2e308b3..492ff05 100644 --- a/functions/package.json +++ b/functions/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "axios": "^0.19.0", + "busboy": "^0.3.1", "firebase": "^6.6.2", "firebase-admin": "^8.6.0", "firebase-functions": "^3.1.0" diff --git a/functions/util/fbAuth.js b/functions/util/fbAuth.js index 35253e7..d440bb9 100644 --- a/functions/util/fbAuth.js +++ b/functions/util/fbAuth.js @@ -4,12 +4,12 @@ const { admin, db } = require('./admin'); // The function will only execute if the user is logged in, or rather, they have // a valid token module.exports = (req, res, next) => { - console.log(req); - console.log(req.body); - console.log(req.headers); - console.log(req.headers.authorization); - console.log(JSON.stringify(req.body)); - console.log(JSON.stringify(req.header)); + // console.log(req); + // console.log(req.body); + // console.log(req.headers); + // console.log(req.headers.authorization); + // console.log(JSON.stringify(req.body)); + // console.log(JSON.stringify(req.header)); let idToken; diff --git a/twistter-frontend/src/pages/editProfile.js b/twistter-frontend/src/pages/editProfile.js index ff6f3f7..39ad93f 100644 --- a/twistter-frontend/src/pages/editProfile.js +++ b/twistter-frontend/src/pages/editProfile.js @@ -4,13 +4,23 @@ import PropTypes from "prop-types"; // TODO: Add a read-only '@' in the left side of the handle input // TODO: Add a cancel button, that takes the user back to their profile page +import noImage from '../images/no-img.png'; + // Material-UI stuff import Button from "@material-ui/core/Button"; +import Box from "@material-ui/core/Box"; import CircularProgress from "@material-ui/core/CircularProgress"; import Grid from "@material-ui/core/Grid"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import withStyles from "@material-ui/core/styles/withStyles"; +import IconButton from "@material-ui/core/IconButton"; +import EditIcon from "@material-ui/icons/Edit"; +import Tooltip from "@material-ui/core/Tooltip"; + +// Redux stuff +import { connect } from "react-redux"; +import { uploadImage } from "../redux/actions/userActions"; const styles = { form: { @@ -27,25 +37,52 @@ const styles = { positon: "relative", marginBottom: 30 }, + box: { + position: "relative" + }, progress: { position: "absolute" + }, + uploadProgress: { + position: "absolute", + marginLeft: -155, + marginTop: 95 } }; export class edit extends Component { + // mapReduxToState = (credentials) => { + // this.setState({ + // imageUrl: credentials.imageUrl ? credentials.imageUrl : noImage, + // firstName: credentials.firstName ? credentials.firstName : '', + // lastName: credentials.lastName ? credentials.lastName : '', + // email: credentials.email ? credentials.email : 'error, email doesn\'t exist', + // handle: credentials.handle ? credentials.handle : 'error, handle doesn\'t exist', + // bio: credentials.bio ? credentials.bio : '' + // }); + // }; + + + // Runs as soon as the page loads. // Sets the default values of all the textboxes to the data // that is stored in the database for the user. componentDidMount() { + // const { credentials } = this.props; + // console.log(this.props.user); + // this.mapReduxToState(credentials); axios .get("/getProfileInfo") .then((res) => { + // Need to have the ternary if statements, because react throws an error if + // any of the res.data keys are undefined this.setState({ - firstName: res.data.firstName, - lastName: res.data.lastName, + imageUrl: res.data.imageUrl, + firstName: res.data.firstName ? res.data.firstName : "", + lastName: res.data.lastName ? res.data.lastName : "", email: res.data.email, handle: res.data.handle, - bio: res.data.bio + bio: res.data.bio ? res.data.bio : "" }); }) .catch((err) => { @@ -63,6 +100,7 @@ export class edit extends Component { constructor() { super(); this.state = { + imageUrl: "", firstName: "", lastName: "", email: "", @@ -108,6 +146,7 @@ export class edit extends Component { }) .catch((err) => { console.log(err); + // TODO: Should redirect to login page if they get a 403 this.setState({ errors: err.response.data, loading: false @@ -128,10 +167,65 @@ export class edit extends Component { }); }; + handleImageChange = (event) => { + const image = event.target.files[0]; + const formData = new FormData(); + formData.append('image', image, image.name); + this.props.uploadImage(formData); + } + + handleEditPicture = () => { + const fileInput = document.getElementById('imageUpload'); + fileInput.click(); + } + + // logging = () => { + // console.log(this.state); + // console.log(this.props); + // this.mapReduxToState(this.props.credentials); + // } + render() { const { classes } = this.props; + const uploading = this.props.UI.loading; const { errors, loading } = this.state; + // let imageMarkup = this.state.imageUrl ? ( + // + // ) : (); + + let imageMarkup = this.props.user.credentials.imageUrl ? ( + + + {uploading && ( + + )} + + ) : ( + + + {uploading && ( + + )} + + ) + return ( @@ -140,6 +234,13 @@ export class edit extends Component { Edit Profile
+ {imageMarkup} + + + + + + Submit {loading && ( @@ -234,8 +335,18 @@ export class edit extends Component { } } +const mapStateToProps = (state) => ({ + user: state.user, + UI: state.UI, + // credentials: state.user.credentials +}); + +const mapActionsToProps = { uploadImage } + edit.propTypes = { + uploadImage: PropTypes.func.isRequired, classes: PropTypes.object.isRequired }; -export default withStyles(styles)(edit); +// export default withStyles(styles)(edit); +export default connect(mapStateToProps, mapActionsToProps)(withStyles(styles)(edit)); \ No newline at end of file diff --git a/twistter-frontend/src/redux/actions/userActions.js b/twistter-frontend/src/redux/actions/userActions.js index 5993804..41bd4bb 100644 --- a/twistter-frontend/src/redux/actions/userActions.js +++ b/twistter-frontend/src/redux/actions/userActions.js @@ -1,8 +1,9 @@ -import {SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI, SET_AUTHENTICATED, SET_UNAUTHENTICATED} from '../types'; +import {SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI, SET_AUTHENTICATED, SET_UNAUTHENTICATED, LOADING_USER} from '../types'; import axios from 'axios'; - +// Gets Database info for the logged in user and sets it in Redux export const getUserData = () => (dispatch) => { + dispatch({ type: LOADING_USER }); axios.get('/user') .then((res) => { dispatch({ @@ -13,6 +14,7 @@ export const getUserData = () => (dispatch) => { .catch((err) => console.error(err)); } +// Sends login data to firebase and sets the user data in Redux export const loginUser = (loginData, history) => (dispatch) => { dispatch({ type: LOADING_UI }); axios @@ -33,6 +35,7 @@ export const loginUser = (loginData, history) => (dispatch) => { }); }; +// Sends signup data to firebase and sets the user data in Redux export const signupUser = (newUserData, history) => (dispatch) => { dispatch({ type: LOADING_UI }); axios @@ -55,12 +58,14 @@ export const signupUser = (newUserData, history) => (dispatch) => { }); }; +// Deletes the Authorization header and clears all user data from Redux export const logoutUser = () => (dispatch) => { localStorage.removeItem('FBIdToken'); delete axios.defaults.headers.common['Authorization']; dispatch({ type: SET_UNAUTHENTICATED }); } + export const deleteUser = () => (dispatch) => { axios .delete("/delete") @@ -81,8 +86,22 @@ export const deleteUser = () => (dispatch) => { dispatch({ type: SET_UNAUTHENTICATED }); } +// Saves Authorization in browser local storage and adds it as a header to axios const setAuthorizationHeader = (token) => { const FBIdToken = `Bearer ${token}`; localStorage.setItem('FBIdToken', FBIdToken); axios.defaults.headers.common['Authorization'] = FBIdToken; +} + +// Sends an image data form to firebase to be uploaded to the user profile +export const uploadImage = (formData) => (dispatch) => { + dispatch({ type: LOADING_UI }); + axios.post('/user/image', formData) + .then(() => { + dispatch(getUserData()); + dispatch({ type: CLEAR_ERRORS }); + }) + .catch(err => { + console.log(err); + }) } \ No newline at end of file diff --git a/twistter-frontend/src/redux/reducers/userReducer.js b/twistter-frontend/src/redux/reducers/userReducer.js index 7a29e90..e5081f2 100644 --- a/twistter-frontend/src/redux/reducers/userReducer.js +++ b/twistter-frontend/src/redux/reducers/userReducer.js @@ -1,4 +1,4 @@ -import {SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI, SET_AUTHENTICATED, SET_UNAUTHENTICATED} from '../types'; +import {SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI, SET_AUTHENTICATED, SET_UNAUTHENTICATED, LOADING_USER} from '../types'; const initialState = { authenticated: false, @@ -20,8 +20,14 @@ export default function(state = initialState, action) { case SET_USER: return { authenticated: true, + loading: false, ...action.payload, }; + case LOADING_USER: + return { + ...state, + loading: true + } default: return state; }