streetmix/streetmix

View on GitHub
app/resources/v1/street_images.js

Summary

Maintainability
F
4 days
Test Coverage
import cloudinary from 'cloudinary'
import models from '../../db/models/index.js'
import logger from '../../lib/logger.js'
import { SAVE_THUMBNAIL_EVENTS } from '../../lib/util.js'

const { User, Street } = models
const ALLOW_ANON_STREET_THUMBNAILS = false

export async function post (req, res) {
  let json

  // The request payload is a stringified JSON due to the data URL for the street thumbnail being too large.
  // Setting the bodyParser.text({ limit }) works for a specific route whereas bodyParser.json({ limit }) does not.
  // As a result of sending the request payload as `text/plain` we have to parse the JSON string to access the object values.
  try {
    json = await JSON.parse(req.body)
  } catch (error) {
    res.status(400).json({ status: 400, msg: 'Could not parse body as JSON.' })
    return
  }

  const { image, event, streetType, editCount, creatorId } = json

  if (!image) {
    res.status(400).json({ status: 400, msg: 'Image data not specified.' })
    return
  }

  if (!req.params.street_id) {
    res.status(400).json({ status: 400, msg: 'Please provide street ID.' })
    return
  }

  // 1) Check if street exists.
  let street

  try {
    street = await Street.findOne({ where: { id: req.params.street_id } })
  } catch (error) {
    logger.error(error)
    res.status(500).json({ status: 500, msg: 'Error finding street.' })
    return
  }

  if (!street) {
    res.status(404).json({ status: 404, msg: 'Street not found.' })
    return
  }

  const publicId =
    `${process.env.NODE_ENV}/street_thumbnails/` +
    (streetType || req.params.street_id)

  const details = {
    public_id: publicId,
    street_type: streetType,
    creatorId,
    edit_count: editCount
  }

  // 2) Check if street thumbnail exists.
  let resource

  try {
    resource = await cloudinary.v2.api.resource(publicId)
  } catch (err) {
    // If the http_code returned is 404, the street thumbnail does not exist which we shouldn't consider an error.
    if (err.error.http_code !== 404) {
      logger.error(err)
    }
  }

  // 3a) If street is a DEFAULT_STREET or EMPTY_STREET and thumbnail exists, return existing street thumbnail.
  // 3b) If nothing changed since the last street thumbnail upload (based on editCount), return existing street thumbnail.
  const tag = resource && resource.tags && resource.tags[0]
  const thumbnailSaved =
    (streetType && resource) ||
    (tag && editCount && Number.parseInt(tag, 10) === editCount)

  // Currently only uploading street thumbnails for initial street render. If not initial street render, only log details.
  if (
    event !== SAVE_THUMBNAIL_EVENTS.INITIAL &&
    event !== SAVE_THUMBNAIL_EVENTS.TEST
  ) {
    // If thumbnailSaved === true, then no upload would have been made.
    if (!thumbnailSaved) {
      logger.info({ event, ...details }, 'Uploading street thumbnail.')
    }

    res.status(501).json({
      status: 501,
      msg: 'Only saving initial street rendered thumbnail.'
    })
    return
  }

  const handleUploadSuccess = function (resource) {
    const thumbnail = {
      public_id: resource.public_id,
      width: resource.width,
      height: resource.height,
      format: resource.format,
      secure_url: resource.secure_url,
      created_at: resource.created_at
    }

    res.status(201).json(thumbnail)
  }

  const handleUploadStreetThumbnail = async function (publicId) {
    if (!publicId) {
      res
        .status(400)
        .json({ status: 400, msg: 'Please provide the public ID to be used.' })
      return
    }

    try {
      await cloudinary.v2.uploader.remove_all_tags([publicId])
      resource = await cloudinary.v2.uploader.upload(image, {
        public_id: publicId,
        tags: editCount
      })
    } catch (error) {
      logger.error(error)
    }

    if (!resource) {
      res
        .status(500)
        .json({ status: 500, msg: 'Error uploading street thumbnail.' })
      return
    }

    logger.info({ event, ...details }, 'Uploading street thumbnail.')
    return resource
  }

  const handleFindStreetWithCreator = async function (street) {
    if (!req.auth?.sub) {
      res.status(401).json({
        status: 401,
        msg: 'Sign in to upload street thumnail for owned street.'
      })
      return
    }

    let user

    try {
      user = await User.findOne({ where: { auth0_id: req.auth.sub } })
    } catch (error) {
      logger.error(error)
      res.status(500).json({ status: 500, msg: 'Error finding user.' })
      return
    }

    if (!user) {
      res.status(403).json({ status: 403, msg: 'User not found.' })
      return
    }

    if (street.creatorId.toString() !== user.id.toString()) {
      res.status(403).json({
        status: 403,
        msg: 'User does not have the right permissions to upload street thumbnail.'
      })
      return
    }

    const publicId = `${process.env.NODE_ENV}/street_thumbnails/${street.id}`
    return publicId
  }

  const handleError = function (error) {
    logger.error(error)
    res.status(500).end()
  }

  if (thumbnailSaved) {
    handleUploadSuccess(resource)
  } else if (!resource || (!street.creatorId && ALLOW_ANON_STREET_THUMBNAILS)) {
    // 3c) If street thumbnail does not exist, upload to Cloudinary no matter the currently signed in user.
    // 3d) If street was created by anonymous user, upload to Cloudinary.
    handleUploadStreetThumbnail(publicId)
      .then(handleUploadSuccess)
      .catch(handleError)
  } else if (street.creatorId) {
    // 3e) If street thumbnail already exists and street was created by a user, check if signed in user = creator.
    handleFindStreetWithCreator(street)
      .then(handleUploadStreetThumbnail)
      .then(handleUploadSuccess)
      .catch(handleError)
  } else {
    res.status(403).json({
      status: 403,
      msg: 'User does not have the right permissions to upload street thumbnail.'
    })
  }
}

