laundree/laundree

View on GitHub
src/api/helper.js

Summary

Maintainability
C
7 hrs
Test Coverage
// @flow
import { StatusError, logError } from '../utils/error'
import type { Response, Request, ParsedParams } from './types'
import { parseParams } from './types'
import UserHandler from '../handlers/user'
import LaundryHandler from '../handlers/laundry'
import TokenHandler from '../handlers/token'
import InviteHandler from '../handlers/laundry_invitation'
import BookingHandler from '../handlers/booking'
import MachineHandler from '../handlers/machine'
import type { ApiResult, Summary } from 'laundree-sdk/lib/sdk'

/**
 * Return success
 * @param res
 * @param {(Promise|Object)=} result
 * @returns {number|*}
 */
async function returnSuccess<X> (res: Response, result: X): Promise<*> {
  res.status(result ? 200 : 204)
  if (!result) return res.end()
  res.json(result)
}

type Subjects = {
  user: ?UserHandler,
  laundry: ?LaundryHandler,
  token: ?TokenHandler,
  invite: ?InviteHandler,
  booking: ?BookingHandler,
  machine: ?MachineHandler,
  currentUser: ?UserHandler
}

type Handler = UserHandler | LaundryHandler | TokenHandler | MachineHandler | BookingHandler | InviteHandler

async function pullSubject<H: Handler> (id: string, _Handler: Class<H>): Promise<H> {
  const instance = await _Handler.lib.findFromId(id)
  if (!instance) throw new StatusError('Not found', 404)
  return instance
}

async function pullSubjects (params: ParsedParams, req: Request): Promise<Subjects> {
  const currentUserId = req.userId || null
  const currentUser = await (currentUserId && UserHandler.lib.findFromId(currentUserId))
  const [user, machine, token, invite, laundry, booking] = await Promise.all([
    params.userId ? pullSubject(params.userId, UserHandler) : null,
    params.machineId ? pullSubject(params.machineId, MachineHandler) : null,
    params.tokenId ? pullSubject(params.tokenId, TokenHandler) : null,
    params.inviteId ? pullSubject(params.inviteId, InviteHandler) : null,
    params.laundryId ? pullSubject(params.laundryId, LaundryHandler) : null,
    params.bookingId ? pullSubject(params.bookingId, BookingHandler) : null
  ])
  const fetcherCandidate = machine || invite || booking
  const newLaundry = (laundry || !fetcherCandidate ? laundry : await fetcherCandidate.fetchLaundry())
  return {user, machine, token, invite, laundry: newLaundry, booking, currentUser}
}

function testUserAccess (subjects: Subjects): UserHandler {
  if (!subjects.currentUser) {
    throw new StatusError('Invalid credentials', 403)
  }
  return subjects.currentUser
}

export function securityUserAccess (subjects: Subjects): void {
  testUserAccess(subjects)
}

export function securityWebApplication (subjects: Subjects, req: Request) {
  if (req.subject === 'app') return
  throw new StatusError('Not allowed', 403)
}

export function securitySelf (subjects: Subjects): void {
  const currentUser = testUserAccess(subjects)
  if (!subjects.user || currentUser.model.id !== subjects.user.model.id) {
    throw new StatusError('Not allowed', 403)
  }
}

export function securityTokenOwner (subjects: Subjects): void {
  const currentUser = testUserAccess(subjects)
  if (subjects.token && subjects.token.isOwner(currentUser)) {
    return
  }
  throw new StatusError('Not allowed', 403)
}

export function securityLaundryOwner (subjects: Subjects, req: Request): void {
  const currentUser = testUserAccess(subjects)
  if (subjects.laundry && subjects.laundry.isOwner(currentUser)) {
    return
  }
  throw new StatusError('Not allowed', 403)
}

export function securityLaundryUser (subjects: Subjects, req: Request): void {
  const currentUser = testUserAccess(subjects)
  if (subjects.laundry && subjects.laundry.isUser(currentUser)) {
    return
  }
  throw new StatusError('Not allowed', 403)
}

export function securityBookingCreator (subjects: Subjects, req: Request): void {
  const currentUser = testUserAccess(subjects)
  if (subjects.booking && subjects.booking.isOwner(currentUser)) {
    return
  }
  throw new StatusError('Not allowed', 403)
}

export function securityAdministrator (subjects: Subjects, req: Request): void {
  const currentUser = testUserAccess(subjects)
  if (!currentUser.isAdmin()) {
    throw new StatusError('Not allowed', 403)
  }
}

export function securityNoop (subjects: Subjects, req: Request): void {
}

type Security = (s: Subjects, r: Request) => void

function buildSecurityFunction (securities: Security[]): (params: ParsedParams, req: Request) => Promise<Subjects> {
  return async (params: ParsedParams, req: Request) => {
    const subjects: Subjects = await pullSubjects(params, req)
    let firstError
    for (const security of securities) {
      try {
        security(subjects, req)
        return subjects
      } catch (err) {
        if (firstError) continue
        firstError = err
      }
    }
    throw firstError || new Error('No security defined!')
  }
}

type Middleware = (subjects: Subjects, p: ParsedParams, req: Request, res: Response) => Promise<?ApiResult>

export function handleError (res: Response, err: Error) {
  const status = (typeof err.status === 'number' && err.status) ||
    (res.statusCode >= 400 && res.statusCode) ||
    500
  res.status(status)
  const headers: Object = (typeof err.headers === 'object') && err.headers ? err.headers : {}
  Object.keys(headers).forEach(key => res.set(key, headers[key]))
  if (status !== 500) {
    res.json({message: err.message})
    return
  }
  logError(err)
  res.json({message: 'Internal server error'})
}

export function wrap (func: Middleware, security: Security, ...securities: Security[]): (req: Request, res: Response) => * {
  const securityFunction = buildSecurityFunction([security].concat(securities))
  return (req: Request, res: Response) => {
    const params: ParsedParams = parseParams(req.swagger.params)
    securityFunction(params, req)
      .then((subjects) => func(subjects, params, req, res))
      .then(result => returnSuccess(res, result))
      .catch(err => handleError(res, err))
  }
}

type PaginateFunction = (since: ?string, pageSize: number, subjects: Subjects, p: ParsedParams, req: Request, res: Response) => Promise<{ summaries: Summary[], linkBase: string }>

function buildQs (vars) {
  const qs = Object.keys(vars).reduce((acc, k) => vars[k] === undefined ? acc : `&${acc}=${encodeURIComponent(vars[k])}`, '')
  return qs && qs.substr(1)
}

export function paginate (p: PaginateFunction): Middleware {
  return async (subjects: Subjects, params: ParsedParams, req, res) => {
    const {pageSize} = assertSubjects({pageSize: params.page_size})
    const {summaries, linkBase} = await p(params.since || null, pageSize, subjects, params, req, res)
    const links: { first: string, next?: string } = {
      first: `${linkBase}?${buildQs({...req.query, since: undefined, page_size: pageSize})}`
    }
    if (summaries.length) {
      links.next = `${linkBase}?${buildQs({...req.query, since: summaries[summaries.length - 1], page_size: pageSize})}`
    }
    res.links(links)
    return summaries
  }
}

export function assert<V> (v: ?V): V {
  if (!v) throw new Error('Failed assertion')
  return v
}

type Exporter = <A>(?A) => A

export function assertSubjects<O: {}> (o: O): $ObjMap<O, Exporter> {
  return Object.keys(o).reduce((acc, key) => {
    const a = o[key]
    if (a === null) {
      throw new Error(`Failed assertion. ${key} not available`)
    }
    return {...acc, [key]: a}
  }, {})
}