2017-08-03 12:57:17 +00:00
|
|
|
import express from 'express'
|
2017-08-23 22:25:52 +00:00
|
|
|
import RateLimit from 'express-rate-limit'
|
2017-08-25 16:42:30 +00:00
|
|
|
import multiparty from 'multiparty'
|
2017-08-03 12:57:17 +00:00
|
|
|
import config from '../../scripts/load-config'
|
|
|
|
import wrap from '../../scripts/asyncRoute'
|
2017-08-25 16:42:30 +00:00
|
|
|
import API from '../api'
|
2017-08-24 10:52:12 +00:00
|
|
|
import News from '../api/news'
|
2017-08-25 16:42:30 +00:00
|
|
|
import Image from '../api/image'
|
|
|
|
import APIExtern from '../api/external'
|
2017-08-03 12:57:17 +00:00
|
|
|
|
|
|
|
let router = express.Router()
|
2017-08-27 11:48:47 +00:00
|
|
|
let dev = process.env.NODE_ENV !== 'production'
|
2017-08-03 12:57:17 +00:00
|
|
|
|
2017-08-27 11:48:47 +00:00
|
|
|
// Restrict API usage
|
2017-08-23 22:25:52 +00:00
|
|
|
let apiLimiter = new RateLimit({
|
|
|
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
|
|
max: 100,
|
|
|
|
delayMs: 0
|
|
|
|
})
|
|
|
|
|
2017-08-27 11:48:47 +00:00
|
|
|
// Restrict image uploads
|
2017-08-25 16:42:30 +00:00
|
|
|
let uploadLimiter = new RateLimit({
|
|
|
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
|
|
max: 10,
|
|
|
|
delayMs: 0
|
|
|
|
})
|
|
|
|
|
2017-08-23 22:25:52 +00:00
|
|
|
router.use(apiLimiter)
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
// Turn things like 'key1[key2]': 'value' into key1: {key2: 'value'} because facebook
|
|
|
|
function objectAssembler (insane) {
|
|
|
|
let object = {}
|
|
|
|
for (let key in insane) {
|
|
|
|
let value = insane[key]
|
|
|
|
if (key.indexOf('[') !== -1) {
|
|
|
|
let subKey = key.match(/^([\w]+)\[(\w+)\]$/)
|
|
|
|
if (subKey[1] && subKey[2]) {
|
|
|
|
if (!object[subKey[1]]) {
|
|
|
|
object[subKey[1]] = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
object[subKey[1]][subKey[2]] = value
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
object[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return object
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a session and return a redirect uri if provided
|
|
|
|
function createSession (req, user) {
|
|
|
|
let uri = '/'
|
|
|
|
req.session.user = {
|
|
|
|
id: user.id,
|
|
|
|
username: user.username,
|
|
|
|
display_name: user.display_name,
|
|
|
|
email: user.email,
|
|
|
|
avatar_file: user.avatar_file,
|
|
|
|
session_refresh: Date.now() + 1800000 // 30 minutes
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.session.redirectUri) {
|
|
|
|
uri = req.session.redirectUri
|
2017-09-06 21:48:51 +00:00
|
|
|
} else if (req.query.redirect) {
|
2017-08-03 12:57:17 +00:00
|
|
|
uri = req.query.redirect
|
|
|
|
}
|
|
|
|
|
|
|
|
return uri
|
|
|
|
}
|
|
|
|
|
|
|
|
// Either give JSON or make a redirect
|
2017-08-24 16:23:03 +00:00
|
|
|
function JsonData (req, res, error, redirect = '/') {
|
2017-08-24 18:36:40 +00:00
|
|
|
res.jsonp({error: error, redirect: redirect})
|
2017-08-03 12:57:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** FACEBOOK LOGIN
|
|
|
|
* Ajax POST only <in-page javascript handeled>
|
|
|
|
* No tokens saved in configs, everything works out-of-the-box
|
|
|
|
*/
|
2017-08-27 11:48:47 +00:00
|
|
|
router.post('/external/facebook/callback', wrap(async (req, res, next) => {
|
|
|
|
if (!config.facebook || !config.facebook.client) return next()
|
2017-08-03 12:57:17 +00:00
|
|
|
let sane = objectAssembler(req.body)
|
|
|
|
sane.ip_address = req.realIP
|
|
|
|
|
|
|
|
let response = await APIExtern.Facebook.callback(req.session.user, sane)
|
|
|
|
|
2017-08-27 17:47:52 +00:00
|
|
|
if (response.banned) {
|
|
|
|
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
|
|
|
|
}
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
if (response.error) {
|
|
|
|
return JsonData(req, res, response.error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create session
|
|
|
|
let uri = '/'
|
|
|
|
if (!req.session.user) {
|
|
|
|
let user = response.user
|
|
|
|
uri = createSession(req, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonData(req, res, null, uri)
|
|
|
|
}))
|
|
|
|
|
2017-08-24 18:36:40 +00:00
|
|
|
router.get('/external/facebook/remove', wrap(async (req, res) => {
|
|
|
|
if (!req.session.user) return res.redirect('/login')
|
|
|
|
let done = await APIExtern.Common.remove(req.session.user, 'fb')
|
|
|
|
|
|
|
|
if (!done) {
|
|
|
|
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
|
|
|
|
}
|
|
|
|
|
|
|
|
res.redirect('/user/manage')
|
|
|
|
}))
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
/** TWITTER LOGIN
|
|
|
|
* OAuth1.0a flows
|
|
|
|
* Tokens in configs
|
|
|
|
*/
|
|
|
|
router.get('/external/twitter/login', wrap(async (req, res) => {
|
|
|
|
if (!config.twitter || !config.twitter.api) return res.redirect('/')
|
|
|
|
let tokens = await APIExtern.Twitter.getRequestToken()
|
|
|
|
|
|
|
|
if (tokens.error) {
|
|
|
|
return res.jsonp({error: tokens.error})
|
|
|
|
}
|
|
|
|
|
|
|
|
req.session.twitter_auth = tokens
|
|
|
|
if (req.query.returnTo) {
|
|
|
|
req.session.twitter_auth.returnTo = req.query.returnTo
|
|
|
|
}
|
|
|
|
|
|
|
|
res.redirect('https://twitter.com/oauth/authenticate?oauth_token=' + tokens.token)
|
|
|
|
}))
|
|
|
|
|
|
|
|
router.get('/external/twitter/callback', wrap(async (req, res) => {
|
|
|
|
if (!config.twitter || !config.twitter.api) return res.redirect('/login')
|
|
|
|
if (!req.session.twitter_auth) return res.redirect('/login')
|
|
|
|
let ta = req.session.twitter_auth
|
|
|
|
let uri = ta.returnTo || '/login'
|
|
|
|
|
|
|
|
if (!req.query.oauth_verifier) {
|
|
|
|
req.flash('message', {error: true, text: 'Couldn\'t get a verifier'})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
let accessTokens = await APIExtern.Twitter.getAccessTokens(ta.token, ta.token_secret, req.query.oauth_verifier)
|
|
|
|
delete req.session.twitter_auth
|
|
|
|
|
|
|
|
if (accessTokens.error) {
|
|
|
|
req.flash('message', {error: true, text: 'Couldn\'t get an access token'})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = await APIExtern.Twitter.callback(req.session.user, accessTokens, req.realIP)
|
2017-08-27 17:47:52 +00:00
|
|
|
if (response.banned) {
|
|
|
|
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
|
|
|
|
}
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
if (response.error) {
|
|
|
|
req.flash('message', {error: true, text: response.error})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!req.session.user) {
|
|
|
|
let user = response.user
|
|
|
|
uri = createSession(req, user)
|
|
|
|
}
|
|
|
|
|
2017-08-25 18:09:04 +00:00
|
|
|
res.render('redirect', {url: uri})
|
2017-08-03 12:57:17 +00:00
|
|
|
}))
|
|
|
|
|
2017-08-24 18:36:40 +00:00
|
|
|
router.get('/external/twitter/remove', wrap(async (req, res) => {
|
|
|
|
if (!req.session.user) return res.redirect('/login')
|
|
|
|
|
|
|
|
let done = await APIExtern.Common.remove(req.session.user, 'twitter')
|
|
|
|
|
|
|
|
if (!done) {
|
|
|
|
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
|
|
|
|
}
|
|
|
|
|
|
|
|
res.redirect('/user/manage')
|
|
|
|
}))
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
/** DISCORD LOGIN
|
|
|
|
* OAuth2 flows
|
|
|
|
* Tokens in configs
|
|
|
|
*/
|
|
|
|
router.get('/external/discord/login', wrap(async (req, res) => {
|
|
|
|
if (!config.discord || !config.discord.api) return res.redirect('/')
|
2017-08-24 16:23:03 +00:00
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
let infos = APIExtern.Discord.getAuthorizeURL()
|
|
|
|
|
|
|
|
req.session.discord_auth = {
|
|
|
|
returnTo: req.query.returnTo || '/login',
|
|
|
|
state: infos.state
|
|
|
|
}
|
|
|
|
|
|
|
|
res.redirect(infos.url)
|
|
|
|
}))
|
|
|
|
|
|
|
|
router.get('/external/discord/callback', wrap(async (req, res) => {
|
|
|
|
if (!config.discord || !config.discord.api) return res.redirect('/login')
|
|
|
|
if (!req.session.discord_auth) return res.redirect('/login')
|
|
|
|
|
|
|
|
let code = req.query.code
|
|
|
|
let state = req.query.state
|
|
|
|
let da = req.session.discord_auth
|
|
|
|
let uri = da.returnTo || '/login'
|
|
|
|
|
|
|
|
if (!code) {
|
|
|
|
req.flash('message', {error: true, text: 'No authorization.'})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!state || state !== da.state) {
|
|
|
|
req.flash('message', {error: true, text: 'Request got intercepted, try again.'})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
delete req.session.discord_auth
|
|
|
|
|
|
|
|
let accessToken = await APIExtern.Discord.getAccessToken(code)
|
|
|
|
if (accessToken.error) {
|
|
|
|
req.flash('message', {error: true, text: accessToken.error})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = await APIExtern.Discord.callback(req.session.user, accessToken.accessToken, req.realIP)
|
2017-08-27 17:47:52 +00:00
|
|
|
if (response.banned) {
|
|
|
|
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
|
|
|
|
}
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
if (response.error) {
|
|
|
|
req.flash('message', {error: true, text: response.error})
|
|
|
|
return res.redirect(uri)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!req.session.user) {
|
|
|
|
let user = response.user
|
|
|
|
uri = createSession(req, user)
|
|
|
|
}
|
|
|
|
|
2017-08-25 18:09:04 +00:00
|
|
|
res.render('redirect', {url: uri})
|
2017-08-03 12:57:17 +00:00
|
|
|
}))
|
|
|
|
|
2017-08-24 18:36:40 +00:00
|
|
|
router.get('/external/discord/remove', wrap(async (req, res) => {
|
|
|
|
if (!req.session.user) return res.redirect('/login')
|
|
|
|
|
|
|
|
let done = await APIExtern.Common.remove(req.session.user, 'discord')
|
|
|
|
|
|
|
|
if (!done) {
|
|
|
|
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
|
|
|
|
}
|
|
|
|
|
|
|
|
res.redirect('/user/manage')
|
|
|
|
}))
|
|
|
|
|
2017-08-24 10:52:12 +00:00
|
|
|
/* ========
|
|
|
|
* NEWS
|
|
|
|
* ========
|
|
|
|
*/
|
|
|
|
|
2017-08-27 11:48:47 +00:00
|
|
|
// Cache news for one day
|
|
|
|
router.get('/news', (req, res, next) => {
|
|
|
|
if (!dev) res.header('Cache-Control', 'max-age=' + 24 * 60 * 60 * 1000) // 1 day
|
|
|
|
next()
|
|
|
|
})
|
|
|
|
|
|
|
|
// Get a page of articles
|
2017-08-24 10:52:12 +00:00
|
|
|
router.get('/news/all/:page', wrap(async (req, res) => {
|
|
|
|
if (!req.params.page || isNaN(parseInt(req.params.page))) {
|
|
|
|
return res.status(400).jsonp({error: 'Invalid page number.'})
|
|
|
|
}
|
|
|
|
|
|
|
|
let page = parseInt(req.params.page)
|
|
|
|
let articles = await News.listNews(page)
|
|
|
|
|
|
|
|
res.jsonp(articles)
|
|
|
|
}))
|
|
|
|
|
|
|
|
// Redirect to page one
|
|
|
|
router.get('/news/all/', (req, res) => {
|
|
|
|
res.redirect('/api/news/all/1')
|
|
|
|
})
|
|
|
|
|
2017-08-29 12:00:36 +00:00
|
|
|
router.post('/news/edit/:id', wrap(async (req, res, next) => {
|
2017-08-30 12:23:45 +00:00
|
|
|
let id = parseInt(req.params.id)
|
|
|
|
|
2017-08-29 12:00:36 +00:00
|
|
|
if (!req.session.user || req.session.user.privilege < 1) return next()
|
2017-08-30 12:23:45 +00:00
|
|
|
|
|
|
|
if (!id || isNaN(id)) {
|
2017-08-29 12:00:36 +00:00
|
|
|
return res.status(400).jsonp({error: 'Invalid ID number.'})
|
|
|
|
}
|
|
|
|
|
2017-08-30 12:23:45 +00:00
|
|
|
if (!req.body.content) {
|
|
|
|
return res.status(400).jsonp({error: 'Content is required.'})
|
|
|
|
}
|
|
|
|
|
2017-08-29 12:00:36 +00:00
|
|
|
let result = await News.edit(id, req.body)
|
|
|
|
if (result.error) {
|
|
|
|
return res.status(400).jsonp({error: result.error})
|
|
|
|
}
|
|
|
|
|
|
|
|
res.status(204).end()
|
|
|
|
}))
|
|
|
|
|
2017-08-24 10:52:12 +00:00
|
|
|
// Fetch article
|
|
|
|
router.get('/news/:id', wrap(async (req, res) => {
|
|
|
|
if (!req.params.id || isNaN(parseInt(req.params.id))) {
|
|
|
|
return res.status(400).jsonp({error: 'Invalid ID number.'})
|
|
|
|
}
|
|
|
|
|
|
|
|
let id = parseInt(req.params.id)
|
|
|
|
let article = await News.article(id)
|
2017-08-27 11:48:47 +00:00
|
|
|
|
2017-08-24 10:52:12 +00:00
|
|
|
res.jsonp(article)
|
|
|
|
}))
|
|
|
|
|
|
|
|
// Preview endpoint
|
|
|
|
router.get('/news', wrap(async (req, res) => {
|
|
|
|
let articles = await News.preview()
|
|
|
|
|
|
|
|
res.jsonp(articles)
|
|
|
|
}))
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
/* ==========
|
|
|
|
* AVATAR
|
|
|
|
* ==========
|
|
|
|
*/
|
|
|
|
// Promisify multiparty form parser
|
2017-08-25 16:42:30 +00:00
|
|
|
async function promiseForm (req) {
|
|
|
|
let form = new multiparty.Form()
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
form.parse(req, async (err, fields, files) => {
|
|
|
|
if (err) return reject(err)
|
|
|
|
resolve({fields: fields, files: files})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
// Upload avatar image
|
2017-08-25 16:42:30 +00:00
|
|
|
router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => {
|
|
|
|
if (!req.session.user) return next()
|
|
|
|
let data = await promiseForm(req)
|
|
|
|
let result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
return res.status(400).jsonp({error: result.error})
|
|
|
|
}
|
|
|
|
|
|
|
|
let avatarUpdate = await API.User.changeAvatar(req.session.user, result.file)
|
|
|
|
if (avatarUpdate.error) {
|
|
|
|
return res.status(400).jsonp({error: avatarUpdate.error})
|
|
|
|
}
|
|
|
|
|
|
|
|
if (avatarUpdate.file) {
|
|
|
|
req.session.user.avatar_file = avatarUpdate.file
|
|
|
|
}
|
|
|
|
|
|
|
|
res.status(200).jsonp({})
|
|
|
|
}))
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
// Remove avatar image
|
2017-08-25 16:42:30 +00:00
|
|
|
router.post('/avatar/remove', wrap(async (req, res, next) => {
|
|
|
|
if (!req.session.user) return next()
|
|
|
|
|
|
|
|
await API.User.removeAvatar(req.session.user)
|
|
|
|
req.session.user.avatar_file = null
|
|
|
|
|
|
|
|
res.status(200).jsonp({done: true})
|
|
|
|
}))
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
// Get latest avatar of logged in user
|
2017-08-25 18:54:03 +00:00
|
|
|
router.get('/avatar', wrap(async (req, res, next) => {
|
|
|
|
if (!req.session.user) return next()
|
|
|
|
let user = req.session.user
|
|
|
|
|
|
|
|
if (!user.avatar_file) return next()
|
|
|
|
|
|
|
|
res.header('Cache-Control', 'max-age=' + 7 * 24 * 60 * 60 * 1000) // 1 week
|
|
|
|
res.redirect('/usercontent/images/' + user.avatar_file)
|
|
|
|
}))
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
// Get latest avatar of user by id
|
2017-08-25 18:54:03 +00:00
|
|
|
router.get('/avatar/:id', wrap(async (req, res, next) => {
|
|
|
|
let id = parseInt(req.params.id)
|
|
|
|
if (isNaN(id)) return next()
|
|
|
|
|
|
|
|
let user = await API.User.get(id)
|
|
|
|
|
|
|
|
if (!user || !user.avatar_file) return next()
|
|
|
|
|
|
|
|
res.header('Cache-Control', 'max-age=' + 7 * 24 * 60 * 60 * 1000) // 1 week
|
|
|
|
res.redirect('/usercontent/images/' + user.avatar_file)
|
|
|
|
}))
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
// Redirect to no avatar on 404
|
2017-08-25 18:54:03 +00:00
|
|
|
router.use('/avatar', (req, res) => {
|
|
|
|
res.redirect('/static/image/avatar.png')
|
|
|
|
})
|
|
|
|
|
2017-08-26 09:47:37 +00:00
|
|
|
/* =====================
|
|
|
|
* OAuth2 Management
|
|
|
|
* =====================
|
|
|
|
*/
|
|
|
|
|
2017-08-27 11:48:47 +00:00
|
|
|
// List authorizations
|
2017-08-26 09:47:37 +00:00
|
|
|
router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
|
|
|
|
if (!req.session.user) return next()
|
|
|
|
|
|
|
|
let list = await API.User.OAuth2.getUserAuthorizations(req.session.user)
|
|
|
|
if (!list) return next()
|
|
|
|
|
|
|
|
res.jsonp(list)
|
|
|
|
}))
|
|
|
|
|
2017-08-27 11:48:47 +00:00
|
|
|
// Revoke an authorization
|
|
|
|
router.post('/oauth2/authorized-clients/revoke', wrap(async (req, res, next) => {
|
2017-08-26 09:47:37 +00:00
|
|
|
if (!req.session.user) return next()
|
|
|
|
|
|
|
|
let clientId = parseInt(req.body.client_id)
|
|
|
|
if (isNaN(clientId)) return res.status(400).jsonp({error: 'Missing Client ID parameter'})
|
|
|
|
|
|
|
|
let done = await API.User.OAuth2.removeUserAuthorization(req.session.user, clientId)
|
|
|
|
if (!done) return res.status(400).jsonp({error: 'Failed to remove client authorization'})
|
|
|
|
|
|
|
|
res.status(204).end()
|
|
|
|
}))
|
|
|
|
|
2017-08-27 17:47:52 +00:00
|
|
|
/* ==================
|
|
|
|
* Donation Store
|
|
|
|
* ==================
|
|
|
|
*/
|
|
|
|
|
|
|
|
router.post('/paypal/ipn', wrap(async (req, res) => {
|
|
|
|
let content = req.body
|
|
|
|
|
|
|
|
if (content && content.payment_status && content.payment_status === 'Completed') {
|
|
|
|
await API.Payment.handleIPN(content)
|
|
|
|
}
|
|
|
|
|
|
|
|
res.status(204).end()
|
|
|
|
}))
|
|
|
|
|
2017-08-31 17:24:38 +00:00
|
|
|
router.get('/donations/user', wrap(async (req, res, next) => {
|
2017-08-27 17:47:52 +00:00
|
|
|
if (!req.session.user) return next()
|
|
|
|
|
|
|
|
let contribs = await API.Payment.userContributions(req.session.user)
|
|
|
|
res.jsonp(contribs)
|
|
|
|
}))
|
|
|
|
|
2017-08-31 17:24:38 +00:00
|
|
|
router.get('/donations', wrap(async (req, res, next) => {
|
|
|
|
let count = parseInt(req.query.count)
|
|
|
|
if (isNaN(count)) {
|
|
|
|
count = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
if (count > 10) {
|
|
|
|
count = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
let mcu = req.query.mcu === '1' || req.query.mcu === 'true'
|
|
|
|
let timeFrame = parseInt(req.query.timeFrame)
|
|
|
|
if (isNaN(timeFrame)) timeFrame = 0
|
|
|
|
|
|
|
|
let contribs = await API.Payment.allContributions(count, mcu, timeFrame)
|
|
|
|
res.jsonp(contribs)
|
|
|
|
}))
|
|
|
|
|
2017-08-24 18:36:40 +00:00
|
|
|
// 404
|
|
|
|
router.use((req, res) => {
|
|
|
|
res.status(404).jsonp({error: 'Not found'})
|
|
|
|
})
|
|
|
|
|
2017-08-29 12:00:36 +00:00
|
|
|
router.use((err, req, res, next) => {
|
|
|
|
console.error(err)
|
|
|
|
res.jsonp({error: 'Internal server error.'})
|
|
|
|
})
|
|
|
|
|
2017-08-03 12:57:17 +00:00
|
|
|
module.exports = router
|