diff --git a/functions/handlers/post.js b/functions/handlers/post.js index ae839c3..7cd966b 100644 --- a/functions/handlers/post.js +++ b/functions/handlers/post.js @@ -238,7 +238,8 @@ exports.quoteWithoutPost = (req, res) => { } }) .catch(err => { - return res.status(500).json({ error: "Something is wrong" }); + // return res.status(500).json({ error: "Something is wrong" }); + return res.status(500).json({ error: err }); }); }; @@ -259,7 +260,11 @@ exports.checkforLikePost = (req, res) => { result = true; return res.status(200).json(result); } - }); + }) + .catch((err) => { + console.log(err); + return res.status(500).json({error: err}); + }) }; exports.likePost = (req, res) => { @@ -303,7 +308,8 @@ exports.likePost = (req, res) => { } }) .catch(err => { - return res.status(500).json({ error: "Something is wrong" }); + // return res.status(500).json({ error: "Something is wrong" }); + return res.status(500).json({ error: err }); }); }; diff --git a/functions/handlers/users.js b/functions/handlers/users.js index 7e63f32..7d2b5c0 100644 --- a/functions/handlers/users.js +++ b/functions/handlers/users.js @@ -1,3 +1,4 @@ +/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/always-return */ const { admin, db } = require("../util/admin"); @@ -416,7 +417,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.status(201).json({ @@ -467,6 +468,7 @@ exports.getAllHandles = (req, res) => { }); }; +// Returns all data stored for a user exports.getAuthenticatedUser = (req, res) => { let credentials = {}; db.doc(`/users/${req.user.handle}`) @@ -602,6 +604,126 @@ exports.getSubs = (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 = {}; + let oldImageFileName = req.userData.imageUrl ? req.userData.imageUrl.split("/o/")[1].split("?alt")[0] : null; + // console.log(`old file: ${oldImageFileName}`); + + 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", () => { + // Save the file to the storage bucket + admin.storage().bucket(config.storageBucket).upload(imageToBeUploaded.filepath, { + resumable: false, + metadata: { + metadata: { + contentType: imageToBeUploaded.mimetype + } + } + }) + .then(() => { + // Add the new URL to the user's profile + const imageUrl = `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media`; + return db.doc(`/users/${req.user.handle}`).update({ imageUrl }); + }) + .then(() => { + // Delete their old image if they have one + if (oldImageFileName !== null && oldImageFileName !== "no-img.png") { + admin.storage().bucket(config.storageBucket).file(oldImageFileName).delete() + .then(() => { + return res.status(201).json({ message: "Image uploaded successfully1"}); + }) + .catch((err) => { + console.log(err); + return res.status(201).json({ message: "Image uploaded successfully2"}); + }) + // return res.status(201).json({ message: "Image uploaded successfully"}); + } else { + return res.status(201).json({ message: "Image uploaded successfully3"}); + } + + }) + .catch((err) => { + console.error(err); + return res.status(500).json({ error: err.code}) + }) + }); + busboy.end(req.rawBody); + + // const BusBoy = require('busboy'); + // const path = require('path'); + // const os = require('os'); + // const fs = require('fs'); + + // const busboy = new BusBoy({ headers: req.headers }); + + // let imageToBeUploaded = {}; + // let imageFileName; + + // busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + // // console.log(fieldname, file, filename, encoding, mimetype); + // if (mimetype !== 'image/jpeg' && mimetype !== 'image/png') { + // return res.status(400).json({ error: 'Wrong file type submitted' }); + // } + // // my.image.png => ['my', 'image', 'png'] + // const imageExtension = filename.split('.')[filename.split('.').length - 1]; + // // 32756238461724837.png + // imageFileName = `${Math.round( + // Math.random() * 1000000000000 + // ).toString()}.${imageExtension}`; + // const filepath = path.join(os.tmpdir(), imageFileName); + // imageToBeUploaded = { filepath, mimetype }; + // file.pipe(fs.createWriteStream(filepath)); + // }); + // busboy.on('finish', () => { + // admin + // .storage() + // .bucket(config.storageBucket) + // .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.json({ message: 'image uploaded successfully' }); + // }) + // .catch((err) => { + // console.error(err); + // return res.status(500).json({ error: 'something went wrong' }); + // }); + // }); + // busboy.end(req.rawBody); +} + exports.removeSub = (req, res) => { let new_following = []; let userRef = db.doc(`/users/${req.userData.handle}`); diff --git a/functions/index.js b/functions/index.js index 41cc61a..4f7c3fb 100644 --- a/functions/index.js +++ b/functions/index.js @@ -18,6 +18,7 @@ const { signup, deleteUser, updateProfileInfo, + uploadProfileImage, verifyUser, unverifyUser, getUserHandles, @@ -50,8 +51,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); + // Verifies the user sent to the request // Must be run by the Admin user app.post("/verifyUser", fbAuth, verifyUser); diff --git a/functions/package.json b/functions/package.json index 29b3e89..04a805c 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/package.json b/twistter-frontend/package.json index 52e50f3..3ac341d 100644 --- a/twistter-frontend/package.json +++ b/twistter-frontend/package.json @@ -43,5 +43,5 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:5001/twistter-e4649/us-central1/api" + "proxy": "https://us-central1-twistter-e4649.cloudfunctions.net/api" } diff --git a/twistter-frontend/src/pages/editProfile.js b/twistter-frontend/src/pages/editProfile.js index b4f7c2e..710636e 100644 --- a/twistter-frontend/src/pages/editProfile.js +++ b/twistter-frontend/src/pages/editProfile.js @@ -3,6 +3,8 @@ import { Link } from 'react-router-dom'; import axios from "axios"; import PropTypes from "prop-types"; +import noImage from '../images/no-img.png'; + // Material-UI stuff import Box from "@material-ui/core/Box" import Button from "@material-ui/core/Button"; @@ -12,6 +14,13 @@ import Popover from "@material-ui/core/Popover"; 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: { @@ -28,6 +37,9 @@ const styles = { positon: "relative", marginBottom: 30 }, + box: { + position: "relative" + }, back: { float: "left", marginLeft: 15 @@ -39,6 +51,11 @@ const styles = { progress: { position: "absolute" }, + uploadProgress: { + position: "absolute", + marginLeft: -155, + marginTop: 95 + }, popoverBackground: { marginTop: "-100px", width: "calc(100vw)", @@ -50,20 +67,38 @@ const styles = { }; export class editProfile 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); this.setState({pageLoading: true}) + 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 : "", pageLoading: false }); }) @@ -80,6 +115,7 @@ export class editProfile extends Component { constructor() { super(); this.state = { + imageUrl: "", firstName: "", lastName: "", email: "", @@ -126,6 +162,7 @@ export class editProfile 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 @@ -146,6 +183,26 @@ export class editProfile extends Component { }); }; + handleImageChange = (event) => { + if (event.target.files[0]) { + 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); + // } + handleOpenConfirmDelete = (event) => { this.setState({ // anchorEl: event.currentTarget @@ -162,8 +219,39 @@ export class editProfile extends Component { render() { const { classes } = this.props; + const uploading = this.props.UI.loading; const { errors, loading } = this.state; +// <<<<<<< edit-profile-image-upload + + let imageMarkup = this.props.user.credentials.imageUrl ? ( + + + {uploading && ( + + )} + + ) : ( + + + {uploading && ( + + )} + + ) + // Used for the delete button const open = Boolean(this.state.anchorEl); const id = open ? 'simple-popover' : undefined; @@ -177,6 +265,8 @@ export class editProfile extends Component {