streetmix/streetmix

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

Summary

Maintainability
D
2 days
Test Coverage
import Sequelize from 'sequelize'
import { v4 as uuidv4 } from 'uuid'
import models from '../../db/models/index.js'
import logger from '../../lib/logger.js'

const { User, Vote, Street } = models

const MAX_COMMENT_LENGTH = 280
const SURVEY_FINISHED_PATH = '/survey-finished'

export function generateRandomBallotFetch ({ redirect = false }) {
  return async function (req, res) {
    let ballots
    const authUser = req.auth || {}
    let user
    if (authUser.sub) {
      try {
        user = await User.findOne({ where: { auth0Id: authUser.sub } })
      } catch (error) {
        logger.error(error)
        res.status(500).json({ status: 500, msg: 'Error finding user.' })
        return
      }
    }

    try {
      let hasValidStreet = false
      while (!hasValidStreet) {
        if (!req.auth) {
          ballots = await Vote.findAll({
            where: {
              voterId: {
                [Sequelize.Op.is]: null
              }
            },
            order: Sequelize.literal('random()'),
            limit: 1
          })
        } else {
          ballots = await Vote.findAll({
            // where (submitted does not contain the user's auth0 ID, or is empty) AND (voterId is null)
            where: {
              [Sequelize.Op.and]: [
                {
                  [Sequelize.Op.or]: [
                    {
                      [Sequelize.Op.not]: {
                        submitted: {
                          [Sequelize.Op.contains]: [user.id]
                        }
                      }
                    },
                    {
                      submitted: {
                        [Sequelize.Op.is]: null
                      }
                    }
                  ]
                },
                {
                  voterId: {
                    [Sequelize.Op.is]: null
                  }
                }
              ]
            },
            order: Sequelize.literal('random()'),
            limit: 1
          })
        }
        if (ballots && ballots.length > 0) {
          const myBallot = ballots[0]
          const { streetId } = myBallot
          if (streetId) {
            const streetForBallot = await Street.findOne({
              where: { id: streetId }
            })

            // do not return a vote to the same user
            const isCreator = user && user.id === streetForBallot.creatorId

            if (
              !isCreator &&
              streetForBallot &&
              streetForBallot.status &&
              streetForBallot.status === 'ACTIVE'
            ) {
              hasValidStreet = true
            } else if (isCreator) {
              // update existing ballot
              await Vote.update(
                {
                  submitted: Sequelize.fn(
                    'array_append',
                    Sequelize.col('submitted'),
                    user.id
                  )
                },
                {
                  where: {
                    streetId: streetForBallot.id,
                    voterId: {
                      [Sequelize.Op.is]: null
                    }
                  }
                }
              )
            } else {
              // since streets cannot be deleted, this ballot is no longer valid
              myBallot.voterId = 'DELETED'
              await myBallot.save()
            }
          } else {
            return res
              .status(500)
              .json({ status: 500, msg: 'Error fetching street for ballot.' })
          }
        }

        if (!ballots || ballots.length === 0) {
          // no ballots remaining
          if (redirect) {
            return res.redirect(SURVEY_FINISHED_PATH)
          } else {
            return res.status(204).json({
              status: 204,
              msg: 'All eligible streets have been voted on.'
            })
          }
        }
      }
    } catch (error) {
      logger.error(error)
      res.status(500).json({ status: 500, msg: 'Error fetching ballots.' })
      return
    }

    if (redirect) {
      let street
      let candidateStreetUrl = '/'
      try {
        if (!(ballots[0] && ballots[0].data && ballots[0].streetId)) {
          res.status(503).json({
            status: 503,
            msg: 'Server found no candidate streets for voting.'
          })
          return
        }

        const streetId = ballots[0].streetId
        if (!streetId) throw new Error('no street ID found for ballots!')
        street = await Street.findOne({ where: { id: streetId } })

        if (!street.creatorId) {
          candidateStreetUrl = `/-/${street.namespacedId}`
        } else {
          candidateStreetUrl = `/${street.creatorId}/${street.namespacedId}`
        }
      } catch (error) {
        logger.error(error)
        res
          .status(500)
          .json({ status: 500, msg: 'Error fetching street from ballot.' })
        return
      }

      // hack to return user to the survey street after signing in
      res.cookie('last_survey_url', candidateStreetUrl)
      return res.redirect(candidateStreetUrl)
    }

    const payload = { ballots }

    return res.status(200).json(payload)
  }
}

export const get = generateRandomBallotFetch({ redirect: false })

export async function put (req, res) {
  const authUser = req.auth || {}
  const { id, comment } = req.body

  if (!authUser.sub) {
    res.status(401).json({ status: 401, msg: 'Please provide user ID.' })
    return
  }

  let user

  try {
    user = await User.findOne({ where: { auth0Id: authUser.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
  }
  const ballot = await Vote.findOne({
    where: { id, voter_id: user.id }
  })

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

  if (comment) {
    if (comment.length > MAX_COMMENT_LENGTH) {
      res
        .status(413)
        .json({ status: 413, msg: 'Ballot must be 280 characters or less' })
      return
    }

    try {
      ballot.comment = comment
      await ballot.save()
    } catch (error) {
      logger.error(error)
      res.status(500).json({ status: 500, msg: 'Error updating ballot.' })
      return
    }
  }

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

export async function post (req, res) {
  const authUser = req.auth || {}

  if (!authUser.sub) {
    res.status(401).json({ status: 401, msg: 'Please provide user ID.' })
    return
  }

  let user

  try {
    user = await User.findOne({ where: { auth0Id: authUser.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
  }

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

  // If requesting user is logged in, create a new vote
  const ballot = {
    id: uuidv4()
  }

  let savedBallot = {}
  let updates = []
  try {
    ballot.data = req.body.data
    ballot.score = req.body.score
    ballot.streetId = req.body.streetId
    ballot.voterId = user.id
    savedBallot = await Vote.create(ballot)

    // update existing ballot
    updates = await Vote.update(
      {
        submitted: Sequelize.fn(
          'array_append',
          Sequelize.col('submitted'),
          user.id
        )
      },
      {
        where: {
          streetId: ballot.streetId,
          voterId: {
            [Sequelize.Op.is]: null
          }
        }
      }
    )
  } catch (error) {
    logger.error(error)
    res.status(500).json({ status: 500, msg: 'Error filling ballot.' })
    return
  }
  const payload = { ballot, savedBallot, updates }

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