Major refactor and code modernization, vol. 1

This commit is contained in:
Evert Prants 2020-12-13 16:36:07 +02:00
parent 229a7db04a
commit 444f8b00f8
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
37 changed files with 4441 additions and 6730 deletions

7378
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,59 +30,60 @@
}, },
"homepage": "https://icynet.eu", "homepage": "https://icynet.eu",
"dependencies": { "dependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.10.4", "@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/register": "^7.10.4", "@babel/register": "^7.12.10",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"connect-redis": "^4.0.4", "connect-redis": "^5.0.0",
"connect-session-knex": "^1.7.3", "connect-session-knex": "^2.0.0",
"email-templates": "^7.0.5", "email-templates": "^8.0.2",
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.2.3",
"express-session": "^1.17.1", "express-session": "^1.17.1",
"feed": "^4.2.1", "feed": "^4.2.1",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"gm": "^1.23.1", "gm": "^1.23.1",
"knex": "^0.21.1", "knex": "^0.21.13",
"multiparty": "^4.2.1", "multiparty": "^4.2.2",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.17",
"notp": "^2.0.3", "notp": "^2.0.3",
"oauth-libre": "^0.9.17", "oauth-libre": "^0.9.17",
"objection": "^2.2.1", "objection": "^2.2.3",
"querystring-es3": "^0.2.1",
"redis": "^3.0.2", "redis": "^3.0.2",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"stylus": "^0.54.7", "stylus": "^0.54.8",
"thirty-two": "^1.0.2", "thirty-two": "^1.0.2",
"toml": "^3.0.0", "toml": "^3.0.0",
"uuid": "^8.2.0", "uuid": "^8.3.2",
"vue": "^2.6.11" "vue": "^2.6.12"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.4", "@babel/core": "^7.12.10",
"@babel/preset-env": "^7.10.4", "@babel/preset-env": "^7.12.10",
"babel-loader": "^8.1.0", "babel-loader": "^8.2.2",
"bootstrap": "^4.5.0", "bootstrap": "^4.5.3",
"concurrently": "^5.2.0", "concurrently": "^5.3.0",
"eslint-plugin-import": "^2.22.0", "eslint-plugin-import": "^2.22.1",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mustache": "^4.0.1", "mustache": "^4.1.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"pug": "^3.0.0", "pug": "^3.0.0",
"pug-plain-loader": "^1.0.0", "pug-plain-loader": "^1.1.0",
"standard": "^14.3.4", "standard": "^16.0.3",
"terser-webpack-plugin": "^3.0.6", "terser-webpack-plugin": "^5.0.3",
"vue-clickaway": "^2.2.2", "vue-clickaway": "^2.2.2",
"vue-loader": "^15.9.3", "vue-loader": "^15.9.5",
"vue-resource": "^1.5.1", "vue-resource": "^1.5.1",
"vue-router": "^3.3.4", "vue-router": "^3.4.9",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.12",
"watch": "^1.0.2", "watch": "^1.0.2",
"webpack": "^4.43.0", "webpack": "^5.10.1",
"webpack-cli": "^3.3.12", "webpack-cli": "^4.2.0",
"webpack-merge": "^5.0.7" "webpack-merge": "^5.7.0"
}, },
"standard": { "standard": {
"env": { "env": {

View File

@ -1 +1 @@
module.exports = fn => (...args) => fn(...args).catch(args[2]) export default fn => (...args) => fn(...args).catch(args[2])

View File

@ -1,9 +1,7 @@
// Make sure the user is logged in // Make sure the user is logged in
// Redirect to login page and store the current path in the session for redirecting later // Redirect to login page and store the current path in the session for redirecting later
function ensureLogin (req, res, next) { export function ensureLogin (req, res, next) {
if (req.session.user) return next() if (req.session.user) return next()
req.session.redirectUri = req.originalUrl req.session.redirectUri = req.originalUrl
res.redirect('/login') res.redirect('/login')
} }
module.exports = ensureLogin

View File

@ -6,7 +6,7 @@ const format = util.format
* Included here to avoid includng ridiculously small modules * Included here to avoid includng ridiculously small modules
*/ */
module.exports = function (options) { export default function (options) {
options = options || {} options = options || {}
const safe = (options.unsafe === undefined) ? true : !options.unsafe const safe = (options.unsafe === undefined) ? true : !options.unsafe

View File

@ -2,7 +2,7 @@ import { URL } from 'url'
import qs from 'querystring' import qs from 'querystring'
import fs from 'fs' import fs from 'fs'
function HTTP_GET (link, headers = {}, lback) { export function httpGET (link, headers = {}, lback) {
if (lback && lback >= 4) throw new Error('infinite loop!') // Prevent infinite loop requests if (lback && lback >= 4) throw new Error('infinite loop!') // Prevent infinite loop requests
const parsed = new URL(link) const parsed = new URL(link)
const opts = { const opts = {
@ -32,7 +32,7 @@ function HTTP_GET (link, headers = {}, lback) {
lback += 1 lback += 1
} }
return HTTP_GET(res.headers.location, headers, lback).then(resolve, reject) return httpGET(res.headers.location, headers, lback).then(resolve, reject)
} }
let data = '' let data = ''
@ -60,7 +60,7 @@ function HTTP_GET (link, headers = {}, lback) {
}) })
} }
function HTTP_POST (link, headers = {}, data) { export function httpPOST (link, headers = {}, data) {
const parsed = new URL(link) const parsed = new URL(link)
let postData = qs.stringify(data) let postData = qs.stringify(data)
@ -106,7 +106,7 @@ function HTTP_POST (link, headers = {}, data) {
}) })
} }
async function Download (url, dest) { export async function downloadURL (url, dest) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest) const file = fs.createWriteStream(dest)
const protocol = url.indexOf('https:') === 0 ? require('https') : require('http') const protocol = url.indexOf('https:') === 0 ? require('https') : require('http')
@ -121,9 +121,3 @@ async function Download (url, dest) {
}) })
}) })
} }
module.exports = {
GET: HTTP_GET,
POST: HTTP_POST,
Download: Download
}

View File

@ -67,10 +67,8 @@ async function initializeLogger () {
} }
} }
module.exports = { export function initialize () {
initialize: function () {
// Create log file write stream // Create log file write stream
if (!config.logger || !config.logger.write) return if (!config.logger || !config.logger.write) return
initializeLogger().catch((e) => console.error(e)) initializeLogger().catch((e) => console.error(e))
} }
}

View File