export async function del (req, res) {
  if (!req.params.street_id) {
    res.status(400).json({ status: 400, msg: 'Please provide street ID.' })
    return
  }

  // 1) Verify user is logged in.
  if (!req.auth?.sub) {
    res.status(401).json({ status: 401, msg: 'Please provide user ID.' })
    return
  }

  let user
  try {
    user = await User.findOne({ where: { auth0_id: req.auth.sub } })
  } catch (error) {
    logger.error(error)
    res.status(500).json({ status: 500, msg: 'Error finding user.' })
    return
  }

  if (!user) {
    res.status(404).json({ status: 404, msg: 'User not found.' })
    return
  }

  // Is requesting user logged in?
  if (!req.auth?.sub || req.auth.sub !== user.auth0Id) {
    res.status(401).end()
    return
  }

  // 2) Check that street exists.
  // 3) Verify that street is owned by logged in user.
  let street

  try {
    street = await Street.findOne({ where: { id: req.params.street_id } })
  } catch (error) {
    logger.error(error)
    res.status(500).json({ status: 500, msg: 'Error finding street.' })
  }

  if (!street) {
    res.status(404).json({ status: 404, msg: 'Street not found.' })
    return
  } else if (street.creatorId.toString() !== user.id.toString()) {
    res.status(403).json({
      status: 403,
      msg: 'Signed in user cannot delete street thumbnail.'
    })
    return
  }

  // 4) Delete street thumbnail from cloudinary.
  const publicId = `${process.env.NODE_ENV}/street_thumbnails/${req.params.street_id}`
  cloudinary.v2.uploader.destroy(publicId, function (error, result) {
    if (error) {
      logger.error(error)
      res
        .status(500)
        .json({ status: 500, msg: 'Error deleting street thumbnail.' })
      return
    }

    res.status(204).end()
  })
}

export async function get (req, res) {
  if (!req.params.street_id) {
    res.status(400).json({ status: 400, msg: 'Please provide a street id.' })
    return
  }

  let resource

  try {
    const publicId = `${process.env.NODE_ENV}/street_thumbnails/${req.params.street_id}`
    resource = await cloudinary.v2.api.resource(publicId)
  } catch (error) {
    if (error?.error?.http_code === 404) {
      res
        .status(404)
        .json({ status: 404, msg: 'Could not find street thumbnail.' })
    } else {
      logger.error(error)
      res
        .status(500)
        .json({ status: 500, msg: 'Error finding street thumbnail.' })
    }
    return
  }

  if (!resource) {
    res
      .status(404)
      .json({ status: 404, msg: 'Could not find street thumbnail.' })
    return
  }

  res.status(200).json(resource)
}