diff --git a/.eslintignore b/.eslintignore index a1dd9427..07ed7069 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -build/* -functions/* \ No newline at end of file +build/* \ No newline at end of file diff --git a/functions/db/users/onDelete.function.js b/functions/db/users/onDelete.function.js new file mode 100644 index 00000000..c7e859ec --- /dev/null +++ b/functions/db/users/onDelete.function.js @@ -0,0 +1,9 @@ +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); + +export default functions.database + .ref('users/{uid}') + .onDelete((snapshot, context) => { + const { uid } = context.params; + return admin.auth().deleteUser(uid); + }); diff --git a/functions/db/users/onUpdate.function.js b/functions/db/users/onUpdate.function.js new file mode 100644 index 00000000..46e35b8d --- /dev/null +++ b/functions/db/users/onUpdate.function.js @@ -0,0 +1,20 @@ +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); + +export default functions.database + .ref('/users/{uid}') + .onUpdate((change, context) => { + const before = change.before.val(); + const after = change.after.val(); + const { isAdmin } = after; + + if (before.isAdmin === isAdmin) { + return null; + } + + const { uid } = context.params; + + return admin.auth().setCustomUserClaims(uid, { + isAdmin + }); + }); diff --git a/functions/requests/routes/users.js b/functions/requests/routes/users.js index e78b7a01..c9dda43f 100644 --- a/functions/requests/routes/users.js +++ b/functions/requests/routes/users.js @@ -1,30 +1,9 @@ const express = require('express'); const admin = require('firebase-admin'); -const cors = require('cors')({ origin: true }); -const Busboy = require('busboy'); -const path = require('path'); -const os = require('os'); -const fs = require('fs'); const uuid = require('uuid/v4'); const router = express.Router(); -const { storageBucket } = JSON.parse(process.env.FIREBASE_CONFIG); - -const bucket = admin.storage().bucket(storageBucket); - -const uploadImageToBucket = async uploadedImage => { - return bucket.upload(uploadedImage.file, { - uploadMedia: 'media', - metada: { - metadata: { - contentType: uploadedImage.type - } - }, - public: true - }); -}; - const createUserAuth = async (email, isAdmin) => { const { uid } = await admin.auth().createUser({ email, password: uuid() }); @@ -53,143 +32,4 @@ router.post('/', async (request, response) => { return response.status(200).json({ uid }); }); -router.delete('/:id', async (request, response) => { - const { id } = request.params; - - const userRef = admin.database().ref(`users/${id}`); - - const logoUrl = (await userRef.child('logoUrl').once('value')).val(); - - let removeLogo = Promise.resolve(); - - if (logoUrl) { - const fileName = logoUrl.split('/').pop(); - - removeLogo = bucket.file(fileName).delete(); - } - - const removeUserDb = userRef.remove(); - - const removeUserAuth = admin.auth().deleteUser(id); - - try { - await Promise.all([removeUserDb, removeUserAuth, removeLogo]); - } catch (error) { - console.error(error); - return response.status(500).json({ error }); - } - - return response.status(200).json({}); -}); - -const modifyUserAuth = (userId, isAdmin) => { - return admin.auth().setCustomUserClaims(userId, { - isAdmin - }); -}; - -const modifyUserDb = (name, location, createdAt, isAdmin, logoUrl, userId) => { - let params = { name, location, createdAt, isAdmin }; - - if (logoUrl) params['logoUrl'] = logoUrl; - - return admin - .database() - .ref(`users/${userId}`) - .update({ ...params }); -}; - -const removeOldLogo = async userId => { - const url = ( - await admin - .database() - .ref(`users/${userId}`) - .child('logoUrl') - .once('value') - ).val(); - - if (url) { - const fileName = url.split('/').pop(); - console.log('filename', fileName); - return bucket.file(fileName).delete(); - } else { - return Promise.resolve(); - } -}; - -router.patch('/:id', (request, response) => { - cors(request, response, () => { - const busboy = new Busboy({ headers: request.headers }); - - let uploadedImage = null; - - let fieldData = {}; - - busboy.on('field', (fieldName, value) => { - fieldData = { ...fieldData, [`${fieldName}`]: value }; - }); - - busboy.on('file', (fieldName, file, fileName, encoding, mimetype) => { - const filepath = path.join(os.tmpdir(), fileName); - - uploadedImage = { file: filepath, type: mimetype, fileName }; - - file.pipe(fs.createWriteStream(filepath)); - }); - - busboy.on('finish', async () => { - const { name, location, createdAt } = fieldData; - - const isAdmin = JSON.parse(fieldData.isAdmin); - - const { id } = request.params; - - const setUserClaims = modifyUserAuth(id, isAdmin); - - let removeLogo = Promise.resolve(); - let uploadImage = Promise.resolve(); - - if (uploadedImage) { - removeLogo = removeOldLogo(id); - uploadImage = uploadImageToBucket(uploadedImage); - } - - await removeLogo; - - let logoUrl = null; - - if (uploadedImage) { - logoUrl = `https://storage.googleapis.com/${bucket.name}/${uploadedImage.fileName}`; - } - - const modifyUser = modifyUserDb( - name, - location, - createdAt, - isAdmin, - logoUrl, - id - ); - - try { - await Promise.all([modifyUser, setUserClaims, uploadImage]); - } catch (error) { - console.error(error); - return response.status(500).json({ error }); - } - - return response.status(201).json({ - id, - name, - location, - logoUrl, - isAdmin, - createdAt - }); - }); - - busboy.end(request.rawBody); - }); -}); - module.exports = router; diff --git a/functions/storage/onFinalize.function.js b/functions/storage/onFinalize.function.js deleted file mode 100644 index 890dea4a..00000000 --- a/functions/storage/onFinalize.function.js +++ /dev/null @@ -1,85 +0,0 @@ -const { storage } = require('firebase-functions'); -const { Storage } = require('@google-cloud/storage'); -const { tmpdir } = require('os'); -const { join, dirname } = require('path'); -const sharp = require('sharp'); -const fs = require('fs-extra'); - -const gcs = new Storage(); - -export default storage.object().onFinalize(async object => { - if (!object.contentType.includes('image')) { - console.log('Uploaded file is not an image'); - return; - } - - if (object.metadata && object.metadata.resizedImage === 'true') { - console.log('Image already resized'); - return; - } - - const bucket = gcs.bucket(object.bucket); - const filePath = object.name; - const bucketFile = bucket.file(filePath); - const fileName = filePath.split('/').pop(); - const bucketDir = dirname(filePath); - - const workingDir = join(tmpdir(), 'temp'); - const tempFilePath = join(workingDir, 'source.png'); - - await fs.ensureDir(workingDir); - - try { - console.log('Downloading file from bucket'); - await bucketFile.download({ - destination: tempFilePath - }); - await bucketFile.delete(); - console.log('Finished downloading file from bucket'); - } catch (error) { - console.log('Error while downloading file from bucket', error); - } - - try { - console.log('Deleting old file from bucket'); - await bucketFile.delete(); - console.log('Deleted old file from bucket'); - } catch (error) { - console.log('Error while deleting old file from bucket', error); - } - - const sizes = [200]; - - const uploadPromises = sizes.map(async size => { - const resizedName = fileName; - const resizedPath = join(workingDir, resizedName); - - console.log(`Resizing image ${resizedName}`); - await sharp(tempFilePath) - .resize(size, size) - .toFile(resizedPath); - console.log(`Resized image ${resizedName}`); - - return bucket.upload(resizedPath, { - destination: join(bucketDir, resizedName), - metadata: { - metadata: { - resizedImage: true - } - }, - public: true - }); - }); - - try { - console.log('Resizing/uploading images to bucket'); - await Promise.all(uploadPromises); - console.log('Uploaded resized images to bucket'); - } catch (error) { - console.log('Error while resizing/uploading images to bucket', error); - } - - const cleaningPromises = [fs.remove(workingDir)]; - - await Promise.all(cleaningPromises); -}); diff --git a/src/state/actions/users.js b/src/state/actions/users.js index f85d1bd7..404d6899 100644 --- a/src/state/actions/users.js +++ b/src/state/actions/users.js @@ -1,11 +1,10 @@ import { createAction } from 'redux-act'; -import uuid from 'uuid/v4'; import { toastr } from 'react-redux-toastr'; import axios from 'utils/axios'; import { firebaseError } from 'utils'; import firebase from 'firebase.js'; -import { checkUserData, AUTH_UPDATE_USER_DATA } from './auth'; +import { checkUserData } from './auth'; export const USERS_FETCH_DATA_INIT = createAction('USERS_FETCH_DATA_INIT'); export const USERS_FETCH_DATA_SUCCESS = createAction( @@ -71,22 +70,36 @@ export const fetchUsers = () => { ); }; }; +const deleteLogo = async id => { + const userRef = firebase.database().ref(`users/${id}`); + const oldLogo = (await userRef.child('logoUrl').once('value')).val(); + if (oldLogo) { + await firebase + .storage() + .ref(oldLogo) + .delete(); + } +}; export const deleteUser = id => { return async dispatch => { dispatch(USERS_DELETE_USER_INIT()); - const user = firebase.auth().currentUser; - - const userToken = await user.getIdToken(); - try { - await axios(userToken).delete(`/users/${id}`); + await deleteLogo(id); } catch (error) { - toastr.error('', error); - return dispatch(USERS_DELETE_USER_FAIL({ error })); + const errorMessage = firebaseError(error.response.data.error.code); + toastr.error('', errorMessage); + return dispatch( + USERS_DELETE_USER_FAIL({ + error: errorMessage + }) + ); } + const userRef = firebase.database().ref(`users/${id}`); + + userRef.remove(); toastr.success('', 'The user was deleted.'); return dispatch(USERS_DELETE_USER_SUCCESS({ id })); }; @@ -98,6 +111,20 @@ export const clearUsersData = () => { }; }; +const uploadLogo = async (uid, file) => { + const storageRef = firebase.storage().ref(); + + const fileExtension = file.name.split('.').pop(); + + const fileName = `${uid}.${fileExtension}`; + + const basePath = 'users/'; + + await storageRef.child(`${basePath}${fileName}`).put(file); + + return `${basePath}${uid}_200x200.${fileExtension}`; +}; + export const createUser = ({ name, email, @@ -127,17 +154,11 @@ export const createUser = ({ } const { uid } = response.data; - let path = null; - if (file) { - const storageRef = firebase.storage().ref(); - - const fileExtension = file.name.split('.').pop(); - const fileName = `${uid}.${fileExtension}`; - - const basePath = 'users/'; + let logoUrl = null; + if (file) { try { - await storageRef.child(`${basePath}${fileName}`).put(file); + logoUrl = await uploadLogo(uid, file); } catch (error) { const errorMessage = firebaseError(error.code); toastr.error('', errorMessage); @@ -147,21 +168,12 @@ export const createUser = ({ }) ); } - path = `${basePath}${uid}_200x200.${fileExtension}`; } - try { await firebase .database() .ref(`users/${uid}`) - .set({ - name, - email, - location, - logoUrl: path, - createdAt, - isAdmin - }); + .set({ name, email, location, logoUrl, createdAt, isAdmin }); } catch (error) { const errorMessage = firebaseError(error.code); toastr.error('', errorMessage); @@ -206,56 +218,64 @@ export const modifyUser = ({ return async dispatch => { dispatch(USERS_MODIFY_USER_INIT()); - const user = firebase.auth().currentUser; - - const userToken = await user.getIdToken(); - - const body = new FormData(); - + let logoUrl = null; if (file) { - const fileExtension = file.name.split('.')[1]; - - const fileName = `${uuid()}.${fileExtension}`; + try { + await deleteLogo(id); + } catch (error) { + const errorMessage = firebaseError(error.code); + toastr.error('', errorMessage); + return dispatch( + USERS_MODIFY_USER_FAIL({ + error: errorMessage + }) + ); + } + } - body.append('logo', file, fileName); + try { + logoUrl = await uploadLogo(id, file); + } catch (error) { + const errorMessage = firebaseError(error.code); + toastr.error('', errorMessage); + return dispatch( + USERS_MODIFY_USER_FAIL({ + error: errorMessage + }) + ); } - body.append('name', name); - body.append('location', location); - body.append('createdAt', createdAt); - body.append('isAdmin', isAdmin); + const userData = { + name, + location, + createdAt, + isAdmin + }; - axios(userToken) - .patch(`/users/${id}`, body) - .then(response => { - const userCreated = response.data; - const { uid } = firebase.auth().currentUser; + if (logoUrl) userData.logoUrl = logoUrl; - if (id === uid) { - dispatch(AUTH_UPDATE_USER_DATA({ ...userCreated })); - } + try { + await firebase + .database() + .ref(`users/${id}`) + .update({ ...userData }); + } catch (error) { + const errorMessage = firebaseError(error.code); + toastr.error('', errorMessage); + return dispatch( + USERS_MODIFY_USER_FAIL({ + error: errorMessage + }) + ); + } - if (isProfile) { - toastr.success('', 'Profile updated successfully'); - } else if (isEditing) { - toastr.success('', 'User updated successfully'); - } + if (isProfile) { + toastr.success('', 'Profile updated successfully'); + } else if (isEditing) { + toastr.success('', 'User updated successfully'); + } - return dispatch( - USERS_MODIFY_USER_SUCCESS({ - user: userCreated - }) - ); - }) - .catch(error => { - const errorMessage = firebaseError(error.response.data.error.code); - toastr.error('', errorMessage); - return dispatch( - USERS_MODIFY_USER_FAIL({ - error: errorMessage - }) - ); - }); + return dispatch(USERS_MODIFY_USER_SUCCESS({ user: { ...userData, id } })); }; };