@ -1,10 +1,10 @@
import Users from './index' import { User, Pagination, Hash, Login, Reset, Register } from './index'
import Models from './models' import * as Models from './models'
const perPage = 6 const perPage = 6
async function cleanUserObject (dbe, admin) { async function cleanUserObject (dbe, admin) {
const totp = await Users.User.Login.totpTokenRequired(dbe) const totp = await Login.totpTokenRequired(dbe)
return { return {
id: dbe.id, id: dbe.id,
@ -25,7 +25,7 @@ async function cleanUserObject (dbe, admin) {
} }
async function cleanClientObject (dbe) { async function cleanClientObject (dbe) {
const user = await Users.User.get(dbe.user_id) const user = await User.get(dbe.user_id)
return { return {
id: dbe.id, id: dbe.id,
title: dbe.title, title: dbe.title,
@ -46,8 +46,8 @@ async function cleanClientObject (dbe) {
} }
async function cleanBanObject (dbe) { async function cleanBanObject (dbe) {
const user = await Users.User.get(dbe.user_id) const user = await User.get(dbe.user_id)
const admin = await Users.User.get(dbe.admin_id) const admin = await User.get(dbe.admin_id)
return { return {
id: dbe.id, id: dbe.id,
reason: dbe.reason, reason: dbe.reason,
@ -82,18 +82,17 @@ function dataFilter (data, fields, optional = []) {
return data return data
} }
const API = {
// List all users (paginated) // List all users (paginated)
getAllUsers: async function (page, adminId) { export async function getAllUsers (page, adminId) {
let count = await Models.User.query().count('id as ids') let count = await Models.User.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) { if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No users found in database' } return { error: 'No users found in database' }
} }
count = count[0].ids count = count[0].ids
const paginated = Users.Pagination(perPage, parseInt(count), page) const paginated = Pagination(perPage, parseInt(count), page)
const raw = await Models.User.query().offset(paginated.offset).limit(perPage) const raw = await Models.User.query().offset(paginated.offset).limit(perPage)
const admin = await Users.User.get(adminId) const admin = await User.get(adminId)
const users = [] const users = []
for (const i in raw) { for (const i in raw) {
@ -106,15 +105,17 @@ const API = {
page: paginated, page: paginated,
users: users users: users
} }
}, }
getUser: async function (id) {
const user = await Users.User.get(id) export async function getUser (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user') if (!user) throw new Error('No such user')
return cleanUserObject(user, null) return cleanUserObject(user, null)
}, }
editUser: async function (id, data) {
const user = await Users.User.get(id) export async function editUser (id, data) {
const user = await User.get(id)
if (!user) throw new Error('No such user') if (!user) throw new Error('No such user')
const fields = [ const fields = [
@ -124,38 +125,42 @@ const API = {
data = dataFilter(data, fields, ['nw_privilege', 'activated']) data = dataFilter(data, fields, ['nw_privilege', 'activated'])
if (!data) throw new Error('Missing fields') if (!data) throw new Error('Missing fields')
await Users.User.update(user, data) await User.update(user, data)
return {} return {}
}, }
resendActivationEmail: async function (id) {
const user = await Users.User.get(id) export async function resendActivationEmail (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user') if (!user) throw new Error('No such user')
if (user.activated === 1) return {} if (user.activated === 1) return {}
await Users.User.Register.activationEmail(user) await Register.activationEmail(user)
return {} return {}
}, }
revokeTotpToken: async function (id) {
const user = await Users.User.get(id) export async function revokeTotpToken (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user') if (!user) throw new Error('No such user')
await Models.TotpToken.query().delete().where('user_id', user.id) await Models.TotpToken.query().delete().where('user_id', user.id)
return {} return {}
}, }
sendPasswordEmail: async function (id) {
const user = await Users.User.get(id) export async function sendPasswordEmail (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user') if (!user) throw new Error('No such user')
const token = await Users.User.Reset.reset(user.email, false, true) const token = await Reset.reset(user.email, false, true)
return { token } return { token }
}, }
// Search for users by terms and fields // Search for users by terms and fields
searchUsers: async function (terms, fields = ['email']) { export async function searchUsers (terms, fields = ['email']) {
let qb = Models.User.query() let qb = Models.User.query()
terms = terms.replace(/_/g, '\\_').replace(/%/g, '\\%') terms = terms.replace(/_/g, '\\_').replace(/%/g, '\\%')
@ -177,16 +182,17 @@ const API = {
} }
return cleaned return cleaned
}, }
// List all clients (paginated) // List all clients (paginated)
getAllClients: async function (page) { export async function getAllClients (page) {
let count = await Models.OAuth2Client.query().count('id as ids') let count = await Models.OAuth2Client.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) { if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No clients' } return { error: 'No clients' }
} }
count = count[0].ids count = count[0].ids
const paginated = Users.Pagination(perPage, parseInt(count), page) const paginated = Pagination(perPage, parseInt(count), page)
const raw = await Models.OAuth2Client.query().offset(paginated.offset).limit(perPage) const raw = await Models.OAuth2Client.query().offset(paginated.offset).limit(perPage)
const clients = [] const clients = []
@ -199,16 +205,18 @@ const API = {
return { return {
page: paginated, clients page: paginated, clients
} }
}, }
// Get information about a client via id // Get information about a client via id
getClient: async function (id) { export async function getClient (id) {
const raw = await Models.OAuth2Client.query().where('id', id) const raw = await Models.OAuth2Client.query().where('id', id)
if (!raw.length) throw new Error('No such client') if (!raw.length) throw new Error('No such client')
return cleanClientObject(raw[0]) return cleanClientObject(raw[0])
}, }
// Update a client `id` in database with `data` // Update a client `id` in database with `data`
updateClient: async function (id, data) { export async function updateClient (id, data) {
const fields = [ const fields = [
'title', 'description', 'url', 'redirect_url', 'scope', 'verified' 'title', 'description', 'url', 'redirect_url', 'scope', 'verified'
] ]
@ -224,11 +232,12 @@ const API = {
} }
return {} return {}
}, }
// Create a new secret for a client // Create a new secret for a client
newSecret: async function (id) { export async function newSecret (id) {
if (isNaN(id)) throw new Error('Invalid client ID') if (isNaN(id)) throw new Error('Invalid client ID')
const secret = Users.Hash(16) const secret = Hash(16)
try { try {
await Models.OAuth2Client.query().patchAndFetchById(id, { secret: secret }) await Models.OAuth2Client.query().patchAndFetchById(id, { secret: secret })
@ -237,9 +246,10 @@ const API = {
} }
return {} return {}
}, }
// Create a new client // Create a new client
createClient: async function (data, user) { export async function createClient (data, user) {
const fields = [ const fields = [
'title', 'description', 'url', 'redirect_url', 'scope' 'title', 'description', 'url', 'redirect_url', 'scope'
] ]
@ -248,32 +258,34 @@ const API = {
if (!data) throw new Error('Missing fields') if (!data) throw new Error('Missing fields')
const obj = Object.assign({ const obj = Object.assign({
secret: Users.Hash(16), secret: Hash(16),
grants: 'authorization_code', grants: 'authorization_code',
created_at: new Date(), created_at: new Date(),
user_id: user.id user_id: user.id
}, data) }, data)
return Models.OAuth2Client.query().insert(obj) return Models.OAuth2Client.query().insert(obj)
}, }
// Remove a client and all associated data // Remove a client and all associated data
removeClient: async function (id) { export async function removeClient (id) {
if (isNaN(id)) throw new Error('Invalid ID number') if (isNaN(id)) throw new Error('Invalid ID number')
await Models.OAuth2Client.query().delete().where('id', id) await Models.OAuth2Client.query().delete().where('id', id)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
await Models.OAuth2AccessToken.query().delete().where('client_id', id) await Models.OAuth2AccessToken.query().delete().where('client_id', id)
await Models.OAuth2RefreshToken.query().delete().where('client_id', id) await Models.OAuth2RefreshToken.query().delete().where('client_id', id)
return true return true
}, }
// List all bans (paginated) // List all bans (paginated)
getAllBans: async function (page) { export async function getAllBans (page) {
let count = await Models.Ban.query().count('id as ids') let count = await Models.Ban.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) { if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No bans on record' } return { error: 'No bans on record' }
} }
count = count[0].ids count = count[0].ids
const paginated = Users.Pagination(perPage, parseInt(count), page) const paginated = Pagination(perPage, parseInt(count), page)
const raw = await Models.Ban.query().offset(paginated.offset).limit(perPage) const raw = await Models.Ban.query().offset(paginated.offset).limit(perPage)
const bans = [] const bans = []
@ -286,19 +298,21 @@ const API = {
return { return {
page: paginated, bans page: paginated, bans
} }
}, }
// Remove a ban // Remove a ban
removeBan: async function (banId) { export async function removeBan (banId) {
return Models.Ban.query().delete().where('id', banId) return Models.Ban.query().delete().where('id', banId)
}, }
// Create a ban // Create a ban
addBan: async function (data, adminId) { export async function addBan (data, adminId) {
const user = await Users.User.get(parseInt(data.user_id)) const user = await User.get(parseInt(data.user_id))
if (!user) throw new Error('No such user.') if (!user) throw new Error('No such user.')
if (user.id === adminId) throw new Error('Cannot ban yourself!') if (user.id === adminId) throw new Error('Cannot ban yourself!')
const admin = await Users.User.get(adminId) const admin = await User.get(adminId)
if (user.nw_privilege > admin.nw_privilege) throw new Error('Cannot ban user.') if (user.nw_privilege > admin.nw_privilege) throw new Error('Cannot ban user.')
@ -313,14 +327,15 @@ const API = {
await Models.Ban.query().insert(banAdd) await Models.Ban.query().insert(banAdd)
return {} return {}
}, }
lockAccount: async function (userId) {
const user = await Users.User.get(userId) export async function lockAccount (userId) {
const user = await User.get(userId)
if (user.id === 1 || user.nw_privilege > 2) { if (user.id === 1 || user.nw_privilege > 2) {
throw new Error('Cannot lock this user.') throw new Error('Cannot lock this user.')
} }
const lockId = Users.Hash(4) const lockId = Hash(4)
const userObf = { const userObf = {
username: lockId, username: lockId,
display_name: user.username, display_name: user.username,
@ -331,8 +346,5 @@ const API = {
avatar_file: null avatar_file: null
} }
return Users.User.update(user, userObf) return User.update(user, userObf)
} }
}
module.exports = API

View File

@ -16,7 +16,7 @@ const email = new Email({
}) })
// Send an email to `email` with `headers` // Send an email to `email` with `headers`
async function sendMail (address, template, context) { export async function sendMail (address, template, context) {
if (!email.transport) throw new Error('No transporter present!') if (!email.transport) throw new Error('No transporter present!')
return email.send({ return email.send({
@ -29,14 +29,14 @@ async function sendMail (address, template, context) {
} }
// Send an email to `email` using `template` rendered with variables from `context` // Send an email to `email` using `template` rendered with variables from `context`
async function pushMail (template, address, context) { export async function pushMail (template, address, context) {
console.debug('Mail being sent: %s to %s', template, email) console.debug('Mail being sent: %s to %s', template, email)
return sendMail(address, template, context) return sendMail(address, template, context)
} }
// Transporter initialization // Transporter initialization
async function init () { export async function init () {
if (!config.email || config.email.enabled === false) return if (!config.email || config.email.enabled === false) return
const transporter = nodemailer.createTransport(config.email.transport) const transporter = nodemailer.createTransport(config.email.transport)
@ -53,8 +53,3 @@ async function init () {
} }
} }
module.exports = {
sendMail: sendMail,
pushMail: pushMail,
init: init
}

View File

@ -4,46 +4,48 @@ import { v1 as uuidV1 } from 'uuid'
import crypto from 'crypto' import crypto from 'crypto'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import http from '../../scripts/http' import { httpGET } from '../../scripts/http'
import models from './models' import * as models from './models'
import Image from './image' import Image from './image'
import UAPI from './index' import { User, Hash } from './index'
const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address'] const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address']
let twitterApp let twitterApp
let discordApp let discordApp
const API = { export class Common {
Common: {
// Generate a hash based on the current session // Generate a hash based on the current session
stateGenerator: (req) => { static stateGenerator (req) {
const sessionCrypto = req.session.id + ':' + config.server.session_secret const sessionCrypto = req.session.id + ':' + config.server.session_secret
return crypto.createHash('sha256').update(sessionCrypto).digest('hex') return crypto.createHash('sha256').update(sessionCrypto).digest('hex')
}, }
// Find an user with an external ID // Find an user with an external ID
getExternal: async (service, identifier) => { static async getExternal (service, identifier) {
let extr = await models.External.query().where('service', service).andWhere('identifier', identifier) let extr = await models.External.query().where('service', service).andWhere('identifier', identifier)
if (!extr || !extr.length) return null if (!extr || !extr.length) return null
extr = extr[0] extr = extr[0]
extr.user = null extr.user = null
if (extr.user_id !== null) { if (extr.user_id !== null) {
const user = await UAPI.User.get(extr.user_id) const user = await User.get(extr.user_id)
if (user) { if (user) {
extr.user = user extr.user = user
} }
} }
return extr return extr
}, }
// Get user ban status // Get user ban status
getBan: async (user, ipAddress) => { static async getBan (user, ipAddress) {
const banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null) const banList = await User.getBanStatus(ipAddress || user.id, ipAddress != null)
return banList return banList
}, }
// Create a new `external` instance for a user // Create a new `external` instance for a user
new: async (service, identifier, user) => { static async new (service, identifier, user) {
const data = { const data = {
user_id: user.id, user_id: user.id,
service: service, service: service,
@ -51,11 +53,12 @@ const API = {
created_at: new Date() created_at: new Date()
} }
await await models.External.query().insert(data) await models.External.query().insert(data)
return true return true
}, }
// Create a new user // Create a new user
newUser: async (service, identifier, data) => { static async newUser (service, identifier, data) {
if (config.external.registrations !== true) throw new Error('Registrations from third-party websites are not allowed.') if (config.external.registrations !== true) throw new Error('Registrations from third-party websites are not allowed.')
const udataLimited = Object.assign({ const udataLimited = Object.assign({
activated: 1, activated: 1,
@ -76,14 +79,14 @@ const API = {
udataLimited.username = udataLimited.username.substring(0, 26) udataLimited.username = udataLimited.username.substring(0, 26)
// Check if the username is already taken // Check if the username is already taken
if (await UAPI.User.get(udataLimited.username) != null || udataLimited.username.length < 4) { if (await User.get(udataLimited.username) != null || udataLimited.username.length < 4) {
udataLimited.username = udataLimited.username + UAPI.Hash(4) udataLimited.username = udataLimited.username + Hash(4)
} }
// Check if the email given to us is already registered, if so, // Check if the email given to us is already registered, if so,
// tell them to log in first. // tell them to log in first.
if (udataLimited.email && udataLimited.email !== '') { if (udataLimited.email && udataLimited.email !== '') {
const getByEmail = await UAPI.User.get(udataLimited.email) const getByEmail = await User.get(udataLimited.email)
if (getByEmail) { if (getByEmail) {
throw new Error('An user with this email address is already registered, but this external account is are not linked. If you wish to link the account, please log in first.') throw new Error('An user with this email address is already registered, but this external account is are not linked. If you wish to link the account, please log in first.')
} }
@ -91,13 +94,14 @@ const API = {
// Create a new user based on the information we got from an external service // Create a new user based on the information we got from an external service
const newUser = await models.User.query().insert(udataLimited) const newUser = await models.User.query().insert(udataLimited)
await API.Common.new(service, identifier, newUser) await Common.new(service, identifier, newUser)
return newUser return newUser
}, }
// Remove an `external` object (thus unlinking from a service) // Remove an `external` object (thus unlinking from a service)
remove: async (user, service) => { static async remove (user, service) {
user = await UAPI.User.ensureObject(user, ['password']) user = await User.ensureObject(user, ['password'])
const userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id) const userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
if (!userExterns.length) { if (!userExterns.length) {
@ -110,33 +114,34 @@ const API = {
} }
return models.External.query().delete().where('user_id', user.id).andWhere('service', service) return models.External.query().delete().where('user_id', user.id).andWhere('service', service)
}, }
// Common code for all auth callbacks // Common code for all auth callbacks
callback: async (identifier, uid, user, ipAddress, remoteData, avatarFunc) => { static async callback (identifier, uid, user, ipAddress, remoteData, avatarFunc) {
const exists = await API.Common.getExternal(identifier, uid) const exists = await Common.getExternal(identifier, uid)
if (user) { if (user) {
// Get bans for user // Get bans for user
const bans = await API.Common.getBan(user) const bans = await Common.getBan(user)
if (bans.length) return { banned: bans, ip: false } if (bans.length) return { banned: bans, ip: false }
if (exists) return { error: null, user: user } if (exists) return { error: null, user: user }
await API.Common.new(identifier, uid, user) await Common.new(identifier, uid, user)
return { error: null, user: user } return { error: null, user: user }
} }
// Callback succeeded with user id and the external table exists, we log in the user // Callback succeeded with user id and the external table exists, we log in the user
if (exists) { if (exists) {
// Get bans for user // Get bans for user
const bans = await API.Common.getBan(exists.user) const bans = await Common.getBan(exists.user)
if (bans.length) return { banned: bans, ip: false } if (bans.length) return { banned: bans, ip: false }
return { error: null, user: exists.user } return { error: null, user: exists.user }
} }
// Get bans for IP address // Get bans for IP address
const bans = await API.Common.getBan(null, ipAddress) const bans = await Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true } if (bans.length) return { banned: bans, ip: true }
// Run the function for avatar fetching // Run the function for avatar fetching
@ -161,16 +166,17 @@ const API = {
let newUser let newUser
try { try {
newUser = await API.Common.newUser(identifier, uid, newUData) newUser = await Common.newUser(identifier, uid, newUData)
} catch (e) { } catch (e) {
return { error: e.message } return { error: e.message }
} }
return { error: null, user: newUser } return { error: null, user: newUser }
} }
}, }
Facebook: {
getAvatar: async (rawData) => { export class Facebook {
static async getAvatar (rawData) {
let profilepic = null let profilepic = null
if (rawData.picture) { if (rawData.picture) {
@ -183,8 +189,9 @@ const API = {
} }
return profilepic return profilepic
}, }
callback: async (user, authResponse, ipAddress) => {
static async callback (user, authResponse, ipAddress) {
if (!authResponse) { if (!authResponse) {
return { error: 'No Authorization' } return { error: 'No Authorization' }
} }
@ -202,7 +209,7 @@ const API = {
} }
try { try {
fbdata = await http.GET('https://graph.facebook.com/v2.10/' + uid + '?' + qs.stringify(intel)) fbdata = await httpGET('https://graph.facebook.com/v2.10/' + uid + '?' + qs.stringify(intel))
fbdata = JSON.parse(fbdata) fbdata = JSON.parse(fbdata)
} catch (e) { } catch (e) {
return { error: 'Could not get user information', errorObject: e } return { error: 'Could not get user information', errorObject: e }
@ -213,16 +220,17 @@ const API = {
} }
const cleanedData = Object.assign(fbdata, { const cleanedData = Object.assign(fbdata, {
username: fbdata.short_name || 'FB' + UAPI.Hash(4), username: fbdata.short_name || 'FB' + Hash(4),
display_name: fbdata.name, display_name: fbdata.name,
email: fbdata.email || '' email: fbdata.email || ''
}) })
return API.Common.callback('facebook', uid, user, ipAddress, cleanedData, API.Facebook.getAvatar) return Common.callback('facebook', uid, user, ipAddress, cleanedData, Facebook.getAvatar)
} }
}, }
Twitter: {
getAvatar: async function (rawData) { export class Twitter {
static async getAvatar (rawData) {
let profilepic = null let profilepic = null
if (rawData.profile_image_url_https) { if (rawData.profile_image_url_https) {
@ -233,8 +241,9 @@ const API = {
} }
return profilepic return profilepic
}, }
oauthApp: function () {
static oauthApp () {
if (!twitterApp) { if (!twitterApp) {
const redirectUri = config.server.domain + '/api/external/twitter/callback' const redirectUri = config.server.domain + '/api/external/twitter/callback'
twitterApp = new oauth.PromiseOAuth( twitterApp = new oauth.PromiseOAuth(
@ -247,9 +256,10 @@ const API = {
'HMAC-SHA1' 'HMAC-SHA1'
) )
} }
}, }
getRequestToken: async function () {
if (!twitterApp) API.Twitter.oauthApp() static async getRequestToken () {
if (!twitterApp) Twitter.oauthApp()
let tokens let tokens
try { try {
@ -262,9 +272,10 @@ const API = {
if (tokens[2].oauth_callback_confirmed !== 'true') return { error: 'No tokens returned.' } if (tokens[2].oauth_callback_confirmed !== 'true') return { error: 'No tokens returned.' }
return { error: null, token: tokens[0], token_secret: tokens[1] } return { error: null, token: tokens[0], token_secret: tokens[1] }
}, }
getAccessTokens: async function (token, secret, verifier) {
if (!twitterApp) API.Twitter.oauthApp() static async getAccessTokens (token, secret, verifier) {
if (!twitterApp) Twitter.oauthApp()
let tokens let tokens
try { try {
@ -277,9 +288,10 @@ const API = {
if (!tokens || !tokens.length) return { error: 'No tokens returned' } if (!tokens || !tokens.length) return { error: 'No tokens returned' }
return { error: null, access_token: tokens[0], access_token_secret: tokens[1] } return { error: null, access_token: tokens[0], access_token_secret: tokens[1] }
}, }
callback: async function (user, accessTokens, ipAddress) {
if (!twitterApp) API.Twitter.oauthApp() static async callback (user, accessTokens, ipAddress) {
if (!twitterApp) Twitter.oauthApp()
let twdata let twdata
try { try {
const resp = await twitterApp.get('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true', const resp = await twitterApp.get('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
@ -298,11 +310,12 @@ const API = {
email: twdata.email || '' email: twdata.email || ''
}) })
return API.Common.callback('twitter', uid, user, ipAddress, cleanedData, API.Twitter.getAvatar) return Common.callback('twitter', uid, user, ipAddress, cleanedData, Twitter.getAvatar)
} }
}, }
Google: {
getAvatar: async (rawData) => { export class Google {
static async getAvatar (rawData) {
let profilepic = null let profilepic = null
if (rawData.image) { if (rawData.image) {
const imgdata = await Image.downloadImage(rawData.image) const imgdata = await Image.downloadImage(rawData.image)
@ -312,12 +325,13 @@ const API = {
} }
return profilepic return profilepic
}, }
callback: async (user, data, ipAddress) => {
static async callback (user, data, ipAddress) {
let uid let uid
try { try {
const test = await http.GET('https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data.id_token) const test = await httpGET('https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data.id_token)
if (!test) throw new Error('No response!') if (!test) throw new Error('No response!')
const jsondata = JSON.parse(test) const jsondata = JSON.parse(test)
@ -338,11 +352,12 @@ const API = {
email: data.email || '' email: data.email || ''
}) })
return API.Common.callback('google', uid, user, ipAddress, cleanedData, API.Google.getAvatar) return Common.callback('google', uid, user, ipAddress, cleanedData, Google.getAvatar)
} }
}, }
Discord: {
getAvatar: async (rawData) => { export class Discord {
static async getAvatar (rawData) {
let profilepic = null let profilepic = null
const aviSnowflake = rawData.avatar const aviSnowflake = rawData.avatar
if (aviSnowflake) { if (aviSnowflake) {
@ -357,8 +372,9 @@ const API = {
} }
return profilepic return profilepic
}, }
oauth2App: function () {
static oauth2App () {
if (discordApp) return if (discordApp) return
discordApp = new oauth.PromiseOAuth2( discordApp = new oauth.PromiseOAuth2(
config.external.discord.api, config.external.discord.api,
@ -369,10 +385,11 @@ const API = {
) )
discordApp.useAuthorizationHeaderforGET(true) discordApp.useAuthorizationHeaderforGET(true)
}, }
getAuthorizeURL: function (req) {
if (!discordApp) API.Discord.oauth2App() static getAuthorizeURL (req) {
const state = API.Common.stateGenerator(req) if (!discordApp) Discord.oauth2App()
const state = Common.stateGenerator(req)
const redirectUri = config.server.domain + '/api/external/discord/callback' const redirectUri = config.server.domain + '/api/external/discord/callback'
const params = { const params = {
@ -386,9 +403,10 @@ const API = {
const url = discordApp.getAuthorizeUrl(params) const url = discordApp.getAuthorizeUrl(params)
return { error: null, state: state, url: url } return { error: null, state: state, url: url }
}, }
getAccessToken: async function (code) {
if (!discordApp) API.Discord.oauth2App() static async getAccessToken (code) {
if (!discordApp) Discord.oauth2App()
const redirectUri = config.server.domain + '/api/external/discord/callback' const redirectUri = config.server.domain + '/api/external/discord/callback'
let tokens let tokens
@ -403,9 +421,10 @@ const API = {
tokens = tokens[2] tokens = tokens[2]
return { error: null, accessToken: tokens.access_token } return { error: null, accessToken: tokens.access_token }
}, }
callback: async function (user, accessToken, ipAddress) {
if (!discordApp) API.Discord.oauth2App() static async callback (user, accessToken, ipAddress) {
if (!discordApp) Discord.oauth2App()
let ddata let ddata
try { try {
@ -424,9 +443,6 @@ const API = {
email: ddata.email || '' email: ddata.email || ''
}) })
return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar) return Common.callback('discord', uid, user, ipAddress, cleanedData, Discord.getAvatar)
} }
} }
}
module.exports = API

View File

@ -4,7 +4,7 @@ import path from 'path'
import crypto from 'crypto' import crypto from 'crypto'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import http from '../../scripts/http' import { downloadURL } from '../../scripts/http'
const fs = require('fs-extra') const fs = require('fs-extra')
@ -13,7 +13,7 @@ const gravatar = 'https://www.gravatar.com/avatar/'
const uploads = path.join(__dirname, '../../', 'usercontent') const uploads = path.join(__dirname, '../../', 'usercontent')
const images = path.join(uploads, 'images') const images = path.join(uploads, 'images')
const maxFileSize = 1000000 const maxFileSize = 1000000
const imageTypes = { export const imageTypes = {
'image/png': '.png', 'image/png': '.png',
'image/jpg': '.jpg', 'image/jpg': '.jpg',
'image/jpeg': '.jpeg' 'image/jpeg': '.jpeg'
@ -51,7 +51,7 @@ async function bailOut (file, error) {
throw new Error(error) throw new Error(error)
} }
async function imageBase64 (baseObj) { export async function imageBase64 (baseObj) {
if (!baseObj) return null if (!baseObj) return null
const imgData = decodeBase64Image(baseObj) const imgData = decodeBase64Image(baseObj)
@ -75,23 +75,23 @@ async function imageBase64 (baseObj) {
return { file: fpath } return { file: fpath }
} }
function gravatarURL (email) { export function gravatarURL (email) {
const sum = crypto.createHash('md5').update(email).digest('hex') const sum = crypto.createHash('md5').update(email).digest('hex')
return gravatar + sum + '.jpg' return gravatar + sum + '.jpg'
} }
async function downloadImage (imgUrl, designation) { export async function downloadImage (imgUrl, designation) {
if (!imgUrl) return null if (!imgUrl) return null
if (!designation) designation = 'download' if (!designation) designation = 'download'
let imageName = designation + '-' + uuid() let imageName = designation + '-' + uuid()
const uridata = new URL(imgUrl) const uridata = new URL(imgUrl)
const pathdata = path.parse(uridata.path) const pathdata = path.parse(uridata.href)
imageName += pathdata.ext || '.png' imageName += pathdata.ext || '.png'
try { try {
await http.Download(imgUrl, path.join(images, imageName)) await downloadURL(imgUrl, path.join(images, imageName))
} catch (e) { } catch (e) {
return null return null
} }
@ -99,7 +99,7 @@ async function downloadImage (imgUrl, designation) {
return imageName return imageName
} }
async function uploadImage (identifier, fields, files) { export async function uploadImage (identifier, fields, files) {
if (!files.image) throw new Error('No image file') if (!files.image) throw new Error('No image file')
let file = files.image[0] let file = files.image[0]
@ -174,11 +174,3 @@ async function uploadImage (identifier, fields, files) {
return { file: fileName } return { file: fileName }
} }
module.exports = {
downloadImage: downloadImage,
uploadImage: uploadImage,
imageBase64: imageBase64,
gravatarURL: gravatarURL,
types: imageTypes
}

View File

@ -7,9 +7,9 @@ import { v1 as uuidV1 } from 'uuid'
import fs from 'fs-extra' import fs from 'fs-extra'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import http from '../../scripts/http' import { httpPOST } from '../../scripts/http'
import models from './models' import * as models from './models'
import emailer from './emailer' import { pushMail } from './emailer'
const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
@ -62,7 +62,7 @@ async function cleanUpDonation (obj, mcOnly, timeframe) {
let user let user
if (obj.user_id) { if (obj.user_id) {
user = await API.User.get(obj.user_id) user = await User.get(obj.user_id)
} }
const result = { const result = {
@ -92,12 +92,12 @@ async function cleanUpDonation (obj, mcOnly, timeframe) {
const txnStore = [] const txnStore = []
const API = { export function Hash (len) {
Hash: (len) => {
return crypto.randomBytes(len).toString('hex') return crypto.randomBytes(len).toString('hex')
}, }
/* ppp - Posts Per Page; dcount - Post Count; page - number of current page */ /* ppp - Posts Per Page; dcount - Post Count; page - number of current page */
Pagination: (ppp, dcount, page) => { export function Pagination (ppp, dcount, page) {
if (!ppp) ppp = 5 if (!ppp) ppp = 5
if (!dcount) return null if (!dcount) return null
@ -113,143 +113,16 @@ const API = {
offset: offset, offset: offset,
total: dcount total: dcount
} }
},
User: {
get: async function (identifier) {
let scope = 'id'
if (typeof identifier === 'string') {
scope = 'username'
if (identifier.indexOf('@') !== -1) {
scope = 'email'
} else if (identifier.length === 36 && identifier.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)) {
scope = 'uuid'
}
} else if (typeof identifier === 'object') {
if (identifier.id != null) {
identifier = identifier.id
} else if (identifier.username) {
scope = 'username'
identifier = identifier.username
} else {
return null
}
} }
const user = await models.User.query().where(scope, identifier) export class Login {
if (!user.length) return null static async password (user, password) {
user = await User.ensureObject(user, ['password'])
return user[0]
},
ensureObject: async function (user, fieldsPresent = ['id']) {
if (typeof user !== 'object' || !keysAvailable(user, fieldsPresent)) {
return API.User.get(user)
}
if (user.id) {
return user
}
return null
},
socialStatus: async function (user) {
user = await API.User.ensureObject(user, ['password'])
if (!user) return null
const external = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
const enabled = {}
for (const i in external) {
const ext = external[i]
enabled[ext.service] = true
}
const accountSourceIsExternal = user.password === null || user.password === ''
const obj = {
enabled: enabled,
password: !accountSourceIsExternal
}
if (accountSourceIsExternal) {
obj.source = external[0].service
}
return obj
},
update: async function (user, data) {
user = await API.User.ensureObject(user)
if (!user) throw new Error('No such user.')
data = Object.assign({
updated_at: new Date()
}, data)
return models.User.query().patchAndFetchById(user.id, data)
},
changeAvatar: async function (user, fileName) {
user = await API.User.ensureObject(user, ['avatar_file'])
const uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
const pathOf = path.join(uploadsDir, fileName)
if (!await fs.exists(pathOf)) {
throw new Error('No such file')
}
// Delete previous upload
if (user.avatar_file != null) {
const file = path.join(uploadsDir, user.avatar_file)
if (await fs.exists(file)) {
await fs.unlink(file)
}
}
await API.User.update(user, { avatar_file: fileName })
return fileName
},
removeAvatar: async function (user) {
user = await API.User.ensureObject(user, ['avatar_file'])
const uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
if (!user.avatar_file) return {}
const file = path.join(uploadsDir, user.avatar_file)
if (await fs.exists(file)) {
await fs.unlink(file)
}
return API.User.update(user, { avatar_file: null })
},
getBanStatus: async function (field, ip = false) {
let bans
if (ip === true) {
bans = await models.Ban.query().where('associated_ip', field)
} else {
bans = await models.Ban.query().where('user_id', field)
}
const bansActive = []
for (const i in bans) {
const ban = bans[i]
// Check expiry
if (ban.expires_at && new Date(ban.expires_at).getTime() < Date.now()) continue
const banInfo = {
banned: ban.created_at,
reason: ban.reason,
expiry: ban.expires_at
}
bansActive.push(banInfo)
}
return bansActive
},
Login: {
password: async function (user, password) {
user = await API.User.ensureObject(user, ['password'])
if (!user.password) return false if (!user.password) return false
return bcryptTask({ task: 'compare', password: password, hash: user.password }) return bcryptTask({ task: 'compare', password: password, hash: user.password })
}, }
activationToken: async function (token) {
static async activationToken (token) {
let getToken = await models.Token.query().where('token', token).andWhere('type', 1) let getToken = await models.Token.query().where('token', token).andWhere('type', 1)
if (!getToken || !getToken.length) return false if (!getToken || !getToken.length) return false
@ -257,23 +130,25 @@ const API = {
if (getToken.expires_at && new Date(getToken.expires_at).getTime() < Date.now()) return false if (getToken.expires_at && new Date(getToken.expires_at).getTime() < Date.now()) return false
const user = await API.User.get(getToken.user_id) const user = await User.get(getToken.user_id)
if (!user) return false if (!user) return false
await models.User.query().patchAndFetchById(user.id, { activated: 1 }) await models.User.query().patchAndFetchById(user.id, { activated: 1 })
await models.Token.query().delete().where('id', getToken.id) await models.Token.query().delete().where('id', getToken.id)
return true return true
}, }
totpTokenRequired: async function (user) {
static async totpTokenRequired (user) {
const getToken = await models.TotpToken.query().where('user_id', user.id) const getToken = await models.TotpToken.query().where('user_id', user.id)
if (!getToken || !getToken.length) return false if (!getToken || !getToken.length) return false
if (getToken[0].activated !== 1) return false if (getToken[0].activated !== 1) return false
return true return true
}, }
totpCheck: async function (user, code, emerg) {
user = await API.User.ensureObject(user) static async totpCheck (user, code, emerg) {
user = await User.ensureObject(user)
let getToken = await models.TotpToken.query().where('user_id', user.id) let getToken = await models.TotpToken.query().where('user_id', user.id)
if (!getToken || !getToken.length) return false if (!getToken || !getToken.length) return false
getToken = getToken[0] getToken = getToken[0]
@ -302,19 +177,21 @@ const API = {
} }
return false return false
}, }
purgeTotp: async function (user, password) {
user = await API.User.ensureObject(user, ['password']) static async purgeTotp (user, password) {
const pwmatch = await API.User.Login.password(user, password) user = await User.ensureObject(user, ['password'])
const pwmatch = await User.Login.password(user, password)
if (!pwmatch) return false if (!pwmatch) return false
// TODO: Inform user via email // TODO: Inform user via email
await models.TotpToken.query().delete().where('user_id', user.id) await models.TotpToken.query().delete().where('user_id', user.id)
return true return true
}, }
totpAquire: async function (user) {
user = await API.User.ensureObject(user, ['password']) static async totpAquire (user) {
user = await User.ensureObject(user, ['password'])
// Do not allow totp for users who have registered using an external service // Do not allow totp for users who have registered using an external service
if (!user.password || user.password === '') return null if (!user.password || user.password === '') return null
@ -327,8 +204,8 @@ const API = {
const newToken = { const newToken = {
user_id: user.id, user_id: user.id,
token: API.Hash(16), token: Hash(16),
recovery_code: API.Hash(8), recovery_code: Hash(8),
activated: 0, activated: 0,
created_at: new Date() created_at: new Date()
} }
@ -342,144 +219,11 @@ const API = {
return uri return uri
} }
},
Register: {
hashPassword: async function (password) {
return bcryptTask({ task: 'hash', password: password })
},
validateEmail: (email) => {
return emailRe.test(email)
},
newAccount: async function (regdata) {
const email = config.email && config.email.enabled
const data = Object.assign(regdata, {
created_at: new Date(),
updated_at: new Date(),
uuid: uuidV1(),
activated: email ? 0 : 1
})
const userTest = await API.User.get(regdata.username)
if (userTest) {
throw new Error('This username is already taken!')
} }
const emailTest = await API.User.get(regdata.email) export class OAuth2 {
if (emailTest) { static async getUserAuthorizations (user) {
throw new Error('This email address is already registered!') user = await User.ensureObject(user)
}
// Create user
const user = await models.User.query().insert(data)
if (email) {
await API.User.Register.activationEmail(user, true)
}
return user
},
activationEmail: async function (user, deleteOnFail = false) {
// Activation token
const activationToken = API.Hash(16)
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: activationToken,
user_id: user.id,
type: 1
})
console.debug('Activation token:', activationToken)
// Send Activation Email
try {
const em = await emailer.pushMail('activate', user.email, {
domain: config.server.domain,
display_name: user.display_name,
activation_token: activationToken
})
console.debug(em)
} catch (e) {
console.error(e)
if (deleteOnFail) {
await models.User.query().delete().where('id', user.id)
}
throw new Error('Invalid email address!')
}
return true
}
},
Reset: {
reset: async function (email, passRequired = true) {
const emailEnabled = config.email && config.email.enabled
if (!emailEnabled) throw new Error('Cannot reset password.')
const user = await API.User.get(email)
if (!user) throw new Error('This email address does not match any user in our database.')
if (!user.password && passRequired) throw new Error('The user associated with this email address has used an external website to log in, thus the password cannot be reset.')
const recentTokens = await models.Token.query().where('user_id', user.id).andWhere('expires_at', '>', new Date()).andWhere('type', 2)
if (recentTokens.length >= 2) {
throw new Error('You\'ve made too many reset requests recently. Please slow down.')
}
const resetToken = API.Hash(16)
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: resetToken,
user_id: user.id,
type: 2
})
// Send Reset Email
console.debug('Reset token:', resetToken)
if (email) {
try {
const em = await emailer.pushMail('reset_password', user.email, {
domain: config.server.domain,
display_name: user.display_name,
reset_token: resetToken
})
console.debug(em)
} catch (e) {
console.error(e)
throw new Error('Invalid email address!')
}
}
return resetToken
},
resetToken: async function (token) {
let getToken = await models.Token.query().where('token', token).andWhere('type', 2)
if (!getToken || !getToken.length) return null
getToken = getToken[0]
if (getToken.expires_at && new Date(getToken.expires_at).getTime() < Date.now()) return null
const user = await API.User.get(getToken.user_id)
if (!user) return null
return user
},
changePassword: async function (user, password, token) {
const hashed = await API.User.Register.hashPassword(password)
await models.User.query().patchAndFetchById(user.id, { password: hashed, updated_at: new Date() })
await models.Token.query().delete().where('token', token)
return true
}
},
OAuth2: {
getUserAuthorizations: async function (user) {
user = await API.User.ensureObject(user)
const auths = await models.OAuth2AuthorizedClient.query().where('user_id', user.id) const auths = await models.OAuth2AuthorizedClient.query().where('user_id', user.id)
const nicelist = [] const nicelist = []
@ -505,9 +249,10 @@ const API = {
} }
return nicelist return nicelist
}, }
removeUserAuthorization: async function (user, clientId) {
user = await API.User.ensureObject(user) static async removeUserAuthorization (user, clientId) {
user = await User.ensureObject(user)
const auth = await models.OAuth2AuthorizedClient.query().where('user_id', user.id).andWhere('client_id', clientId) const auth = await models.OAuth2AuthorizedClient.query().where('user_id', user.id).andWhere('client_id', clientId)
if (!auth.length) return false if (!auth.length) return false
@ -521,14 +266,292 @@ const API = {
return true return true
} }
} }
},
Payment: { export class Register {
handleIPN: async function (body) { static async hashPassword (password) {
return bcryptTask({ task: 'hash', password: password })
}
static validateEmail (email) {
return emailRe.test(email)
}
static async newAccount (regdata) {
const email = config.email && config.email.enabled
const data = Object.assign(regdata, {
created_at: new Date(),
updated_at: new Date(),
uuid: uuidV1(),
activated: email ? 0 : 1
})
const userTest = await User.get(regdata.username)
if (userTest) {
throw new Error('This username is already taken!')
}
const emailTest = await User.get(regdata.email)
if (emailTest) {
throw new Error('This email address is already registered!')
}
// Create user
const user = await models.User.query().insert(data)
if (email) {
await User.Register.activationEmail(user, true)
}
return user
}
static async activationEmail (user, deleteOnFail = false) {
// Activation token
const activationToken = Hash(16)
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: activationToken,
user_id: user.id,
type: 1
})
console.debug('Activation token:', activationToken)
// Send Activation Email
try {
const em = await pushMail('activate', user.email, {
domain: config.server.domain,
display_name: user.display_name,
activation_token: activationToken
})
console.debug(em)
} catch (e) {
console.error(e)
if (deleteOnFail) {
await models.User.query().delete().where('id', user.id)
}
throw new Error('Invalid email address!')
}
return true
}
}
export class Reset {
static async reset (email, passRequired = true) {
const emailEnabled = config.email && config.email.enabled
if (!emailEnabled) throw new Error('Cannot reset password.')
const user = await User.get(email)
if (!user) throw new Error('This email address does not match any user in our database.')
if (!user.password && passRequired) throw new Error('The user associated with this email address has used an external website to log in, thus the password cannot be reset.')
const recentTokens = await models.Token.query().where('user_id', user.id).andWhere('expires_at', '>', new Date()).andWhere('type', 2)
if (recentTokens.length >= 2) {
throw new Error('You\'ve made too many reset requests recently. Please slow down.')
}
const resetToken = Hash(16)
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: resetToken,
user_id: user.id,
type: 2
})
// Send Reset Email
console.debug('Reset token:', resetToken)
if (email) {
try {
const em = await pushMail('reset_password', user.email, {
domain: config.server.domain,
display_name: user.display_name,
reset_token: resetToken
})
console.debug(em)
} catch (e) {
console.error(e)
throw new Error('Invalid email address!')
}
}
return resetToken
}
static async resetToken (token) {
let getToken = await models.Token.query().where('token', token).andWhere('type', 2)
if (!getToken || !getToken.length) return null
getToken = getToken[0]
if (getToken.expires_at && new Date(getToken.expires_at).getTime() < Date.now()) return null
const user = await User.get(getToken.user_id)
if (!user) return null
return user
}
static async changePassword (user, password, token) {
const hashed = await User.Register.hashPassword(password)
await models.User.query().patchAndFetchById(user.id, { password: hashed, updated_at: new Date() })
await models.Token.query().delete().where('token', token)
return true
}
}
export class User {
static async get (identifier) {
let scope = 'id'
if (typeof identifier === 'string') {
scope = 'username'
if (identifier.indexOf('@') !== -1) {
scope = 'email'
} else if (identifier.length === 36 && identifier.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)) {
scope = 'uuid'
}
} else if (typeof identifier === 'object') {
if (identifier.id != null) {
identifier = identifier.id
} else if (identifier.username) {
scope = 'username'
identifier = identifier.username
} else {
return null
}
}
const user = await models.User.query().where(scope, identifier)
if (!user.length) return null
return user[0]
}
static async ensureObject (user, fieldsPresent = ['id']) {
if (typeof user !== 'object' || !keysAvailable(user, fieldsPresent)) {
return User.get(user)
}
if (user.id) {
return user
}
return null
}
static async socialStatus (user) {
user = await User.ensureObject(user, ['password'])
if (!user) return null
const external = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
const enabled = {}
for (const i in external) {
const ext = external[i]
enabled[ext.service] = true
}
const accountSourceIsExternal = user.password === null || user.password === ''
const obj = {
enabled: enabled,
password: !accountSourceIsExternal
}
if (accountSourceIsExternal) {
obj.source = external[0].service
}
return obj
}
static async update (user, data) {
user = await User.ensureObject(user)
if (!user) throw new Error('No such user.')
data = Object.assign({
updated_at: new Date()
}, data)
return models.User.query().patchAndFetchById(user.id, data)
}
static async changeAvatar (user, fileName) {
user = await User.ensureObject(user, ['avatar_file'])
const uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
const pathOf = path.join(uploadsDir, fileName)
if (!await fs.pathExists(pathOf)) {
throw new Error('No such file')
}
// Delete previous upload
if (user.avatar_file != null) {
const file = path.join(uploadsDir, user.avatar_file)
if (await fs.pathExists(file)) {
await fs.unlink(file)
}
}
await User.update(user, { avatar_file: fileName })
return fileName
}
static async removeAvatar (user) {
user = await User.ensureObject(user, ['avatar_file'])
const uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
if (!user.avatar_file) return {}
const file = path.join(uploadsDir, user.avatar_file)
if (await fs.pathExists(file)) {
await fs.unlink(file)
}
return User.update(user, { avatar_file: null })
}
static async getBanStatus (field, ip = false) {
let bans
if (ip === true) {
bans = await models.Ban.query().where('associated_ip', field)
} else {
bans = await models.Ban.query().where('user_id', field)
}
const bansActive = []
for (const i in bans) {
const ban = bans[i]
// Check expiry
if (ban.expires_at && new Date(ban.expires_at).getTime() < Date.now()) continue
const banInfo = {
banned: ban.created_at,
reason: ban.reason,
expiry: ban.expires_at
}
bansActive.push(banInfo)
}
return bansActive
}
}
export class Paymen {
static async handleIPN (body) {
const sandboxed = body.test_ipn === '1' const sandboxed = body.test_ipn === '1'
const url = 'https://ipnpb.' + (sandboxed ? 'sandbox.' : '') + 'paypal.com/cgi-bin/webscr' const url = 'https://ipnpb.' + (sandboxed ? 'sandbox.' : '') + 'paypal.com/cgi-bin/webscr'
console.debug('Incoming payment') console.debug('Incoming payment')
const verification = await http.POST(url, {}, Object.assign({ const verification = await httpPOST(url, {}, Object.assign({
cmd: '_notify-validate' cmd: '_notify-validate'
}, body)) }, body))
@ -568,9 +591,9 @@ const API = {
} }
if (body.user_id != null) { if (body.user_id != null) {
user = await API.User.get(body.user_id) user = await User.get(body.user_id)
} else if (body.payer_email != null) { } else if (body.payer_email != null) {
user = await API.User.get(body.payer_email) user = await User.get(body.payer_email)
} }
const donation = { const donation = {
@ -585,9 +608,10 @@ const API = {
console.log('Server receieved a successful PayPal IPN message.') console.log('Server receieved a successful PayPal IPN message.')
return models.Donation.query().insert(donation) return models.Donation.query().insert(donation)
}, }
userContributions: async function (user) {
user = await API.User.ensureObject(user) static async userContributions (user) {
user = await User.ensureObject(user)
const dbq = await models.Donation.query().orderBy('created_at', 'desc').where('user_id', user.id) const dbq = await models.Donation.query().orderBy('created_at', 'desc').where('user_id', user.id)
const contribs = [] const contribs = []
@ -597,8 +621,9 @@ const API = {
} }
return contribs return contribs
}, }
allContributions: async function (count, mcOnly, timeframe = 0) {
static async allContributions (count, mcOnly, timeframe = 0) {
const dbq = await models.Donation.query().orderBy('created_at', 'desc').limit(count) const dbq = await models.Donation.query().orderBy('created_at', 'desc').limit(count)
const contribs = [] const contribs = []
@ -611,6 +636,3 @@ const API = {
return contribs return contribs
} }
} }
}
module.exports = API

View File

@ -1,109 +1,91 @@
import { Model } from '../../scripts/load-database' import { Model } from '../../scripts/load-database'
class User extends Model { export class User extends Model {
static get tableName () { static get tableName () {
return 'users' return 'users'
} }
} }
class External extends Model { export class External extends Model {
static get tableName () { static get tableName () {
return 'external' return 'external'
} }
} }
class Token extends Model { export class Token extends Model {
static get tableName () { static get tableName () {
return 'simple_token' return 'simple_token'
} }
} }
class OAuth2Client extends Model { export class OAuth2Client extends Model {
static get tableName () { static get tableName () {
return 'oauth2_client' return 'oauth2_client'
} }
} }
class OAuth2AuthorizedClient extends Model { export class OAuth2AuthorizedClient extends Model {
static get tableName () { static get tableName () {
return 'oauth2_client_authorization' return 'oauth2_client_authorization'
} }
} }
class OAuth2Code extends Model { export class OAuth2Code extends Model {
static get tableName () { static get tableName () {
return 'oauth2_code' return 'oauth2_code'
} }
} }
class OAuth2AccessToken extends Model { export class OAuth2AccessToken extends Model {
static get tableName () { static get tableName () {
return 'oauth2_access_token' return 'oauth2_access_token'
} }
} }
class OAuth2RefreshToken extends Model { export class OAuth2RefreshToken extends Model {
static get tableName () { static get tableName () {
return 'oauth2_refresh_token' return 'oauth2_refresh_token'
} }
} }
class TotpToken extends Model { export class TotpToken extends Model {
static get tableName () { static get tableName () {
return 'totp_token' return 'totp_token'
} }
} }
class Ban extends Model { export class Ban extends Model {
static get tableName () { static get tableName () {
return 'network_ban' return 'network_ban'
} }
} }
class News extends Model { export class News extends Model {
static get tableName () { static get tableName () {
return 'news' return 'news'
} }
} }
class Donation extends Model { export class Donation extends Model {
static get tableName () { static get tableName () {
return 'donation' return 'donation'
} }
} }
class Subscription extends Model { export class Subscription extends Model {
static get tableName () { static get tableName () {
return 'subscription' return 'subscription'
} }
} }
class MinecraftMember extends Model { export class MinecraftMember extends Model {
static get tableName () { static get tableName () {
return 'mc_member' return 'mc_member'
} }
} }
class MinecraftToken extends Model { export class MinecraftToken extends Model {
static get tableName () { static get tableName () {
return 'mc_verify' return 'mc_verify'
} }
} }
module.exports = {
User: User,
External: External,
Token: Token,
OAuth2Client: OAuth2Client,
OAuth2AuthorizedClient: OAuth2AuthorizedClient,
OAuth2Code: OAuth2Code,
OAuth2AccessToken: OAuth2AccessToken,
OAuth2RefreshToken: OAuth2RefreshToken,
TotpToken: TotpToken,
Ban: Ban,
News: News,
Donation: Donation,
Subscription: Subscription,
MinecraftMember: MinecraftMember,
MinecraftToken: MinecraftToken
}

View File

@ -1,5 +1,5 @@
import API from './index' import { User, Pagination } from './index'
import Models from './models' import * as Models from './models'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import { Feed } from 'feed' import { Feed } from 'feed'
@ -13,7 +13,7 @@ function slugify (title) {
} }
async function cleanArticle (entry, shortenContent = false) { async function cleanArticle (entry, shortenContent = false) {
const poster = await API.User.get(entry.user_id) const poster = await User.get(entry.user_id)
const article = { const article = {
id: entry.id, id: entry.id,
slug: slugify(entry.title), slug: slugify(entry.title),
@ -37,8 +37,7 @@ async function cleanArticle (entry, shortenContent = false) {
return article return article
} }
const News = { export async function preview () {
preview: async () => {
// Fetch 3 latest stories // Fetch 3 latest stories
const news = await Models.News.query().orderBy('created_at', 'desc').limit(3) const news = await Models.News.query().orderBy('created_at', 'desc').limit(3)
@ -51,8 +50,9 @@ const News = {
} }
return articles return articles
}, }
listNews: async (page) => {
export async function listNews (page) {
let count = await Models.News.query().count('id as ids') let count = await Models.News.query().count('id as ids')
if (page < 1) page = 1 if (page < 1) page = 1
@ -61,7 +61,7 @@ const News = {
} }
count = count[0].ids count = count[0].ids
const paginated = API.Pagination(perPage, parseInt(count), page) const paginated = Pagination(perPage, parseInt(count), page)
const news = await Models.News.query().orderBy('created_at', 'desc').offset(paginated.offset).limit(perPage) const news = await Models.News.query().orderBy('created_at', 'desc').offset(paginated.offset).limit(perPage)
const articles = [] const articles = []
@ -75,15 +75,17 @@ const News = {
page: paginated, page: paginated,
articles: articles articles: articles
} }
}, }
article: async (id) => {
export async function article (id) {
let article = await Models.News.query().where('id', id) let article = await Models.News.query().where('id', id)
if (!article.length) return {} if (!article.length) return {}
article = article[0] article = article[0]
return cleanArticle(article) return cleanArticle(article)
}, }
compose: async (user, body) => {
export async function compose (user, body) {
const article = { const article = {
title: body.title, title: body.title,
content: body.content, content: body.content,
@ -97,8 +99,9 @@ const News = {
result.slug = slugify(result.title) result.slug = slugify(result.title)
return result return result
}, }
edit: async (id, body) => {
export async function edit (id, body) {
const patch = { const patch = {
content: body.content, content: body.content,
updated_at: new Date() updated_at: new Date()
@ -107,8 +110,9 @@ const News = {
const result = await Models.News.query().patchAndFetchById(id, patch) const result = await Models.News.query().patchAndFetchById(id, patch)
if (!result) throw new Error('Something went wrong.') if (!result) throw new Error('Something went wrong.')
return {} return {}
}, }
generateFeed: async () => {
export async function generateFeed () {
if (feed && new Date(feed.options.updated).getTime() > Date.now() - 3600000) return feed // Update feed hourly if (feed && new Date(feed.options.updated).getTime() > Date.now() - 3600000) return feed // Update feed hourly
const posts = await Models.News.query().orderBy('created_at', 'desc').limit(perPage) const posts = await Models.News.query().orderBy('created_at', 'desc').limit(perPage)
@ -143,13 +147,13 @@ const News = {
feed.addItem({ feed.addItem({
title: post.title, title: post.title,
id: post.id,
link: `${config.server.domain}/news/${post.id}-${slugify(post.title)}`, link: `${config.server.domain}/news/${post.id}-${slugify(post.title)}`,
description: post.description, description: post.description,
content: post.content, content: post.content,
author: [{ author: [{
name: post.author.display_name, name: post.author.display_name,
email: post.author.email email: post.author.email,
link: config.server.domain
}], }],
date: new Date(post.updated_at) date: new Date(post.updated_at)
}) })
@ -157,6 +161,3 @@ const News = {
return feed return feed
} }
}
module.exports = News

View File

@ -1,9 +1,16 @@
import error from '../error' import {
import response from '../response' InvalidRequest,
import model from '../model' UnsupportedResponseType,
InvalidClient,
UnauthorizedClient,
InvalidScope,
AccessDenied
} from '../error'
import { data as dataResponse } from '../response'
import * as model from '../model'
import wrap from '../wrap' import wrap from '../wrap'
module.exports = wrap(async (req, res, next) => { export const authorization = wrap(async (req, res, next) => {
let clientId = null let clientId = null
let redirectUri = null let redirectUri = null
let responseType = null let responseType = null
@ -12,26 +19,26 @@ module.exports = wrap(async (req, res, next) => {
let user = null let user = null
if (!req.query.redirect_uri) { if (!req.query.redirect_uri) {
throw new error.InvalidRequest('redirect_uri field is mandatory for authorization endpoint') throw new InvalidRequest('redirect_uri field is mandatory for authorization endpoint')
} }
redirectUri = req.query.redirect_uri redirectUri = req.query.redirect_uri
console.debug('Parameter redirect uri is', redirectUri) console.debug('Parameter redirect uri is', redirectUri)
if (!req.query.client_id) { if (!req.query.client_id) {
throw new error.InvalidRequest('client_id field is mandatory for authorization endpoint') throw new InvalidRequest('client_id field is mandatory for authorization endpoint')
} }
// Check for client_secret (prevent passing it) // Check for client_secret (prevent passing it)
if (req.query.client_secret) { if (req.query.client_secret) {
throw new error.InvalidRequest('client_secret field should not be passed to the authorization endpoint') throw new InvalidRequest('client_secret field should not be passed to the authorization endpoint')
} }
clientId = req.query.client_id clientId = req.query.client_id
console.debug('Parameter client_id is', clientId) console.debug('Parameter client_id is', clientId)
if (!req.query.response_type) { if (!req.query.response_type) {
throw new error.InvalidRequest('response_type field is mandatory for authorization endpoint') throw new InvalidRequest('response_type field is mandatory for authorization endpoint')
} }
responseType = req.query.response_type responseType = req.query.response_type
@ -52,7 +59,7 @@ module.exports = wrap(async (req, res, next) => {
grantTypes.push(responseTypes[i]) grantTypes.push(responseTypes[i])
break break
default: default:
throw new error.UnsupportedResponseType('Unknown response_type parameter passed') throw new UnsupportedResponseType('Unknown response_type parameter passed')
} }
} }
@ -63,28 +70,28 @@ module.exports = wrap(async (req, res, next) => {
// "None" type cannot be combined with others // "None" type cannot be combined with others
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) { if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
throw new error.InvalidRequest('Grant type "none" cannot be combined with other grant types') throw new InvalidRequest('Grant type "none" cannot be combined with other grant types')
} }
console.debug('Parameter grant_type is', grantTypes.join(' ')) console.debug('Parameter grant_type is', grantTypes.join(' '))
const client = await req.oauth2.model.client.fetchById(clientId) const client = await req.oauth2.model.client.fetchById(clientId)
if (!client) { if (!client) {
throw new error.InvalidClient('Client not found') throw new InvalidClient('Client not found')
} }
// TODO: multiple redirect URI // TODO: multiple redirect URI
if (!req.oauth2.model.client.getRedirectUri(client)) { if (!req.oauth2.model.client.getRedirectUri(client)) {
throw new error.UnsupportedResponseType('The client has not set a redirect uri') throw new UnsupportedResponseType('The client has not set a redirect uri')
} else if (!req.oauth2.model.client.checkRedirectUri(client, redirectUri)) { } else if (!req.oauth2.model.client.checkRedirectUri(client, redirectUri)) {
throw new error.InvalidRequest('Wrong RedirectUri provided') throw new InvalidRequest('Wrong RedirectUri provided')
} }
console.debug('redirect_uri check passed') console.debug('redirect_uri check passed')
// The client needs to support all grant types // The client needs to support all grant types
for (const i in grantTypes) { for (const i in grantTypes) {
if (!req.oauth2.model.client.checkGrantType(client, grantTypes[i]) && grantTypes[i] !== 'none') { if (!req.oauth2.model.client.checkGrantType(client, grantTypes[i]) && grantTypes[i] !== 'none') {
throw new error.UnauthorizedClient('This client does not support grant type ' + grantTypes[i]) throw new UnauthorizedClient('This client does not support grant type ' + grantTypes[i])
} }
} }
console.debug('Grant type check passed') console.debug('Grant type check passed')
@ -92,16 +99,16 @@ module.exports = wrap(async (req, res, next) => {
scope = req.oauth2.model.client.transformScope(req.query.scope) scope = req.oauth2.model.client.transformScope(req.query.scope)
scope = req.oauth2.model.client.checkScope(client, scope) scope = req.oauth2.model.client.checkScope(client, scope)
if (!scope) { if (!scope) {
throw new error.InvalidScope('Client does not allow access to this scope') throw new InvalidScope('Client does not allow access to this scope')
} }
console.debug('Scope check passed') console.debug('Scope check passed')
user = await req.oauth2.model.user.fetchFromRequest(req) user = await req.oauth2.model.user.fetchFromRequest(req)
if (!user) { if (!user) {
throw new error.InvalidRequest('There is no currently logged in user') throw new InvalidRequest('There is no currently logged in user')
} else { } else {
if (!user.username) { if (!user.username) {
throw new error.Forbidden(user) throw new Forbidden(user)
} }
console.debug('User fetched from request') console.debug('User fetched from request')
} }
@ -122,15 +129,15 @@ module.exports = wrap(async (req, res, next) => {
// Consent pushed, ensure valid session // Consent pushed, ensure valid session
if (req.method === 'POST' && req.session.csrf && !(req.body.csrf && req.body.csrf === req.session.csrf)) { if (req.method === 'POST' && req.session.csrf && !(req.body.csrf && req.body.csrf === req.session.csrf)) {
throw new error.InvalidRequest('Invalid session') throw new InvalidRequest('Invalid session')
} }
// Save consent // Save consent
if (!consented) { if (!consented) {
if (!req.body || (typeof req.body.decision) === 'undefined') { if (!req.body || (typeof req.body.decision) === 'undefined') {
throw new error.InvalidRequest('No decision parameter passed') throw new InvalidRequest('No decision parameter passed')
} else if (req.body.decision === '0') { } else if (req.body.decision === '0') {
throw new error.AccessDenied('User denied access to the resource') throw new AccessDenied('User denied access to the resource')
} }
console.debug('Decision check passed') console.debug('Decision check passed')
@ -161,10 +168,10 @@ module.exports = wrap(async (req, res, next) => {
resObj = {} resObj = {}
break break
default: default:
throw new error.UnsupportedResponseType('Unknown response_type parameter passed') throw new UnsupportedResponseType('Unknown response_type parameter passed')
} }
} }
// Return non-code response types as fragment instead of query // Return non-code response types as fragment instead of query
return response.data(req, res, resObj, redirectUri, responseType !== 'code') return dataResponse(req, res, resObj, redirectUri, responseType !== 'code')
}, true) }, true)

View File

@ -1,3 +1,3 @@
module.exports = function (req, res, client, scope, user) { export function decision (req, res, client, scope, user) {
res.render('authorization', { client: client, scope: scope }) res.render('authorization', { client: client, scope: scope })
} }

View File

@ -1,5 +1,3 @@
module.exports = { export * from './authorization';
authorization: require('./authorization'), export * from './introspection';
introspection: require('./introspection'), export * from './token';
token: require('./token')
}

View File

@ -1,8 +1,8 @@
import error from '../error' import { InvalidRequest } from '../error'
import response from '../response' import { data as dataResponse } from '../response'
import wrap from '../wrap' import wrap from '../wrap'
module.exports = wrap(async function (req, res) { export const introspection = wrap(async function (req, res) {
let clientId = null let clientId = null
let clientSecret = null let clientSecret = null
@ -12,21 +12,21 @@ module.exports = wrap(async function (req, res) {
console.debug('Client credentials parsed from body parameters ', clientId, clientSecret) console.debug('Client credentials parsed from body parameters ', clientId, clientSecret)
} else { } else {
if (!req.headers || !req.headers.authorization) { if (!req.headers || !req.headers.authorization) {
throw new error.InvalidRequest('No authorization header passed') throw new InvalidRequest('No authorization header passed')
} }
let pieces = req.headers.authorization.split(' ', 2) let pieces = req.headers.authorization.split(' ', 2)
if (!pieces || pieces.length !== 2) { if (!pieces || pieces.length !== 2) {
throw new error.InvalidRequest('Authorization header is corrupted') throw new InvalidRequest('Authorization header is corrupted')
} }
if (pieces[0] !== 'Basic') { if (pieces[0] !== 'Basic') {
throw new error.InvalidRequest('Unsupported authorization method:', pieces[0]) throw new InvalidRequest('Unsupported authorization method:', pieces[0])
} }
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2) pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2)
if (!pieces || pieces.length !== 2) { if (!pieces || pieces.length !== 2) {
throw new error.InvalidRequest('Authorization header has corrupted data') throw new InvalidRequest('Authorization header has corrupted data')
} }
clientId = pieces[0] clientId = pieces[0]
@ -35,12 +35,12 @@ module.exports = wrap(async function (req, res) {
} }
if (!req.body.token) { if (!req.body.token) {
throw new error.InvalidRequest('Token not provided in request body') throw new InvalidRequest('Token not provided in request body')
} }
const token = await req.oauth2.model.accessToken.fetchByToken(req.body.token) const token = await req.oauth2.model.accessToken.fetchByToken(req.body.token)
if (!token) { if (!token) {
throw new error.InvalidRequest('Token does not exist') throw new InvalidRequest('Token does not exist')
} }
const ttl = req.oauth2.model.accessToken.getTTL(token) const ttl = req.oauth2.model.accessToken.getTTL(token)
@ -50,5 +50,5 @@ module.exports = wrap(async function (req, res) {
expires_in: Math.floor(ttl / 1000) expires_in: Math.floor(ttl / 1000)
} }
response.data(req, res, resObj) dataResponse(req, res, resObj)
}) })

View File

@ -1,9 +1,9 @@
import token from './tokens' import * as tokens from './tokens'
import error from '../error' import { InvalidRequest, InvalidClient, UnauthorizedClient } from '../error'
import response from '../response' import { data as dataResponse, error as errorResponse } from '../response'
import wrap from '../wrap' import wrap from '../wrap'
module.exports = wrap(async (req, res) => { export const token = wrap(async (req, res) => {
let clientId = null let clientId = null
let clientSecret = null let clientSecret = null
let grantType = null let grantType = null
@ -14,21 +14,21 @@ module.exports = wrap(async (req, res) => {
console.debug('Client credentials parsed from body parameters', clientId, clientSecret) console.debug('Client credentials parsed from body parameters', clientId, clientSecret)
} else { } else {
if (!req.headers || !req.headers.authorization) { if (!req.headers || !req.headers.authorization) {
throw new error.InvalidRequest('No authorization header passed') throw new InvalidRequest('No authorization header passed')
} }
let pieces = req.headers.authorization.split(' ', 2) let pieces = req.headers.authorization.split(' ', 2)
if (!pieces || pieces.length !== 2) { if (!pieces || pieces.length !== 2) {
throw new error.InvalidRequest('Authorization header is corrupted') throw new InvalidRequest('Authorization header is corrupted')
} }
if (pieces[0] !== 'Basic') { if (pieces[0] !== 'Basic') {
throw new error.InvalidRequest('Unsupported authorization method:', pieces[0]) throw new InvalidRequest('Unsupported authorization method:', pieces[0])
} }
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2) pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2)
if (!pieces || pieces.length !== 2) { if (!pieces || pieces.length !== 2) {
throw new error.InvalidRequest('Authorization header has corrupted data') throw new InvalidRequest('Authorization header has corrupted data')
} }
clientId = pieces[0] clientId = pieces[0]
@ -37,7 +37,7 @@ module.exports = wrap(async (req, res) => {
} }
if (!req.body.grant_type) { if (!req.body.grant_type) {
throw new error.InvalidRequest('Request body does not contain grant_type parameter') throw new InvalidRequest('Request body does not contain grant_type parameter')
} }
grantType = req.body.grant_type grantType = req.body.grant_type
@ -46,16 +46,16 @@ module.exports = wrap(async (req, res) => {
const client = await req.oauth2.model.client.fetchById(clientId) const client = await req.oauth2.model.client.fetchById(clientId)
if (!client) { if (!client) {
throw new error.InvalidClient('Client not found') throw new InvalidClient('Client not found')
} }
const valid = req.oauth2.model.client.checkSecret(client, clientSecret) const valid = req.oauth2.model.client.checkSecret(client, clientSecret)
if (!valid) { if (!valid) {
throw new error.UnauthorizedClient('Invalid client secret') throw new UnauthorizedClient('Invalid client secret')
} }
if (!req.oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'refresh_token') { if (!req.oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
throw new error.UnauthorizedClient('Invalid grant type for the client') throw new UnauthorizedClient('Invalid grant type for the client')
} else { } else {
console.debug('Grant type check passed') console.debug('Grant type check passed')
} }
@ -64,25 +64,25 @@ module.exports = wrap(async (req, res) => {
try { try {
switch (grantType) { switch (grantType) {
case 'authorization_code': case 'authorization_code':
evt = await token.authorizationCode(req.oauth2, client, req.body.code, req.body.redirect_uri) evt = await tokens.authorizationCode(req.oauth2, client, req.body.code, req.body.redirect_uri)
break break
case 'password': case 'password':
evt = await token.password(req.oauth2, client, req.body.username, req.body.password, req.body.scope) evt = await tokens.password(req.oauth2, client, req.body.username, req.body.password, req.body.scope)
break break
case 'client_credentials': case 'client_credentials':
evt = await token.clientCredentials(req.oauth2, client, req.body.scope) evt = await tokens.clientCredentials(req.oauth2, client, req.body.scope)
break break
case 'refresh_token': case 'refresh_token':
evt = await token.refreshToken(req.oauth2, client, req.body.refresh_token, req.body.scope) evt = await tokens.refreshToken(req.oauth2, client, req.body.refresh_token, req.body.scope)
break break
default: default:
throw new error.UnsupportedGrantType('Grant type does not match any supported type') throw new error.UnsupportedGrantType('Grant type does not match any supported type')
} }
if (evt) { if (evt) {
response.data(req, res, evt) dataResponse(req, res, evt)
} }
} catch (e) { } catch (e) {
response.error(req, res, e) errorResponse(req, res, e)
} }
}) })

View File

@ -1,6 +1,6 @@
import error from '../../error' import { InvalidRequest, ServerError, InvalidGrant } from '../../error'
module.exports = async (oauth2, client, providedCode, redirectUri) => { export async function authorizationCode (oauth2, client, providedCode, redirectUri) {
const respObj = { const respObj = {
token_type: 'bearer' token_type: 'bearer'
} }
@ -8,26 +8,26 @@ module.exports = async (oauth2, client, providedCode, redirectUri) => {
let code = null let code = null
if (!providedCode) { if (!providedCode) {
throw new error.InvalidRequest('code is mandatory for authorization_code grant type') throw new InvalidRequest('code is mandatory for authorization_code grant type')
} }
try { try {
code = await oauth2.model.code.fetchByCode(providedCode) code = await oauth2.model.code.fetchByCode(providedCode)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new error.ServerError('Failed to call code.fetchByCode function') throw new ServerError('Failed to call code.fetchByCode function')
} }
if (code) { if (code) {
if (oauth2.model.code.getClientId(code) !== oauth2.model.client.getId(client)) { if (oauth2.model.code.getClientId(code) !== oauth2.model.client.getId(client)) {
throw new error.InvalidGrant('Code was issued by another client') throw new InvalidGrant('Code was issued by another client')
} }
if (!oauth2.model.code.checkTTL(code)) { if (!oauth2.model.code.checkTTL(code)) {
throw new error.InvalidGrant('Code has already expired') throw new InvalidGrant('Code has already expired')
} }
} else { } else {
throw new error.InvalidGrant('Code not found') throw new InvalidGrant('Code not found')
} }
console.debug('Code fetched ', code) console.debug('Code fetched ', code)
@ -37,7 +37,7 @@ module.exports = async (oauth2, client, providedCode, redirectUri) => {
oauth2.model.code.getClientId(code)) oauth2.model.code.getClientId(code))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new error.ServerError('Failed to call refreshToken.removeByUserIdClientId function') throw new ServerError('Failed to call refreshToken.removeByUserIdClientId function')
} }
console.debug('Refresh token removed') console.debug('Refresh token removed')
@ -50,7 +50,7 @@ module.exports = async (oauth2, client, providedCode, redirectUri) => {
oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code)) oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new error.ServerError('Failed to call refreshToken.create function') throw new ServerError('Failed to call refreshToken.create function')
} }
} }
@ -59,7 +59,7 @@ module.exports = async (oauth2, client, providedCode, redirectUri) => {
oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code), oauth2.model.accessToken.ttl) oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code), oauth2.model.accessToken.ttl)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new error.ServerError('Failed to call accessToken.create function') throw new ServerError('Failed to call accessToken.create function')
} }
respObj.expires_in = oauth2.model.accessToken.ttl respObj.expires_in = oauth2.model.accessToken.ttl
@ -69,7 +69,7 @@ module.exports = async (oauth2, client, providedCode, redirectUri) => {
await oauth2.model.code.removeByCode(providedCode) await oauth2.model.code.removeByCode(providedCode)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new error.ServerError('Failed to call code.removeByCode function') throw new ServerError('Failed to call code.removeByCode function')
} }
return respObj return respObj

View File

@ -1,6 +1,6 @@
import error from '../../error' import { ServerError, InvalidScope } from '../../error'
module.exports = async (oauth2, client, wantScope) => { export async function clientCredentials (oauth2, client, wantScope) {
let scope = null let scope = null
const resObj = { const resObj = {
@ -11,7 +11,7 @@ module.exports = async (oauth2, client, wantScope) => {
scope = oauth2.model.client.checkScope(client, scope) scope = oauth2.model.client.checkScope(client, scope)
if (!scope) { if (!scope) {
throw new error.InvalidScope('Client does not allow access to this scope') throw new InvalidScope('Client does not allow access to this scope')
} }
console.debug('Scope check passed ', scope) console.debug('Scope check passed ', scope)
@ -20,7 +20,7 @@ module.exports = async (oauth2, client, wantScope) => {
resObj.access_token = await oauth2.model.accessToken.create(null, oauth2.model.client.getId(client), resObj.access_token = await oauth2.model.accessToken.create(null, oauth2.model.client.getId(client),
scope, oauth2.model.accessToken.ttl) scope, oauth2.model.accessToken.ttl)
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call accessToken.create function') throw new ServerError('Failed to call accessToken.create function')
} }
resObj.expires_in = oauth2.model.accessToken.ttl resObj.expires_in = oauth2.model.accessToken.ttl

View File

@ -1,11 +1,4 @@
import authorizationCode from './authorizationCode' export * from './authorizationCode'
import clientCredentials from './clientCredentials' export * from './clientCredentials'
import password from './password' export * from './password'
import refreshToken from './refreshToken' export * from './refreshToken'
module.exports = {
authorizationCode: authorizationCode,
clientCredentials: clientCredentials,
password: password,
refreshToken: refreshToken
}

View File

@ -1,23 +1,23 @@
import error from '../../error' import { ServerError, InvalidRequest, InvalidScope, InvalidClient } from '../../error'
module.exports = async (oauth2, client, username, password, scope) => { export async function password (oauth2, client, username, password, scope) {
let user = null let user = null
const resObj = { const resObj = {
token_type: 'bearer' token_type: 'bearer'
} }
if (!username) { if (!username) {
throw new error.InvalidRequest('Username is mandatory for password grant type') throw new InvalidRequest('Username is mandatory for password grant type')
} }
if (!password) { if (!password) {
throw new error.InvalidRequest('Password is mandatory for password grant type') throw new InvalidRequest('Password is mandatory for password grant type')
} }
scope = oauth2.model.client.transformScope(scope) scope = oauth2.model.client.transformScope(scope)
scope = oauth2.model.client.checkScope(client, scope) scope = oauth2.model.client.checkScope(client, scope)
if (!scope) { if (!scope) {
throw new error.InvalidScope('Client does not allow access to this scope') throw new InvalidScope('Client does not allow access to this scope')
} else { } else {
console.debug('Scope check passed: ', scope) console.debug('Scope check passed: ', scope)
} }
@ -25,23 +25,23 @@ module.exports = async (oauth2, client, username, password, scope) => {
try { try {
user = await oauth2.model.user.fetchByUsername(username) user = await oauth2.model.user.fetchByUsername(username)
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call user.fetchByUsername function') throw new ServerError('Failed to call user.fetchByUsername function')
} }
if (!user) { if (!user) {
throw new error.InvalidClient('User not found') throw new InvalidClient('User not found')
} }
const valid = await oauth2.model.user.checkPassword(user, password) const valid = await oauth2.model.user.checkPassword(user, password)
if (!valid) { if (!valid) {
throw new error.InvalidClient('Wrong password') throw new InvalidClient('Wrong password')
} }
try { try {
await oauth2.model.refreshToken.removeByUserIdClientId(oauth2.model.user.getId(user), await oauth2.model.refreshToken.removeByUserIdClientId(oauth2.model.user.getId(user),
oauth2.model.client.getId(client)) oauth2.model.client.getId(client))
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call refreshToken.removeByUserIdClientId function') throw new ServerError('Failed to call refreshToken.removeByUserIdClientId function')
} }
console.debug('Refresh token removed') console.debug('Refresh token removed')
@ -53,7 +53,7 @@ module.exports = async (oauth2, client, username, password, scope) => {
resObj.refresh_token = await oauth2.model.refreshToken.create(oauth2.model.user.getId(user), resObj.refresh_token = await oauth2.model.refreshToken.create(oauth2.model.user.getId(user),
oauth2.model.client.getId(client), scope) oauth2.model.client.getId(client), scope)
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call refreshToken.create function') throw new ServerError('Failed to call refreshToken.create function')
} }
} }
@ -61,7 +61,7 @@ module.exports = async (oauth2, client, username, password, scope) => {
resObj.access_token = await oauth2.model.accessToken.create(oauth2.model.user.getId(user), resObj.access_token = await oauth2.model.accessToken.create(oauth2.model.user.getId(user),
oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl) oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl)
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call accessToken.create function') throw new ServerError('Failed to call accessToken.create function')
} }
resObj.expires_in = oauth2.model.accessToken.ttl resObj.expires_in = oauth2.model.accessToken.ttl

View File

@ -1,6 +1,6 @@
import error from '../../error' import { InvalidRequest, ServerError, InvalidGrant, InvalidClient } from '../../error'
module.exports = async (oauth2, client, pRefreshToken, scope) => { export async function refreshToken (oauth2, client, pRefreshToken, scope) {
let user = null let user = null
let ttl = null let ttl = null
let refreshToken = null let refreshToken = null
@ -11,40 +11,40 @@ module.exports = async (oauth2, client, pRefreshToken, scope) => {
} }
if (!pRefreshToken) { if (!pRefreshToken) {
throw new error.InvalidRequest('refresh_token is mandatory for refresh_token grant type') throw new InvalidRequest('refresh_token is mandatory for refresh_token grant type')
} }
try { try {
refreshToken = await oauth2.model.refreshToken.fetchByToken(pRefreshToken) refreshToken = await oauth2.model.refreshToken.fetchByToken(pRefreshToken)
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call refreshToken.fetchByToken function') throw new ServerError('Failed to call refreshToken.fetchByToken function')
} }
if (!refreshToken) { if (!refreshToken) {
throw new error.InvalidGrant('Refresh token not found') throw new InvalidGrant('Refresh token not found')
} }
if (oauth2.model.refreshToken.getClientId(refreshToken) !== oauth2.model.client.getId(client)) { if (oauth2.model.refreshToken.getClientId(refreshToken) !== oauth2.model.client.getId(client)) {
console.warn('Client %s tried to fetch a refresh token which belongs to client %s!', oauth2.model.client.getId(client), console.warn('Client %s tried to fetch a refresh token which belongs to client %s!', oauth2.model.client.getId(client),
oauth2.model.refreshToken.getClientId(refreshToken)) oauth2.model.refreshToken.getClientId(refreshToken))
throw new error.InvalidGrant('Refresh token not found') throw new InvalidGrant('Refresh token not found')
} }
try { try {
user = await oauth2.model.user.fetchById(oauth2.model.refreshToken.getUserId(refreshToken)) user = await oauth2.model.user.fetchById(oauth2.model.refreshToken.getUserId(refreshToken))
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call user.fetchById function') throw new ServerError('Failed to call user.fetchById function')
} }
if (!user) { if (!user) {
throw new error.InvalidClient('User not found') throw new InvalidClient('User not found')
} }
try { try {
accessToken = await oauth2.model.accessToken.fetchByUserIdClientId(oauth2.model.user.getId(user), accessToken = await oauth2.model.accessToken.fetchByUserIdClientId(oauth2.model.user.getId(user),
oauth2.model.client.getId(client)) oauth2.model.client.getId(client))
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call accessToken.fetchByUserIdClientId function') throw new ServerError('Failed to call accessToken.fetchByUserIdClientId function')
} }
if (accessToken) { if (accessToken) {
@ -63,7 +63,7 @@ module.exports = async (oauth2, client, pRefreshToken, scope) => {
resObj.access_token = await oauth2.model.accessToken.create(oauth2.model.user.getId(user), resObj.access_token = await oauth2.model.accessToken.create(oauth2.model.user.getId(user),
oauth2.model.client.getId(client), oauth2.model.refreshToken.getScope(refreshToken), oauth2.model.accessToken.ttl) oauth2.model.client.getId(client), oauth2.model.refreshToken.getScope(refreshToken), oauth2.model.accessToken.ttl)
} catch (err) { } catch (err) {
throw new error.ServerError('Failed to call accessToken.create function') throw new ServerError('Failed to call accessToken.create function')
} }
resObj.expires_in = oauth2.model.accessToken.ttl resObj.expires_in = oauth2.model.accessToken.ttl

View File

@ -1,4 +1,4 @@
class OAuth2Error extends Error { export class OAuth2Error extends Error {
constructor (code, msg, status) { constructor (code, msg, status) {
super() super()
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor)
@ -12,7 +12,7 @@ class OAuth2Error extends Error {
} }
} }
class AccessDenied extends OAuth2Error { export class AccessDenied extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('access_denied', msg, 403) super('access_denied', msg, 403)
@ -21,7 +21,7 @@ class AccessDenied extends OAuth2Error {
} }
} }
class InvalidClient extends OAuth2Error { export class InvalidClient extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('invalid_client', msg, 401) super('invalid_client', msg, 401)
@ -30,7 +30,7 @@ class InvalidClient extends OAuth2Error {
} }
} }
class InvalidGrant extends OAuth2Error { export class InvalidGrant extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('invalid_grant', msg, 400) super('invalid_grant', msg, 400)
@ -39,7 +39,7 @@ class InvalidGrant extends OAuth2Error {
} }
} }
class InvalidRequest extends OAuth2Error { export class InvalidRequest extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('invalid_request', msg, 400) super('invalid_request', msg, 400)
@ -48,7 +48,7 @@ class InvalidRequest extends OAuth2Error {
} }
} }
class InvalidScope extends OAuth2Error { export class InvalidScope extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('invalid_scope', msg, 400) super('invalid_scope', msg, 400)
@ -57,7 +57,7 @@ class InvalidScope extends OAuth2Error {
} }
} }
class ServerError extends OAuth2Error { export class ServerError extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('server_error', msg, 500) super('server_error', msg, 500)
@ -66,7 +66,7 @@ class ServerError extends OAuth2Error {
} }
} }
class UnauthorizedClient extends OAuth2Error { export class UnauthorizedClient extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('unauthorized_client', msg, 400) super('unauthorized_client', msg, 400)
@ -75,7 +75,7 @@ class UnauthorizedClient extends OAuth2Error {
} }
} }
class UnsupportedGrantType extends OAuth2Error { export class UnsupportedGrantType extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('unsupported_grant_type', msg, 400) super('unsupported_grant_type', msg, 400)
@ -84,7 +84,7 @@ class UnsupportedGrantType extends OAuth2Error {
} }
} }
class UnsupportedResponseType extends OAuth2Error { export class UnsupportedResponseType extends OAuth2Error {
constructor (msg) { constructor (msg) {
super('unsupported_response_type', msg, 400) super('unsupported_response_type', msg, 400)
@ -92,16 +92,3 @@ class UnsupportedResponseType extends OAuth2Error {
this.logLevel = 'info' this.logLevel = 'info'
} }
} }
module.exports = {
OAuth2Error: OAuth2Error,
AccessDenied: AccessDenied,
InvalidClient: InvalidClient,
InvalidGrant: InvalidGrant,
InvalidRequest: InvalidRequest,
InvalidScope: InvalidScope,
ServerError: ServerError,
UnauthorizedClient: UnauthorizedClient,
UnsupportedGrantType: UnsupportedGrantType,
UnsupportedResponseType: UnsupportedResponseType
}

View File

@ -1,9 +1,9 @@
import middleware from './middleware' import * as controller from './controller'
import controller from './controller' import { middleware } from './middleware'
import decision from './controller/decision' import { decision } from './controller/decision'
import model from './model' import * as model from './model'
class OAuth2Provider { export class OAuth2Provider {
constructor () { constructor () {
this.bearer = middleware this.bearer = middleware
this.controller = controller this.controller = controller
@ -19,5 +19,3 @@ class OAuth2Provider {
} }
} }
} }
module.exports = OAuth2Provider

View File

@ -1,7 +1,7 @@
import error from './error' import { AccessDenied, Forbidden } from './error'
import wrap from './wrap' import wrap from './wrap'
const middleware = wrap(async function (req, res, next) { export const middleware = wrap(async function (req, res, next) {
console.debug('Parsing bearer token') console.debug('Parsing bearer token')
let token = null let token = null
@ -11,12 +11,12 @@ const middleware = wrap(async function (req, res, next) {
// Check authorization header // Check authorization header
if (!pieces || pieces.length !== 2) { if (!pieces || pieces.length !== 2) {
throw new error.AccessDenied('Wrong authorization header') throw new AccessDenied('Wrong authorization header')
} }
// Only bearer auth is supported // Only bearer auth is supported
if (pieces[0].toLowerCase() !== 'bearer') { if (pieces[0].toLowerCase() !== 'bearer') {
throw new error.AccessDenied('Unsupported authorization method in header') throw new AccessDenied('Unsupported authorization method in header')
} }
token = pieces[1] token = pieces[1]
@ -28,20 +28,18 @@ const middleware = wrap(async function (req, res, next) {
token = req.body.access_token token = req.body.access_token
console.debug('Bearer token parsed from body params:', token) console.debug('Bearer token parsed from body params:', token)
} else { } else {
throw new error.AccessDenied('Bearer token not found') throw new AccessDenied('Bearer token not found')
} }
// Try to fetch access token // Try to fetch access token
const object = await req.oauth2.model.accessToken.fetchByToken(token) const object = await req.oauth2.model.accessToken.fetchByToken(token)
if (!object) { if (!object) {
throw new error.Forbidden('Token not found or has expired') throw new Forbidden('Token not found or has expired')
} else if (!req.oauth2.model.accessToken.checkTTL(object)) { } else if (!req.oauth2.model.accessToken.checkTTL(object)) {
throw new error.Forbidden('Token is expired') throw new Forbidden('Token is expired')
} else { } else {
req.oauth2.accessToken = object req.oauth2.accessToken = object
console.debug('AccessToken fetched', object) console.debug('AccessToken fetched', object)
next() next()
} }
}) })
module.exports = middleware

View File

@ -1,15 +1,15 @@
import config from '../../../scripts/load-config' import config from '../../../scripts/load-config'
import Models from '../models' import * as Models from '../models'
import Users from '../index' import { User, Login } from '../index'
import crypto from 'crypto' import crypto from 'crypto'
const OAuthDB = { export const scopes = {
scopes: {
email: ['See your Email address', 'View the user\'s email address'], email: ['See your Email address', 'View the user\'s email address'],
image: ['', 'View the user\'s profile picture'], image: ['', 'View the user\'s profile picture'],
privilege: ['', 'See the user\'s privilege level'] privilege: ['', 'See the user\'s privilege level']
}, }
accessToken: {
export const accessToken = {
ttl: config.oauth2.access_token_life, ttl: config.oauth2.access_token_life,
getToken: (object) => { getToken: (object) => {
if (object) return object.token if (object) return object.token
@ -65,34 +65,35 @@ const OAuthDB = {
return tkn[0] return tkn[0]
} }
}, }
client: {
getId: (client) => { export const client = {
return client.id getId: (c) => {
return c.id
}, },
fetchById: async (id) => { fetchById: async (id) => {
const client = await Models.OAuth2Client.query().where('id', id) const c = await Models.OAuth2Client.query().where('id', id)
if (!client.length) return null if (!c.length) return null
return client[0] return c[0]
}, },
checkSecret: (client, secret) => { checkSecret: (c, secret) => {
return client.secret === secret return c.secret === secret
}, },
checkGrantType: (client, grant) => { checkGrantType: (c, grant) => {
if (client.grants.indexOf(grant) !== -1) { if (c.grants.indexOf(grant) !== -1) {
return true return true
} }
return false return false
}, },
getRedirectUri: (client) => { getRedirectUri: (c) => {
return client.redirect_url return c.redirect_url
}, },
checkRedirectUri: (client, redirectUri) => { checkRedirectUri: (c, redirectUri) => {
return (redirectUri.indexOf(OAuthDB.client.getRedirectUri(client)) === 0 && return (redirectUri.indexOf(client.getRedirectUri(c)) === 0 &&
redirectUri.replace(OAuthDB.client.getRedirectUri(client), '').indexOf('#') === -1) redirectUri.replace(client.getRedirectUri(c), '').indexOf('#') === -1)
}, },
transformScope: (scope) => { transformScope: (scope) => {
if (!scope) return [] if (!scope) return []
@ -109,13 +110,13 @@ const OAuthDB = {
return scope return scope
}, },
checkScope: (client, scope) => { checkScope: (c, scope) => {
if (!scope) return [] if (!scope) return []
if (typeof scope === 'string') { if (typeof scope === 'string') {
scope = OAuthDB.client.transformScope(scope) scope = c.transformScope(scope)
} }
const clientScopes = client.scope.split(' ') const clientScopes = c.scope.split(' ')
for (const i in scope) { for (const i in scope) {
if (clientScopes.indexOf(scope[i]) === -1) { if (clientScopes.indexOf(scope[i]) === -1) {
@ -125,11 +126,12 @@ const OAuthDB = {
return scope return scope
} }
}, }
code: {
export const code = {
ttl: config.oauth2.code_life, ttl: config.oauth2.code_life,
create: async (userId, clientId, scope, ttl, special = false) => { create: async (userId, clientId, scope, ttl, special = false) => {
const code = crypto.randomBytes(config.oauth2.token_length).toString('hex') const newCode = crypto.randomBytes(config.oauth2.token_length).toString('hex')
const expr = new Date(Date.now() + ttl * 1000) const expr = new Date(Date.now() + ttl * 1000)
if (typeof scope === 'object') { if (typeof scope === 'object') {
@ -141,7 +143,7 @@ const OAuthDB = {
await Models.OAuth2Code.query().delete().where('user_id', userId).andWhere('client_id', clientId) await Models.OAuth2Code.query().delete().where('user_id', userId).andWhere('client_id', clientId)
const obj = { const obj = {
code, code: newCode,
scope, scope,
user_id: userId, user_id: userId,
client_id: clientId, client_id: clientId,
@ -153,34 +155,35 @@ const OAuthDB = {
return obj.code return obj.code
}, },
fetchByCode: async (code) => { fetchByCode: async (c) => {
code = await Models.OAuth2Code.query().where('code', code) c = await Models.OAuth2Code.query().where('code', c)
if (!code.length) return null if (!c.length) return null
return code[0] return c[0]
}, },
removeByCode: async (code) => { removeByCode: async (c) => {
if (typeof code === 'object') { if (typeof c === 'object') {
code = code.code c = c.code
} }
return Models.OAuth2Code.query().delete().where('code', code) return Models.OAuth2Code.query().delete().where('code', c)
}, },
getUserId: (code) => { getUserId: (c) => {
return code.user_id return c.user_id
}, },
getClientId: (code) => { getClientId: (c) => {
return code.client_id return c.client_id
}, },
getScope: (code) => { getScope: (c) => {
return code.scope return c.scope
}, },
checkTTL: (code) => { checkTTL: (c) => {
return (code.expires_at > Date.now()) return (c.expires_at > Date.now())
} }
}, }
refreshToken: {
export const refreshToken = {
create: async (userId, clientId, scope) => { create: async (userId, clientId, scope) => {
const token = crypto.randomBytes(config.oauth2.token_length).toString('hex') const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
@ -214,26 +217,27 @@ const OAuthDB = {
removeByRefreshToken: async (token) => { removeByRefreshToken: async (token) => {
return Models.OAuth2RefreshToken.query().delete().where('token', token) return Models.OAuth2RefreshToken.query().delete().where('token', token)
}, },
getUserId: (refreshToken) => { getUserId: (t) => {
return refreshToken.user_id return t.user_id
}, },
getClientId: (refreshToken) => { getClientId: (t) => {
return refreshToken.client_id return t.client_id
}, },
getScope: (refreshToken) => { getScope: (t) => {
return refreshToken.scope return t.scope
} }
}
export const user = {
getId: (u) => {
return u.id
}, },
user: { fetchById: User.get,
getId: (user) => { fetchByUsername: User.get,
return user.id checkPassword: Login.password,
},
fetchById: Users.User.get,
fetchByUsername: Users.User.get,
checkPassword: Users.User.Login.password,
fetchFromRequest: async (req) => { fetchFromRequest: async (req) => {
if (!req.session.user) return null if (!req.session.user) return null
const banStatus = await Users.User.getBanStatus(req.session.user.id) const banStatus = await User.getBanStatus(req.session.user.id)
if (banStatus.length) { if (banStatus.length) {
delete req.session.user delete req.session.user
@ -288,6 +292,3 @@ const OAuthDB = {
return true return true
} }
} }
}
module.exports = OAuthDB

View File

@ -1,7 +1,7 @@
import query from 'querystring' import query from 'querystring'
import error from './error' import { OAuth2Error, ServerError } from './error'
function data (req, res, code, data) { function dataRes (req, res, code, data) {
res.header('Cache-Control', 'no-store') res.header('Cache-Control', 'no-store')
res.header('Pragma', 'no-cache') res.header('Pragma', 'no-cache')
res.status(code).send(data) res.status(code).send(data)
@ -14,11 +14,11 @@ function redirect (req, res, redirectUri) {
console.debug('Redirecting to ', redirectUri) console.debug('Redirecting to ', redirectUri)
} }
module.exports.error = function (req, res, err, redirectUri) { export function error (req, res, err, redirectUri) {
// Transform unknown error // Transform unknown error
if (!(err instanceof error.OAuth2Error)) { if (!(err instanceof OAuth2Error)) {
console.error(err.stack) console.error(err.stack)
err = new error.ServerError('Uncaught exception') err = new ServerError('Uncaught exception')
} else { } else {
console.error('Exception caught', err.stack) console.error('Exception caught', err.stack)
} }
@ -36,11 +36,11 @@ module.exports.error = function (req, res, err, redirectUri) {
redirectUri += '?' + query.stringify(obj) redirectUri += '?' + query.stringify(obj)
redirect(req, res, redirectUri) redirect(req, res, redirectUri)
} else { } else {
data(req, res, err.status, { error: err.code, error_description: err.message }) dataRes(req, res, err.status, { error: err.code, error_description: err.message })
} }
} }
module.exports.data = function (req, res, obj, redirectUri, fragment) { export function data (req, res, obj, redirectUri, fragment) {
if (redirectUri) { if (redirectUri) {
if (fragment) { if (fragment) {
redirectUri += '#' redirectUri += '#'
@ -55,6 +55,6 @@ module.exports.data = function (req, res, obj, redirectUri, fragment) {
redirectUri += query.stringify(obj) redirectUri += query.stringify(obj)
redirect(req, res, redirectUri) redirect(req, res, redirectUri)
} else { } else {
data(req, res, 200, obj) dataRes(req, res, 200, obj)
} }
} }

View File

@ -1,5 +1,5 @@
import response from './response' import { error } from './response'
module.exports = (fn, redir) => (req, res, next) => export default (fn, redir) => (req, res, next) =>
fn(req, res, next).catch(e => fn(req, res, next).catch(e =>
response.error(req, res, e, redir ? req.query.redirect_uri : null)) error(req, res, e, redir ? req.query.redirect_uri : null))

View File

@ -1,10 +1,10 @@
import express from 'express' import express from 'express'
import ensureLogin from '../../scripts/ensureLogin' import { ensureLogin } from '../../scripts/ensureLogin'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import API from '../api/admin' import * as API from '../api/admin'
import Emailer from '../api/emailer' import { sendMail } from '../api/emailer'
import { User } from '../api' import { User, Login } from '../api'
const router = express.Router() const router = express.Router()
const apiRouter = express.Router() const apiRouter = express.Router()
@ -46,7 +46,7 @@ router.post('/', wrap(async (req, res, next) => {
return next() return next()
} }
const passReady = await User.Login.password(req.session.user, req.body.password) const passReady = await Login.password(req.session.user, req.body.password)
if (passReady) { if (passReady) {
req.session.accesstime = Date.now() + 600000 // 10 minutes req.session.accesstime = Date.now() + 600000 // 10 minutes
return res.redirect('/admin') return res.redirect('/admin')
@ -299,7 +299,7 @@ apiRouter.post('/email', csrfVerify, wrap(async (req, res) => {
html: req.body.content html: req.body.content
} }
const result = await Emailer.sendMail(req.body.email, message) const result = await sendMail(req.body.email, message)
res.jsonp(result) res.jsonp(result)
})) }))
@ -311,4 +311,4 @@ apiRouter.use((err, req, res, next) => {
router.use('/api', apiRouter) router.use('/api', apiRouter)
module.exports = router export default router

View File

@ -4,10 +4,10 @@ import multiparty from 'multiparty'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import APIExtern from '../api/external' import { Common, Facebook, Twitter, Discord, Google } from '../api/external'
import Image from '../api/image' import * as Image from '../api/image'
import News from '../api/news' import * as News from '../api/news'
import API from '../api' import { User, Payment, OAuth2 } from '../api'
const router = express.Router() const router = express.Router()
const dev = process.env.NODE_ENV !== 'production' const dev = process.env.NODE_ENV !== 'production'
@ -84,7 +84,7 @@ function JsonData (req, res, error, redirect = '/') {
function removeAuthMiddleware (identifier) { function removeAuthMiddleware (identifier) {
return wrap(async (req, res) => { return wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login') if (!req.session.user) return res.redirect('/login')
const done = await APIExtern.Common.remove(req.session.user, identifier) const done = await Common.remove(req.session.user, identifier)
if (!done) { if (!done) {
req.flash('message', { error: true, text: 'Unable to unlink social media account' }) req.flash('message', { error: true, text: 'Unable to unlink social media account' })
@ -107,7 +107,7 @@ router.post('/external/facebook/callback', wrap(async (req, res, next) => {
return next() return next()
} }
const response = await APIExtern.Facebook.callback(req.session.user, sane.authResponse, req.realIP) const response = await Facebook.callback(req.session.user, sane.authResponse, req.realIP)
if (response.banned) { if (response.banned) {
return JsonData(req, res, 'You are banned.') return JsonData(req, res, 'You are banned.')
@ -134,7 +134,7 @@ router.get('/external/facebook/remove', removeAuthMiddleware('facebook'))
*/ */
router.get('/external/twitter/login', wrap(async (req, res) => { router.get('/external/twitter/login', wrap(async (req, res) => {
if (!config.external || !config.external.twitter || !config.external.twitter.api) return res.redirect('/') if (!config.external || !config.external.twitter || !config.external.twitter.api) return res.redirect('/')
const tokens = await APIExtern.Twitter.getRequestToken() const tokens = await Twitter.getRequestToken()
if (tokens.error) { if (tokens.error) {
return res.jsonp({ error: tokens.error }) return res.jsonp({ error: tokens.error })
@ -156,7 +156,7 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
return res.redirect(uri) return res.redirect(uri)
} }
const accessTokens = await APIExtern.Twitter.getAccessTokens(ta.token, ta.token_secret, req.query.oauth_verifier) const accessTokens = await Twitter.getAccessTokens(ta.token, ta.token_secret, req.query.oauth_verifier)
delete req.session.twitter_auth delete req.session.twitter_auth
if (accessTokens.error) { if (accessTokens.error) {
@ -164,7 +164,7 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
return res.redirect(uri) return res.redirect(uri)
} }
const response = await APIExtern.Twitter.callback(req.session.user, accessTokens, req.realIP) const response = await Twitter.callback(req.session.user, accessTokens, req.realIP)
if (response.banned) { if (response.banned) {
return res.render('user/banned', { bans: response.banned, ipban: response.ip }) return res.render('user/banned', { bans: response.banned, ipban: response.ip })
} }
@ -191,7 +191,7 @@ router.get('/external/twitter/remove', removeAuthMiddleware('twitter'))
router.get('/external/discord/login', wrap(async (req, res) => { router.get('/external/discord/login', wrap(async (req, res) => {
if (!config.external || !config.external.discord || !config.external.discord.api) return res.redirect('/') if (!config.external || !config.external.discord || !config.external.discord.api) return res.redirect('/')
const infos = APIExtern.Discord.getAuthorizeURL(req) const infos = Discord.getAuthorizeURL(req)
res.redirect(infos.url) res.redirect(infos.url)
})) }))
@ -208,20 +208,20 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
return res.redirect(uri) return res.redirect(uri)
} }
if (!state || state !== APIExtern.Common.stateGenerator(req)) { if (!state || state !== Common.stateGenerator(req)) {
req.flash('message', { error: true, text: 'Request got intercepted, try again.' }) req.flash('message', { error: true, text: 'Request got intercepted, try again.' })
return res.redirect(uri) return res.redirect(uri)
} }
delete req.session.discord_auth delete req.session.discord_auth
const accessToken = await APIExtern.Discord.getAccessToken(code) const accessToken = await Discord.getAccessToken(code)
if (accessToken.error) { if (accessToken.error) {
req.flash('message', { error: true, text: accessToken.error }) req.flash('message', { error: true, text: accessToken.error })
return res.redirect(uri) return res.redirect(uri)
} }
const response = await APIExtern.Discord.callback(req.session.user, accessToken.accessToken, req.realIP) const response = await Discord.callback(req.session.user, accessToken.accessToken, req.realIP)
if (response.banned) { if (response.banned) {
return res.render('user/banned', { bans: response.banned, ipban: response.ip }) return res.render('user/banned', { bans: response.banned, ipban: response.ip })
} }
@ -258,7 +258,7 @@ router.post('/external/google/callback', wrap(async (req, res) => {
return JsonData(req, res, 'Invalid or missing ID token!', '/login') return JsonData(req, res, 'Invalid or missing ID token!', '/login')
} }
const response = await APIExtern.Google.callback(req.session.user, req.body, req.realIP) const response = await Google.callback(req.session.user, req.body, req.realIP)
if (response.banned) { if (response.banned) {
return JsonData(req, res, 'You are banned.', '/login') return JsonData(req, res, 'You are banned.', '/login')
} }
@ -371,7 +371,7 @@ router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => {
try { try {
const result = await Image.uploadImage(req.session.user.username, data.fields, data.files) const result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
avatarFile = await API.User.changeAvatar(req.session.user, result.file) avatarFile = await User.changeAvatar(req.session.user, result.file)
} catch (e) { } catch (e) {
return res.status(400).jsonp({ error: e.message }) return res.status(400).jsonp({ error: e.message })
} }
@ -388,7 +388,7 @@ router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => {
router.post('/avatar/remove', wrap(async (req, res, next) => { router.post('/avatar/remove', wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
await API.User.removeAvatar(req.session.user) await User.removeAvatar(req.session.user)
req.session.user.avatar_file = null req.session.user.avatar_file = null
res.status(200).jsonp({ done: true }) res.status(200).jsonp({ done: true })
@ -410,7 +410,7 @@ router.get('/avatar/:id', wrap(async (req, res, next) => {
const id = idParam(req) const id = idParam(req)
if (!id) return next() if (!id) return next()
const user = await API.User.get(id) const user = await User.get(id)
if (!user || !user.avatar_file) return next() if (!user || !user.avatar_file) return next()
@ -432,12 +432,13 @@ router.post('/avatar/gravatar', wrap(async (req, res, next) => {
try { try {
const gravURL = await Image.downloadImage(Image.gravatarURL(user.email), 'GRAV-' + user.username) const gravURL = await Image.downloadImage(Image.gravatarURL(user.email), 'GRAV-' + user.username)
const file = await API.User.changeAvatar(user, gravURL) const file = await User.changeAvatar(user, gravURL)
req.session.user.avatar_file = file req.session.user.avatar_file = file
req.flash('message', { error: false, text: 'Success!' }) req.flash('message', { error: false, text: 'Success!' })
} catch (e) { } catch (e) {
console.error(e);
req.flash('message', { error: true, text: 'Failed to use gravatar avatar.' }) req.flash('message', { error: true, text: 'Failed to use gravatar avatar.' })
} }
@ -458,7 +459,7 @@ router.use('/avatar', (req, res) => {
router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => { router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
const list = await API.User.OAuth2.getUserAuthorizations(req.session.user) const list = await OAuth2.getUserAuthorizations(req.session.user)
if (!list) return next() if (!list) return next()
res.jsonp(list) res.jsonp(list)
@ -471,7 +472,7 @@ router.post('/oauth2/authorized-clients/revoke', wrap(async (req, res, next) =>
const clientId = parseInt(req.body.client_id) const clientId = parseInt(req.body.client_id)
if (isNaN(clientId)) return res.status(400).jsonp({ error: 'Missing Client ID parameter' }) if (isNaN(clientId)) return res.status(400).jsonp({ error: 'Missing Client ID parameter' })
const done = await API.User.OAuth2.removeUserAuthorization(req.session.user, clientId) const done = await OAuth2.removeUserAuthorization(req.session.user, clientId)
if (!done) return res.status(400).jsonp({ error: 'Failed to remove client authorization' }) if (!done) return res.status(400).jsonp({ error: 'Failed to remove client authorization' })
res.status(204).end() res.status(204).end()
@ -486,7 +487,7 @@ router.post('/paypal/ipn', wrap(async (req, res) => {
const content = req.body const content = req.body
if (content && content.payment_status && content.payment_status === 'Completed') { if (content && content.payment_status && content.payment_status === 'Completed') {
await API.Payment.handleIPN(content) await Payment.handleIPN(content)
} }
res.status(204).end() res.status(204).end()
@ -495,7 +496,7 @@ router.post('/paypal/ipn', wrap(async (req, res) => {
router.get('/donations/user', wrap(async (req, res, next) => { router.get('/donations/user', wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
const contribs = await API.Payment.userContributions(req.session.user) const contribs = await Payment.userContributions(req.session.user)
res.jsonp(contribs) res.jsonp(contribs)
})) }))
@ -513,7 +514,7 @@ router.get('/donations', wrap(async (req, res, next) => {
let timeFrame = parseInt(req.query.timeFrame) let timeFrame = parseInt(req.query.timeFrame)
if (isNaN(timeFrame)) timeFrame = 0 if (isNaN(timeFrame)) timeFrame = 0
const contribs = await API.Payment.allContributions(count, mcu, timeFrame) const contribs = await Payment.allContributions(count, mcu, timeFrame)
res.jsonp(contribs) res.jsonp(contribs)
})) }))
@ -527,4 +528,4 @@ router.use((err, req, res, next) => {
res.jsonp({ error: 'Internal server error.' }) res.jsonp({ error: 'Internal server error.' })
}) })
module.exports = router export default router

View File

@ -3,13 +3,13 @@ import path from 'path'
import express from 'express' import express from 'express'
import RateLimit from 'express-rate-limit' import RateLimit from 'express-rate-limit'
import ensureLogin from '../../scripts/ensureLogin' import { ensureLogin } from '../../scripts/ensureLogin'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import http from '../../scripts/http' import { httpPOST } from '../../scripts/http'
import API from '../api' import { User, Login, Reset, Register } from '../api'
import News from '../api/news' import * as News from '../api/news'
import emailer from '../api/emailer' import { pushMail } from '../api/emailer'
import apiRouter from './api' import apiRouter from './api'
import oauthRouter from './oauth2' import oauthRouter from './oauth2'
@ -70,7 +70,7 @@ router.use(wrap(async (req, res, next) => {
console.debug('User session update') console.debug('User session update')
// Check for bans // Check for bans
const banStatus = await API.User.getBanStatus(req.session.user.id) const banStatus = await User.getBanStatus(req.session.user.id)
if (banStatus.length) { if (banStatus.length) {
delete req.session.user delete req.session.user
@ -78,11 +78,11 @@ router.use(wrap(async (req, res, next) => {
} }
// Update user session // Update user session
const udata = await API.User.get(req.session.user) const udata = await User.get(req.session.user)
setSession(req, udata) setSession(req, udata)
// Update IP address // Update IP address
await API.User.update(udata, { ip_address: req.realIP }) await User.update(udata, { ip_address: req.realIP })
} }
} }
@ -156,7 +156,7 @@ router.get('/login/reset', extraButtons(false), (req, res) => {
router.get('/reset/:token', wrap(async (req, res) => { router.get('/reset/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login') if (req.session.user) return res.redirect('/login')
const token = req.params.token const token = req.params.token
const success = await API.User.Reset.resetToken(token) const success = await Reset.resetToken(token)
if (!success) { if (!success) {
req.flash('message', { error: true, text: 'Invalid or expired reset token.' }) req.flash('message', { error: true, text: 'Invalid or expired reset token.' })
@ -167,7 +167,7 @@ router.get('/reset/:token', wrap(async (req, res) => {
res.render('user/password_new', { token: true }) res.render('user/password_new', { token: true })
})) }))
router.get('/login', extraButtons(false), (req, res) => { router.get('/login', extraButtons(false), formKeep, (req, res) => {
if (req.session.user) return redirectLogin(req, res) if (req.session.user) return redirectLogin(req, res)
if (req.query.returnTo) { if (req.query.returnTo) {
@ -191,7 +191,7 @@ router.get('/register', extraButtons(true), formKeep, (req, res) => {
router.get('/activate/:token', wrap(async (req, res) => { router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login') if (req.session.user) return res.redirect('/login')
const token = req.params.token const token = req.params.token
const success = await API.User.Login.activationToken(token) const success = await Login.activationToken(token)
if (!success) { if (!success) {
req.flash('message', { error: true, text: 'Invalid or expired activation token.' }) req.flash('message', { error: true, text: 'Invalid or expired activation token.' })
@ -204,10 +204,10 @@ router.get('/activate/:token', wrap(async (req, res) => {
// View for enabling Two-Factor Authentication // View for enabling Two-Factor Authentication
router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => { router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
const twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user) const twoFaEnabled = await Login.totpTokenRequired(req.session.user)
if (twoFaEnabled) return res.redirect('/') if (twoFaEnabled) return res.redirect('/')
const newToken = await API.User.Login.totpAquire(req.session.user) const newToken = await Login.totpAquire(req.session.user)
if (!newToken) return res.redirect('/') if (!newToken) return res.redirect('/')
res.render('user/totp', { uri: newToken }) res.render('user/totp', { uri: newToken })
@ -215,7 +215,7 @@ router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
// View for disabling Two-Factor Authentication // View for disabling Two-Factor Authentication
router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => { router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
const twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user) const twoFaEnabled = await Login.totpTokenRequired(req.session.user)
if (!twoFaEnabled) return res.redirect('/') if (!twoFaEnabled) return res.redirect('/')
res.render('user/password') res.render('user/password')
@ -229,10 +229,10 @@ router.get('/login/verify', (req, res) => {
// User settings page // User settings page
router.get('/user/manage', ensureLogin, wrap(async (req, res) => { router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
let totpEnabled = false let totpEnabled = false
const socialStatus = await API.User.socialStatus(req.session.user) const socialStatus = await User.socialStatus(req.session.user)
if (socialStatus.password) { if (socialStatus.password) {
totpEnabled = await API.User.Login.totpTokenRequired(req.session.user) totpEnabled = await Login.totpTokenRequired(req.session.user)
} }
const et = config.external const et = config.external
@ -277,7 +277,7 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
// Change password // Change password
router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => { router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => {
const socialStatus = await API.User.socialStatus(req.session.user) const socialStatus = await User.socialStatus(req.session.user)
res.render('user/password_new', { token: !socialStatus.password }) res.render('user/password_new', { token: !socialStatus.password })
})) }))
@ -291,7 +291,7 @@ router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
obfuscated = rep + '@' + split[1] obfuscated = rep + '@' + split[1]
} }
const socialStatus = await API.User.socialStatus(req.session.user) const socialStatus = await User.socialStatus(req.session.user)
res.render('user/email_change', { email: obfuscated, password: socialStatus.password }) res.render('user/email_change', { email: obfuscated, password: socialStatus.password })
})) }))
@ -350,7 +350,7 @@ router.post('/user/two-factor', csrfValidation, wrap(async (req, res, next) => {
return formError(req, res, 'You need to enter the code.') return formError(req, res, 'You need to enter the code.')
} }
const verified = await API.User.Login.totpCheck(req.session.user, req.body.code) const verified = await Login.totpCheck(req.session.user, req.body.code)
if (!verified) { if (!verified) {
return formError(req, res, 'Something went wrong! Try scanning the code again.') return formError(req, res, 'Something went wrong! Try scanning the code again.')
} }
@ -366,7 +366,7 @@ router.post('/user/two-factor/disable', csrfValidation, wrap(async (req, res, ne
return formError(req, res, 'Please enter your password.') return formError(req, res, 'Please enter your password.')
} }
const purge = await API.User.Login.purgeTotp(req.session.user, req.body.password) const purge = await Login.purgeTotp(req.session.user, req.body.password)
if (!purge) { if (!purge) {
return formError(req, res, 'Invalid password.') return formError(req, res, 'Invalid password.')
} }
@ -384,12 +384,12 @@ router.post('/login/verify', csrfValidation, wrap(async (req, res, next) => {
return formError(req, res, 'You need to enter the code.') return formError(req, res, 'You need to enter the code.')
} }
const totpCheck = await API.User.Login.totpCheck(req.session.totp_check, req.body.code, req.body.recovery || false) const totpCheck = await Login.totpCheck(req.session.totp_check, req.body.code, req.body.recovery || false)
if (!totpCheck) { if (!totpCheck) {
return formError(req, res, 'Invalid code!') return formError(req, res, 'Invalid code!')
} }
const user = await API.User.get(req.session.totp_check) const user = await User.get(req.session.totp_check)
delete req.session.totp_check delete req.session.totp_check
setSession(req, user) setSession(req, user)
@ -404,12 +404,12 @@ router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next
return res.redirect('/login') return res.redirect('/login')
} }
const user = await API.User.get(req.body.username) const user = await User.get(req.body.username)
if (!user) return formError(req, res, 'Invalid username or password.') if (!user) return formError(req, res, 'Invalid username or password.')
if (!user.password || user.password === '') return formError(req, res, 'Please log in using the buttons on the right.') if (!user.password || user.password === '') return formError(req, res, 'Please log in using the buttons on the right.')
const pwMatch = await API.User.Login.password(user, req.body.password) const pwMatch = await Login.password(user, req.body.password)
if (!pwMatch) return formError(req, res, 'Invalid username or password.') if (!pwMatch) return formError(req, res, 'Invalid username or password.')
if (user.activated === 0) return formError(req, res, 'Please activate your account first. If you did not receive an email, please contact an administrator.') if (user.activated === 0) return formError(req, res, 'Please activate your account first. If you did not receive an email, please contact an administrator.')
@ -417,13 +417,13 @@ router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next
if (user.locked === 1) return formError(req, res, 'This account has been locked. Please contact an administrator for more information.') if (user.locked === 1) return formError(req, res, 'This account has been locked. Please contact an administrator for more information.')
// Check if the user is banned // Check if the user is banned
const banStatus = await API.User.getBanStatus(user.id) const banStatus = await User.getBanStatus(user.id)
if (banStatus.length) { if (banStatus.length) {
return res.render('user/banned', { bans: banStatus, ipban: false }) return res.render('user/banned', { bans: banStatus, ipban: false })
} }
// Redirect to the verification dialog if 2FA is enabled // Redirect to the verification dialog if 2FA is enabled
const totpRequired = await API.User.Login.totpTokenRequired(user) const totpRequired = await Login.totpTokenRequired(user)
if (totpRequired) { if (totpRequired) {
req.session.totp_check = user.id req.session.totp_check = user.id
return res.redirect('/login/verify') return res.redirect('/login/verify')
@ -454,13 +454,13 @@ router.post('/login/reset', accountLimiter, csrfValidation, wrap(async (req, res
} }
const email = req.body.email const email = req.body.email
const validEmail = await API.User.Register.validateEmail(email) const validEmail = await Register.validateEmail(email)
if (!validEmail) { if (!validEmail) {
return formError(req, res, 'You need to enter a valid email address.') return formError(req, res, 'You need to enter a valid email address.')
} }
try { try {
await API.User.Reset.reset(email, false) await Reset.reset(email, false)
req.flash('message', { error: false, text: 'We\'ve sent a link to your email address. Please check spam folders, too!' }) req.flash('message', { error: false, text: 'We\'ve sent a link to your email address. Please check spam folders, too!' })
res.redirect('/login/reset?success=true') res.redirect('/login/reset?success=true')
@ -473,7 +473,7 @@ router.post('/login/reset', accountLimiter, csrfValidation, wrap(async (req, res
router.post('/reset/:token', csrfValidation, wrap(async (req, res) => { router.post('/reset/:token', csrfValidation, wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login') if (req.session.user) return res.redirect('/login')
const token = req.params.token const token = req.params.token
const user = await API.User.Reset.resetToken(token) const user = await Reset.resetToken(token)
if (!user) { if (!user) {
req.flash('message', { error: true, text: 'Invalid or expired reset token.' }) req.flash('message', { error: true, text: 'Invalid or expired reset token.' })
@ -494,7 +494,7 @@ router.post('/reset/:token', csrfValidation, wrap(async (req, res) => {
} }
try { try {
await API.User.Reset.changePassword(user, password, token) await Reset.changePassword(user, password, token)
console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP) console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP)
@ -514,7 +514,7 @@ router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, n
} }
// Ban check // Ban check
const banStatus = await API.User.getBanStatus(req.realIP, true) const banStatus = await User.getBanStatus(req.realIP, true)
if (banStatus.length) { if (banStatus.length) {
return res.render('user/banned', { bans: banStatus, ipban: true }) return res.render('user/banned', { bans: banStatus, ipban: true })
} }
@ -533,7 +533,7 @@ router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, n
// 3rd Check: Email Address // 3rd Check: Email Address
const email = req.body.email const email = req.body.email
if (!email || !API.User.Register.validateEmail(email)) { if (!email || !Register.validateEmail(email)) {
return formError(req, res, 'Invalid email address!') return formError(req, res, 'Invalid email address!')
} }
@ -554,7 +554,7 @@ router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, n
if (!req.body['g-recaptcha-response']) return formError(req, res, 'Please complete the reCAPTCHA!') if (!req.body['g-recaptcha-response']) return formError(req, res, 'Please complete the reCAPTCHA!')
try { try {
let data = await http.POST('https://www.google.com/recaptcha/api/siteverify', {}, { let data = await httpPOST('https://www.google.com/recaptcha/api/siteverify', {}, {
secret: config.security.recaptcha.secret_key, secret: config.security.recaptcha.secret_key,
response: req.body['g-recaptcha-response'] response: req.body['g-recaptcha-response']
}) })
@ -570,12 +570,12 @@ router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, n
} }
// Hash the password // Hash the password
const hash = await API.User.Register.hashPassword(password) const hash = await Register.hashPassword(password)
let newUser let newUser
// Attempt to create the user // Attempt to create the user
try { try {
newUser = await API.User.Register.newAccount({ newUser = await Register.newAccount({
username: username, username: username,
display_name: cleanString(displayName), display_name: cleanString(displayName),
password: hash, password: hash,
@ -617,7 +617,7 @@ router.post('/user/manage', csrfValidation, wrap(async (req, res, next) => {
} }
try { try {
await API.User.update(req.session.user, { await User.update(req.session.user, {
display_name: displayName display_name: displayName
}) })
} catch (e) { } catch (e) {
@ -634,14 +634,14 @@ router.post('/user/manage', csrfValidation, wrap(async (req, res, next) => {
router.post('/user/manage/password', accountLimiter, csrfValidation, wrap(async (req, res, next) => { router.post('/user/manage/password', accountLimiter, csrfValidation, wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
const user = req.session.user const user = req.session.user
const socialStatus = await API.User.socialStatus(user) const socialStatus = await User.socialStatus(user)
if (!req.body.password_old && socialStatus.password) { if (!req.body.password_old && socialStatus.password) {
return formError(req, res, 'Please enter your current password.') return formError(req, res, 'Please enter your current password.')
} }
if (socialStatus.password) { if (socialStatus.password) {
const passwordMatch = await API.User.Login.password(user, req.body.password_old) const passwordMatch = await Login.password(user, req.body.password_old)
if (!passwordMatch) { if (!passwordMatch) {
return formError(req, res, 'The password you provided is incorrect.') return formError(req, res, 'The password you provided is incorrect.')
} }
@ -657,10 +657,10 @@ router.post('/user/manage/password', accountLimiter, csrfValidation, wrap(async
return formError(req, res, 'The passwords do not match!') return formError(req, res, 'The passwords do not match!')
} }
password = await API.User.Register.hashPassword(password) password = await Register.hashPassword(password)
try { try {
await API.User.update(user, { await User.update(user, {
password: password password: password
}) })
} catch (e) { } catch (e) {
@ -670,7 +670,7 @@ router.post('/user/manage/password', accountLimiter, csrfValidation, wrap(async
console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP) console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP)
if (config.email && config.email.enabled) { if (config.email && config.email.enabled) {
await emailer.pushMail('password_alert', user.email, { await pushMail('password_alert', user.email, {
display_name: user.display_name, display_name: user.display_name,
ip: req.realIP ip: req.realIP
}) })
@ -684,7 +684,7 @@ router.post('/user/manage/password', accountLimiter, csrfValidation, wrap(async
router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (req, res, next) => { router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
const user = await API.User.get(req.session.user) const user = await User.get(req.session.user)
const email = req.body.email const email = req.body.email
const newEmail = req.body.email_new const newEmail = req.body.email_new
const password = req.body.password const password = req.body.password
@ -702,24 +702,24 @@ router.post('/user/manage/email', accountLimiter, csrfValidation, wrap(async (re
return formError(req, res, 'Enter a password.') return formError(req, res, 'Enter a password.')
} }
const passwordMatch = await API.User.Login.password(user, password) const passwordMatch = await Login.password(user, password)
if (!passwordMatch) { if (!passwordMatch) {
return formError(req, res, 'The password you provided is incorrect.') return formError(req, res, 'The password you provided is incorrect.')
} }
} }
const emailValid = API.User.Register.validateEmail(newEmail) const emailValid = Register.validateEmail(newEmail)
if (!emailValid) { if (!emailValid) {
return formError(req, res, 'Invalid email address.') return formError(req, res, 'Invalid email address.')
} }
const emailTaken = await API.User.get(newEmail) const emailTaken = await User.get(newEmail)
if (emailTaken) { if (emailTaken) {
return formError(req, res, 'This email is already taken.') return formError(req, res, 'This email is already taken.')
} }
try { try {
await API.User.update(user, { await User.update(user, {
email: newEmail email: newEmail
}) })
} catch (e) { } catch (e) {
@ -883,4 +883,4 @@ router.use((err, req, res, next) => {
next() next()
}) })
module.exports = router export default router

View File

@ -1,13 +1,13 @@
import express from 'express' import express from 'express'
import UAPI from '../api' import { User } from '../api'
import OAuth2 from '../api/oauth2' import { OAuth2Provider } from '../api/oauth2'
import RateLimit from 'express-rate-limit' import RateLimit from 'express-rate-limit'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import config from '../../scripts/load-config.js' import config from '../../scripts/load-config.js'
const router = express.Router() const router = express.Router()
const oauth = new OAuth2() const oauth = new OAuth2Provider()
router.use(oauth.express()) router.use(oauth.express())
@ -33,7 +33,7 @@ router.post('/introspect', oauth.controller.introspection)
// Protected user information resource // Protected user information resource
router.get('/user', oauth.bearer, wrap(async (req, res) => { router.get('/user', oauth.bearer, wrap(async (req, res) => {
const accessToken = req.oauth2.accessToken const accessToken = req.oauth2.accessToken
const user = await UAPI.User.get(accessToken.user_id) const user = await User.get(accessToken.user_id)
if (!user) { if (!user) {
return res.status(404).jsonp({ return res.status(404).jsonp({
@ -75,4 +75,4 @@ router.use((err, req, res, next) => {
next() next()
}) })
module.exports = router export default router

View File

@ -10,7 +10,7 @@ import path from 'path'
import routes from './routes' import routes from './routes'
import flash from '../scripts/flash' import flash from '../scripts/flash'
import config from '../scripts/load-config' import config from '../scripts/load-config'
import email from './api/emailer' import { init as initEmail } from './api/emailer'
const app = express() const app = express()
const SessionStore = connectSession(session) const SessionStore = connectSession(session)
@ -95,6 +95,6 @@ module.exports = (args) => {
console.log('Listening on 0.0.0.0:' + args.port) console.log('Listening on 0.0.0.0:' + args.port)
// Initialize the email transporter (if configured) // Initialize the email transporter (if configured)
email.init() initEmail()
}) })
} }

View File

@ -19,7 +19,7 @@ block body
input(type="hidden", name="csrf", value=csrf) input(type="hidden", name="csrf", value=csrf)
.form-group .form-group
label(for="username") Username or Email Address label(for="username") Username or Email Address
input.form-control(type="text", name="username", id="username", autofocus) input.form-control(type="text", name="username", id="username", value=formkeep.username, autofocus)
.form-group .form-group
label(for="password") Password label(for="password") Password
input.form-control(type="password", name="password", id="password") input.form-control(type="password", name="password", id="password")

View File

@ -13,6 +13,9 @@ module.exports = {
resolve: { resolve: {
alias: { alias: {
vue$: 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1 vue$: 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
},
fallback: {
querystring: require.resolve('querystring-es3')
} }
}, },
module: { module: {