This repository has been archived on 2022-11-26. You can view files and clone it, but cannot push or open issues or pull requests.
IcyNet.eu/server/routes/api.js

531 lines
14 KiB
JavaScript
Raw Normal View History

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'
import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute'
2017-08-25 16:42:30 +00:00
import APIExtern from '../api/external'
import Image from '../api/image'
import News from '../api/news'
import API from '../api'
let router = express.Router()
2017-08-27 11:48:47 +00:00
let dev = process.env.NODE_ENV !== 'production'
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)
// 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) {
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
}
}
2017-11-18 09:01:14 +00:00
// Get either `uuid` or `id` from `:id` parameter
function idParam (req) {
let id = req.params.id
if (id.length === 36 && id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) {
return id
}
if (!isNaN(parseInt(id))) {
return parseInt(id)
}
return null
}
// Either give JSON or make a redirect
2017-08-24 16:23:03 +00:00
function JsonData (req, res, error, redirect = '/') {
res.jsonp({error: error, redirect: redirect})
}
// Common middleware for all external account unlinks
function removeAuthMiddleware (identifier) {
return wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, identifier)
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
})
}
/** 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()
// Fix up the retarded object Facebook sends us
let sane = objectAssembler(req.body)
if (!sane || !sane.authResponse) {
return next()
}
let response = await APIExtern.Facebook.callback(req.session.user, sane.authResponse, req.realIP)
if (response.banned) {
2017-10-14 20:40:12 +00:00
return JsonData(req, res, 'You are banned.')
}
if (response.error) {
return JsonData(req, res, response.error)
}
// Create session
if (!req.session.user) {
let user = response.user
2017-10-09 14:38:27 +00:00
createSession(req, user)
}
2017-10-09 14:38:27 +00:00
JsonData(req, res, null, '/login')
}))
router.get('/external/facebook/remove', removeAuthMiddleware('facebook'))
/** 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
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
2017-10-09 14:38:27 +00:00
let uri = '/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)
if (response.banned) {
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
}
if (response.error) {
req.flash('message', {error: true, text: response.error})
return res.redirect(uri)
}
if (!req.session.user) {
let user = response.user
2017-10-09 14:38:27 +00:00
createSession(req, user)
}
res.render('redirect', {url: uri})
}))
router.get('/external/twitter/remove', removeAuthMiddleware('twitter'))
/** 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
let infos = APIExtern.Discord.getAuthorizeURL(req)
res.redirect(infos.url)
}))
router.get('/external/discord/callback', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/login')
let code = req.query.code
let state = req.query.state
2017-10-09 14:38:27 +00:00
let uri = '/login'
if (!code) {
req.flash('message', {error: true, text: 'No authorization.'})
return res.redirect(uri)
}
if (!state || state !== APIExtern.Common.stateGenerator(req)) {
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)
if (response.banned) {
return res.render('user/banned', {bans: response.banned, ipban: response.ip})
}
if (response.error) {
req.flash('message', {error: true, text: response.error})
return res.redirect(uri)
}
if (!req.session.user) {
let user = response.user
2017-10-09 14:38:27 +00:00
createSession(req, user)
}
res.render('redirect', {url: uri})
}))
router.get('/external/discord/remove', removeAuthMiddleware('discord'))
2017-10-13 16:18:17 +00:00
/** GOOGLE LOGIN
* Google Token Verification
* Tokens in configs
*/
router.get('/external/google/login', wrap(async (req, res) => {
if (!config.google || !config.google.api) return res.redirect('/')
res.redirect('/login')
}))
router.post('/external/google/callback', wrap(async (req, res) => {
if (!config.google || !config.google.api) return res.redirect('/login')
if (!req.body.id_token) {
return JsonData(req, res, 'Invalid or missing ID token!', '/login')
}
let response = await APIExtern.Google.callback(req.session.user, req.body, req.realIP)
if (response.banned) {
2017-10-14 20:40:12 +00:00
return JsonData(req, res, 'You are banned.', '/login')
2017-10-13 16:18:17 +00:00
}
if (response.error) {
return JsonData(req, res, response.error, '/login')
}
if (!req.session.user) {
let user = response.user
createSession(req, user)
}
JsonData(req, res, null, '/login')
}))
router.get('/external/google/remove', removeAuthMiddleware('google'))
2017-10-13 16:18:17 +00:00
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.'})
}
try {
await News.edit(id, req.body)
} catch (e) {
return res.status(400).jsonp({error: e.message})
2017-08-29 12:00:36 +00:00
}
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)
}))
/* ==========
* 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})
})
})
}
// 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 avatarFile
try {
let result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
2017-08-25 16:42:30 +00:00
avatarFile = await API.User.changeAvatar(req.session.user, result.file)
} catch (e) {
return res.status(400).jsonp({error: e.message})
2017-08-25 16:42:30 +00:00
}
if (avatarFile) {
req.session.user.avatar_file = avatarFile
2017-08-25 16:42:30 +00:00
}
2018-02-04 16:25:45 +00:00
req.flash('message', {error: false, text: 'Success!'})
2017-08-25 16:42:30 +00:00
res.status(200).jsonp({})
}))
// 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})
}))
// 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)
}))
// Get latest avatar of user by id
2017-08-25 18:54:03 +00:00
router.get('/avatar/:id', wrap(async (req, res, next) => {
2017-11-18 09:01:14 +00:00
let id = idParam(req)
if (!id) return next()
2017-08-25 18:54:03 +00:00
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)
}))
2018-02-04 16:25:45 +00:00
router.get('/avatar/gravatar', (req, res, next) => {
if (!req.session.user) return next()
let email = req.session.user.email
res.set('Content-Type', 'text/plain')
res.end(Image.gravatarURL(email))
})
router.post('/avatar/gravatar', wrap(async (req, res, next) => {
if (!req.session.user) return next()
let user = req.session.user
try {
let gravURL = await Image.downloadImage(Image.gravatarURL(user.email), 'GRAV-' + user.username)
let file = await API.User.changeAvatar(user, gravURL)
req.session.user.avatar_file = file
req.flash('message', {error: false, text: 'Success!'})
} catch (e) {
req.flash('message', {error: true, text: 'Failed to use gravatar avatar.'})
}
res.jsonp({})
}))
// 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')
})
/* =====================
* OAuth2 Management
* =====================
*/
2017-08-27 11:48:47 +00:00
// List authorizations
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) => {
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()
}))
/* ==================
* 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()
}))
router.get('/donations/user', wrap(async (req, res, next) => {
if (!req.session.user) return next()
let contribs = await API.Payment.userContributions(req.session.user)
res.jsonp(contribs)
}))
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)
}))
// 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.'})
})
module.exports = router