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

7366
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,257 +82,269 @@ function dataFilter (data, fields, optional = []) {
return data return data
} }
const API = { // List all users (paginated)
// List all users (paginated) export async function getAllUsers (page, adminId) {
getAllUsers: async function (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) {
const entry = raw[i] const entry = raw[i]
users.push(await cleanUserObject(entry, admin)) users.push(await cleanUserObject(entry, admin))
} }
return { return {
page: paginated, page: paginated,
users: users users: users
}
},
getUser: async function (id) {
const user = await Users.User.get(id)
if (!user) throw new Error('No such user')
return cleanUserObject(user, null)
},
editUser: async function (id, data) {
const user = await Users.User.get(id)
if (!user) throw new Error('No such user')
const fields = [
'username', 'display_name', 'email', 'nw_privilege', 'activated'
]
data = dataFilter(data, fields, ['nw_privilege', 'activated'])
if (!data) throw new Error('Missing fields')
await Users.User.update(user, data)
return {}
},
resendActivationEmail: async function (id) {
const user = await Users.User.get(id)
if (!user) throw new Error('No such user')
if (user.activated === 1) return {}
await Users.User.Register.activationEmail(user)
return {}
},
revokeTotpToken: async function (id) {
const user = await Users.User.get(id)
if (!user) throw new Error('No such user')
await Models.TotpToken.query().delete().where('user_id', user.id)
return {}
},
sendPasswordEmail: async function (id) {
const user = await Users.User.get(id)
if (!user) throw new Error('No such user')
const token = await Users.User.Reset.reset(user.email, false, true)
return { token }
},
// Search for users by terms and fields
searchUsers: async function (terms, fields = ['email']) {
let qb = Models.User.query()
terms = terms.replace(/_/g, '\\_').replace(/%/g, '\\%')
qb = qb.where(fields[0], 'like', '%' + terms + '%')
if (fields.length >= 1) {
for (let i = 1; i < fields.length; i++) {
qb = qb.orWhere(fields[i], 'like', '%' + terms + '%')
}
}
const rows = await qb.limit(8)
if (!rows.length) return { error: 'No results' }
const cleaned = []
for (const i in rows) {
const userRaw = rows[i]
cleaned.push(await cleanUserObject(userRaw, null))
}
return cleaned
},
// List all clients (paginated)
getAllClients: async function (page) {
let count = await Models.OAuth2Client.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No clients' }
}
count = count[0].ids
const paginated = Users.Pagination(perPage, parseInt(count), page)
const raw = await Models.OAuth2Client.query().offset(paginated.offset).limit(perPage)
const clients = []
for (const i in raw) {
const entry = raw[i]
clients.push(await cleanClientObject(entry))
}
return {
page: paginated, clients
}
},
// Get information about a client via id
getClient: async function (id) {
const raw = await Models.OAuth2Client.query().where('id', id)
if (!raw.length) throw new Error('No such client')
return cleanClientObject(raw[0])
},
// Update a client `id` in database with `data`
updateClient: async function (id, data) {
const fields = [
'title', 'description', 'url', 'redirect_url', 'scope', 'verified'
]
data = dataFilter(data, fields, ['scope', 'verified'])
if (!data) throw new Error('Missing fields')
try {
await Models.OAuth2Client.query().patchAndFetchById(id, data)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
} catch (e) {
throw new Error('No such client')
}
return {}
},
// Create a new secret for a client
newSecret: async function (id) {
if (isNaN(id)) throw new Error('Invalid client ID')
const secret = Users.Hash(16)
try {
await Models.OAuth2Client.query().patchAndFetchById(id, { secret: secret })
} catch (e) {
throw new Error('No such client')
}
return {}
},
// Create a new client
createClient: async function (data, user) {
const fields = [
'title', 'description', 'url', 'redirect_url', 'scope'
]
data = dataFilter(data, fields, ['scope'])
if (!data) throw new Error('Missing fields')
const obj = Object.assign({
secret: Users.Hash(16),
grants: 'authorization_code',
created_at: new Date(),
user_id: user.id
}, data)
return Models.OAuth2Client.query().insert(obj)
},
// Remove a client and all associated data
removeClient: async function (id) {
if (isNaN(id)) throw new Error('Invalid ID number')
await Models.OAuth2Client.query().delete().where('id', id)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
await Models.OAuth2AccessToken.query().delete().where('client_id', id)
await Models.OAuth2RefreshToken.query().delete().where('client_id', id)
return true
},
// List all bans (paginated)
getAllBans: async function (page) {
let count = await Models.Ban.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No bans on record' }
}
count = count[0].ids
const paginated = Users.Pagination(perPage, parseInt(count), page)
const raw = await Models.Ban.query().offset(paginated.offset).limit(perPage)
const bans = []
for (const i in raw) {
const entry = raw[i]
bans.push(await cleanBanObject(entry))
}
return {
page: paginated, bans
}
},
// Remove a ban
removeBan: async function (banId) {
return Models.Ban.query().delete().where('id', banId)
},
// Create a ban
addBan: async function (data, adminId) {
const user = await Users.User.get(parseInt(data.user_id))
if (!user) throw new Error('No such user.')
if (user.id === adminId) throw new Error('Cannot ban yourself!')
const admin = await Users.User.get(adminId)
if (user.nw_privilege > admin.nw_privilege) throw new Error('Cannot ban user.')
const banAdd = {
reason: data.reason || 'Unspecified ban',
admin_id: adminId,
user_id: user.id,
expires_at: data.expires_at != null ? new Date(data.expires_at) : null,
created_at: new Date(),
associated_ip: data.ip_address || user.ip_address || null
}
await Models.Ban.query().insert(banAdd)
return {}
},
lockAccount: async function (userId) {
const user = await Users.User.get(userId)
if (user.id === 1 || user.nw_privilege > 2) {
throw new Error('Cannot lock this user.')
}
const lockId = Users.Hash(4)
const userObf = {
username: lockId,
display_name: user.username,
email: `${lockId}@icynet.eu`,
password: null,
activated: false,
locked: true,
avatar_file: null
}
return Users.User.update(user, userObf)
} }
} }
module.exports = API export async function getUser (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user')
return cleanUserObject(user, null)
}
export async function editUser (id, data) {
const user = await User.get(id)
if (!user) throw new Error('No such user')
const fields = [
'username', 'display_name', 'email', 'nw_privilege', 'activated'
]
data = dataFilter(data, fields, ['nw_privilege', 'activated'])
if (!data) throw new Error('Missing fields')
await User.update(user, data)
return {}
}
export async function resendActivationEmail (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user')
if (user.activated === 1) return {}
await Register.activationEmail(user)
return {}
}
export async function revokeTotpToken (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user')
await Models.TotpToken.query().delete().where('user_id', user.id)
return {}
}
export async function sendPasswordEmail (id) {
const user = await User.get(id)
if (!user) throw new Error('No such user')
const token = await Reset.reset(user.email, false, true)
return { token }
}
// Search for users by terms and fields
export async function searchUsers (terms, fields = ['email']) {
let qb = Models.User.query()
terms = terms.replace(/_/g, '\\_').replace(/%/g, '\\%')
qb = qb.where(fields[0], 'like', '%' + terms + '%')
if (fields.length >= 1) {
for (let i = 1; i < fields.length; i++) {
qb = qb.orWhere(fields[i], 'like', '%' + terms + '%')
}
}
const rows = await qb.limit(8)
if (!rows.length) return { error: 'No results' }
const cleaned = []
for (const i in rows) {
const userRaw = rows[i]
cleaned.push(await cleanUserObject(userRaw, null))
}
return cleaned
}
// List all clients (paginated)
export async function getAllClients (page) {
let count = await Models.OAuth2Client.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No clients' }
}
count = count[0].ids
const paginated = Pagination(perPage, parseInt(count), page)
const raw = await Models.OAuth2Client.query().offset(paginated.offset).limit(perPage)
const clients = []
for (const i in raw) {
const entry = raw[i]
clients.push(await cleanClientObject(entry))
}
return {
page: paginated, clients
}
}
// Get information about a client via id
export async function getClient (id) {
const raw = await Models.OAuth2Client.query().where('id', id)
if (!raw.length) throw new Error('No such client')
return cleanClientObject(raw[0])
}
// Update a client `id` in database with `data`
export async function updateClient (id, data) {
const fields = [
'title', 'description', 'url', 'redirect_url', 'scope', 'verified'
]
data = dataFilter(data, fields, ['scope', 'verified'])
if (!data) throw new Error('Missing fields')
try {
await Models.OAuth2Client.query().patchAndFetchById(id, data)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
} catch (e) {
throw new Error('No such client')
}
return {}
}
// Create a new secret for a client
export async function newSecret (id) {
if (isNaN(id)) throw new Error('Invalid client ID')
const secret = Hash(16)
try {
await Models.OAuth2Client.query().patchAndFetchById(id, { secret: secret })
} catch (e) {
throw new Error('No such client')
}
return {}
}
// Create a new client
export async function createClient (data, user) {
const fields = [
'title', 'description', 'url', 'redirect_url', 'scope'
]
data = dataFilter(data, fields, ['scope'])
if (!data) throw new Error('Missing fields')
const obj = Object.assign({
secret: Hash(16),
grants: 'authorization_code',
created_at: new Date(),
user_id: user.id
}, data)
return Models.OAuth2Client.query().insert(obj)
}
// Remove a client and all associated data
export async function removeClient (id) {
if (isNaN(id)) throw new Error('Invalid ID number')
await Models.OAuth2Client.query().delete().where('id', id)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
await Models.OAuth2AccessToken.query().delete().where('client_id', id)
await Models.OAuth2RefreshToken.query().delete().where('client_id', id)
return true
}
// List all bans (paginated)
export async function getAllBans (page) {
let count = await Models.Ban.query().count('id as ids')
if (!count.length || !count[0].ids || isNaN(page)) {
return { error: 'No bans on record' }
}
count = count[0].ids
const paginated = Pagination(perPage, parseInt(count), page)
const raw = await Models.Ban.query().offset(paginated.offset).limit(perPage)
const bans = []
for (const i in raw) {
const entry = raw[i]
bans.push(await cleanBanObject(entry))
}
return {
page: paginated, bans
}
}
// Remove a ban
export async function removeBan (banId) {
return Models.Ban.query().delete().where('id', banId)
}
// Create a ban
export async function addBan (data, adminId) {
const user = await User.get(parseInt(data.user_id))
if (!user) throw new Error('No such user.')
if (user.id === adminId) throw new Error('Cannot ban yourself!')
const admin = await User.get(adminId)
if (user.nw_privilege > admin.nw_privilege) throw new Error('Cannot ban user.')
const banAdd = {
reason: data.reason || 'Unspecified ban',
admin_id: adminId,
user_id: user.id,
expires_at: data.expires_at != null ? new Date(data.expires_at) : null,
created_at: new Date(),
associated_ip: data.ip_address || user.ip_address || null
}
await Models.Ban.query().insert(banAdd)
return {}
}
export async function lockAccount (userId) {
const user = await User.get(userId)
if (user.id === 1 || user.nw_privilege > 2) {
throw new Error('Cannot lock this user.')
}
const lockId = Hash(4)
const userObf = {
username: lockId,
display_name: user.username,
email: `${lockId}@icynet.eu`,
password: null,
activated: false,
locked: true,
avatar_file: null
}
return User.update(user, userObf)
}

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,429 +4,445 @@ 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 static stateGenerator (req) {
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
getExternal: async (service, identifier) => {
let extr = await models.External.query().where('service', service).andWhere('identifier', identifier)
if (!extr || !extr.length) return null
extr = extr[0]
extr.user = null
if (extr.user_id !== null) { // Find an user with an external ID
const user = await UAPI.User.get(extr.user_id) static async getExternal (service, identifier) {
if (user) { let extr = await models.External.query().where('service', service).andWhere('identifier', identifier)
extr.user = user if (!extr || !extr.length) return null
} extr = extr[0]
} extr.user = null
return extr
},
// Get user ban status
getBan: async (user, ipAddress) => {
const banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null)
return banList
},
// Create a new `external` instance for a user
new: async (service, identifier, user) => {
const data = {
user_id: user.id,
service: service,
identifier: identifier,
created_at: new Date()
}
await await models.External.query().insert(data)
return true
},
// Create a new user
newUser: async (service, identifier, data) => {
if (config.external.registrations !== true) throw new Error('Registrations from third-party websites are not allowed.')
const udataLimited = Object.assign({
activated: 1,
created_at: new Date(),
updated_at: new Date(),
uuid: uuidV1()
}, data)
// Some data cleanups
// Limit display name length
udataLimited.display_name = udataLimited.display_name.substring(0, 32)
// Remove illegal characters from the username
udataLimited.username = udataLimited.username.replace(/\W+/gi, '')
// Limit user name length
udataLimited.username = udataLimited.username.substring(0, 26)
// Check if the username is already taken
if (await UAPI.User.get(udataLimited.username) != null || udataLimited.username.length < 4) {
udataLimited.username = udataLimited.username + UAPI.Hash(4)
}
// Check if the email given to us is already registered, if so,
// tell them to log in first.
if (udataLimited.email && udataLimited.email !== '') {
const getByEmail = await UAPI.User.get(udataLimited.email)
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.')
}
}
// Create a new user based on the information we got from an external service
const newUser = await models.User.query().insert(udataLimited)
await API.Common.new(service, identifier, newUser)
return newUser
},
// Remove an `external` object (thus unlinking from a service)
remove: async (user, service) => {
user = await UAPI.User.ensureObject(user, ['password'])
const userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
if (!userExterns.length) {
return false
}
// Do not remove the service the user signed up with
if (userExterns[0] && (user.password === '' || user.password === null) && userExterns[0].service === service) {
return false
}
return models.External.query().delete().where('user_id', user.id).andWhere('service', service)
},
// Common code for all auth callbacks
callback: async (identifier, uid, user, ipAddress, remoteData, avatarFunc) => {
const exists = await API.Common.getExternal(identifier, uid)
if (extr.user_id !== null) {
const user = await User.get(extr.user_id)
if (user) { if (user) {
// Get bans for user extr.user = user
const bans = await API.Common.getBan(user)
if (bans.length) return { banned: bans, ip: false }
if (exists) return { error: null, user: user }
await API.Common.new(identifier, uid, user)
return { error: null, user: user }
} }
// Callback succeeded with user id and the external table exists, we log in the user
if (exists) {
// Get bans for user
const bans = await API.Common.getBan(exists.user)
if (bans.length) return { banned: bans, ip: false }
return { error: null, user: exists.user }
}
// Get bans for IP address
const bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Run the function for avatar fetching
let avatar = null
if (avatarFunc) {
avatar = await avatarFunc(remoteData)
}
// Assign the data
const newUData = Object.assign({
email: remoteData.email || '',
avatar_file: avatar,
ip_address: ipAddress
}, remoteData)
// Remove unnecessary fields
for (const i in newUData) {
if (userFields.indexOf(i) === -1) {
delete newUData[i]
}
}
let newUser
try {
newUser = await API.Common.newUser(identifier, uid, newUData)
} catch (e) {
return { error: e.message }
}
return { error: null, user: newUser }
} }
},
Facebook: {
getAvatar: async (rawData) => {
let profilepic = null
if (rawData.picture) { return extr
if (rawData.picture.is_silhouette === false && rawData.picture.url) { }
const imgdata = await Image.downloadImage(rawData.picture.url)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
}
return profilepic // Get user ban status
}, static async getBan (user, ipAddress) {
callback: async (user, authResponse, ipAddress) => { const banList = await User.getBanStatus(ipAddress || user.id, ipAddress != null)
if (!authResponse) { return banList
return { error: 'No Authorization' } }
}
const uid = authResponse.userID // Create a new `external` instance for a user
if (!uid) { static async new (service, identifier, user) {
return { error: 'No Authorization' } const data = {
} user_id: user.id,
service: service,
// Get facebook user information in order to create a new user or verify identifier: identifier,
let fbdata created_at: new Date()
const intel = {
access_token: authResponse.accessToken,
fields: 'name,email,picture,short_name'
}
try {
fbdata = await http.GET('https://graph.facebook.com/v2.10/' + uid + '?' + qs.stringify(intel))
fbdata = JSON.parse(fbdata)
} catch (e) {
return { error: 'Could not get user information', errorObject: e }
}
if (fbdata.error) {
return { error: fbdata.error.message }
}
const cleanedData = Object.assign(fbdata, {
username: fbdata.short_name || 'FB' + UAPI.Hash(4),
display_name: fbdata.name,
email: fbdata.email || ''
})
return API.Common.callback('facebook', uid, user, ipAddress, cleanedData, API.Facebook.getAvatar)
} }
},
Twitter: {
getAvatar: async function (rawData) {
let profilepic = null
if (rawData.profile_image_url_https) { await models.External.query().insert(data)
const imgdata = await Image.downloadImage(rawData.profile_image_url_https) return true
if (imgdata && imgdata.fileName) { }
profilepic = imgdata.fileName
}
}
return profilepic // Create a new user
}, static async newUser (service, identifier, data) {
oauthApp: function () { if (config.external.registrations !== true) throw new Error('Registrations from third-party websites are not allowed.')
if (!twitterApp) { const udataLimited = Object.assign({
const redirectUri = config.server.domain + '/api/external/twitter/callback' activated: 1,
twitterApp = new oauth.PromiseOAuth( created_at: new Date(),
'https://api.twitter.com/oauth/request_token', updated_at: new Date(),
'https://api.twitter.com/oauth/access_token', uuid: uuidV1()
config.external.twitter.api, }, data)
config.external.twitter.api_secret,
'1.0A',
redirectUri,
'HMAC-SHA1'
)
}
},
getRequestToken: async function () {
if (!twitterApp) API.Twitter.oauthApp()
let tokens
try { // Some data cleanups
tokens = await twitterApp.getOAuthRequestToken()
} catch (e) {
console.error(e)
return { error: 'No tokens returned' }
}
if (tokens[2].oauth_callback_confirmed !== 'true') return { error: 'No tokens returned.' } // Limit display name length
udataLimited.display_name = udataLimited.display_name.substring(0, 32)
return { error: null, token: tokens[0], token_secret: tokens[1] } // Remove illegal characters from the username
}, udataLimited.username = udataLimited.username.replace(/\W+/gi, '')
getAccessTokens: async function (token, secret, verifier) {
if (!twitterApp) API.Twitter.oauthApp()
let tokens
try { // Limit user name length
tokens = await twitterApp.getOAuthAccessToken(token, secret, verifier) udataLimited.username = udataLimited.username.substring(0, 26)
} catch (e) {
console.error(e)
return { error: 'No tokens returned' }
}
if (!tokens || !tokens.length) return { error: 'No tokens returned' } // Check if the username is already taken
if (await User.get(udataLimited.username) != null || udataLimited.username.length < 4) {
return { error: null, access_token: tokens[0], access_token_secret: tokens[1] } udataLimited.username = udataLimited.username + Hash(4)
},
callback: async function (user, accessTokens, ipAddress) {
if (!twitterApp) API.Twitter.oauthApp()
let twdata
try {
const resp = await twitterApp.get('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
accessTokens.access_token, accessTokens.access_token_secret)
twdata = JSON.parse(resp[0])
} catch (e) {
console.error(e)
return { error: 'Failed to verify user credentials.' }
}
const uid = twdata.id_str
const cleanedData = Object.assign(twdata, {
username: twdata.screen_name,
display_name: twdata.name,
email: twdata.email || ''
})
return API.Common.callback('twitter', uid, user, ipAddress, cleanedData, API.Twitter.getAvatar)
} }
},
Google: { // Check if the email given to us is already registered, if so,
getAvatar: async (rawData) => { // tell them to log in first.
let profilepic = null if (udataLimited.email && udataLimited.email !== '') {
if (rawData.image) { const getByEmail = await User.get(udataLimited.email)
const imgdata = await Image.downloadImage(rawData.image) if (getByEmail) {
if (imgdata && imgdata.fileName) { 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.')
profilepic = imgdata.fileName
}
} }
return profilepic
},
callback: async (user, data, ipAddress) => {
let uid
try {
const test = await http.GET('https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data.id_token)
if (!test) throw new Error('No response!')
const jsondata = JSON.parse(test)
if (!jsondata || !jsondata.email || !jsondata.name) throw new Error('Please allow Basic Profile and Email.')
if (jsondata.email !== data.email || jsondata.name !== data.name) throw new Error('Conflicting data. Please try again!')
if (new Date(parseInt(jsondata.exp) * 1000) < Date.now()) throw new Error('Expired token! Please try again!')
uid = jsondata.sub
} catch (e) {
return { error: e.message }
}
const cleanedData = Object.assign(data, {
username: data.name,
display_name: data.name,
email: data.email || ''
})
return API.Common.callback('google', uid, user, ipAddress, cleanedData, API.Google.getAvatar)
} }
},
Discord: {
getAvatar: async (rawData) => {
let profilepic = null
const aviSnowflake = rawData.avatar
if (aviSnowflake) {
try {
const avpt = await Image.downloadImage('https://cdn.discordapp.com/avatars/' + rawData.id + '/' + aviSnowflake + '.png')
if (avpt && avpt.fileName) {
profilepic = avpt.fileName
}
} catch (e) {
profilepic = null
}
}
return profilepic // Create a new user based on the information we got from an external service
}, const newUser = await models.User.query().insert(udataLimited)
oauth2App: function () { await Common.new(service, identifier, newUser)
if (discordApp) return
discordApp = new oauth.PromiseOAuth2(
config.external.discord.api,
config.external.discord.api_secret,
'https://discordapp.com/api/',
'oauth2/authorize',
'oauth2/token'
)
discordApp.useAuthorizationHeaderforGET(true) return newUser
}, }
getAuthorizeURL: function (req) {
if (!discordApp) API.Discord.oauth2App()
const state = API.Common.stateGenerator(req)
const redirectUri = config.server.domain + '/api/external/discord/callback'
const params = { // Remove an `external` object (thus unlinking from a service)
client_id: config.external.discord.api, static async remove (user, service) {
redirect_uri: redirectUri, user = await User.ensureObject(user, ['password'])
scope: 'identify email', const userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
response_type: 'code',
state: state
}
const url = discordApp.getAuthorizeUrl(params) if (!userExterns.length) {
return false
return { error: null, state: state, url: url }
},
getAccessToken: async function (code) {
if (!discordApp) API.Discord.oauth2App()
const redirectUri = config.server.domain + '/api/external/discord/callback'
let tokens
try {
tokens = await discordApp.getOAuthAccessToken(code, { grant_type: 'authorization_code', redirect_uri: redirectUri })
} catch (e) {
console.error(e)
return { error: 'No Authorization' }
}
if (!tokens.length) return { error: 'No Tokens' }
tokens = tokens[2]
return { error: null, accessToken: tokens.access_token }
},
callback: async function (user, accessToken, ipAddress) {
if (!discordApp) API.Discord.oauth2App()
let ddata
try {
const resp = await discordApp.get('https://discordapp.com/api/users/@me', accessToken)
ddata = JSON.parse(resp)
} catch (e) {
console.error(e)
return { error: 'Could not get user information' }
}
const uid = ddata.id
// Create a new user
const cleanedData = Object.assign(ddata, {
display_name: ddata.username,
email: ddata.email || ''
})
return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar)
} }
// Do not remove the service the user signed up with
if (userExterns[0] && (user.password === '' || user.password === null) && userExterns[0].service === service) {
return false
}
return models.External.query().delete().where('user_id', user.id).andWhere('service', service)
}
// Common code for all auth callbacks
static async callback (identifier, uid, user, ipAddress, remoteData, avatarFunc) {
const exists = await Common.getExternal(identifier, uid)
if (user) {
// Get bans for user
const bans = await Common.getBan(user)
if (bans.length) return { banned: bans, ip: false }
if (exists) return { error: null, user: user }
await Common.new(identifier, uid, user)
return { error: null, user: user }
}
// Callback succeeded with user id and the external table exists, we log in the user
if (exists) {
// Get bans for user
const bans = await Common.getBan(exists.user)
if (bans.length) return { banned: bans, ip: false }
return { error: null, user: exists.user }
}
// Get bans for IP address
const bans = await Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Run the function for avatar fetching
let avatar = null
if (avatarFunc) {
avatar = await avatarFunc(remoteData)
}
// Assign the data
const newUData = Object.assign({
email: remoteData.email || '',
avatar_file: avatar,
ip_address: ipAddress
}, remoteData)
// Remove unnecessary fields
for (const i in newUData) {
if (userFields.indexOf(i) === -1) {
delete newUData[i]
}
}
let newUser
try {
newUser = await Common.newUser(identifier, uid, newUData)
} catch (e) {
return { error: e.message }
}
return { error: null, user: newUser }
} }
} }
module.exports = API export class Facebook {
static async getAvatar (rawData) {
let profilepic = null
if (rawData.picture) {
if (rawData.picture.is_silhouette === false && rawData.picture.url) {
const imgdata = await Image.downloadImage(rawData.picture.url)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
}
return profilepic
}
static async callback (user, authResponse, ipAddress) {
if (!authResponse) {
return { error: 'No Authorization' }
}
const uid = authResponse.userID
if (!uid) {
return { error: 'No Authorization' }
}
// Get facebook user information in order to create a new user or verify
let fbdata
const intel = {
access_token: authResponse.accessToken,
fields: 'name,email,picture,short_name'
}
try {
fbdata = await httpGET('https://graph.facebook.com/v2.10/' + uid + '?' + qs.stringify(intel))
fbdata = JSON.parse(fbdata)
} catch (e) {
return { error: 'Could not get user information', errorObject: e }
}
if (fbdata.error) {
return { error: fbdata.error.message }
}
const cleanedData = Object.assign(fbdata, {
username: fbdata.short_name || 'FB' + Hash(4),
display_name: fbdata.name,
email: fbdata.email || ''
})
return Common.callback('facebook', uid, user, ipAddress, cleanedData, Facebook.getAvatar)
}
}
export class Twitter {
static async getAvatar (rawData) {
let profilepic = null
if (rawData.profile_image_url_https) {
const imgdata = await Image.downloadImage(rawData.profile_image_url_https)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
return profilepic
}
static oauthApp () {
if (!twitterApp) {
const redirectUri = config.server.domain + '/api/external/twitter/callback'
twitterApp = new oauth.PromiseOAuth(
'https://api.twitter.com/oauth/request_token',
'https://api.twitter.com/oauth/access_token',
config.external.twitter.api,
config.external.twitter.api_secret,
'1.0A',
redirectUri,
'HMAC-SHA1'
)
}
}
static async getRequestToken () {
if (!twitterApp) Twitter.oauthApp()
let tokens
try {
tokens = await twitterApp.getOAuthRequestToken()
} catch (e) {
console.error(e)
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] }
}
static async getAccessTokens (token, secret, verifier) {
if (!twitterApp) Twitter.oauthApp()
let tokens
try {
tokens = await twitterApp.getOAuthAccessToken(token, secret, verifier)
} catch (e) {
console.error(e)
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] }
}
static async callback (user, accessTokens, ipAddress) {
if (!twitterApp) Twitter.oauthApp()
let twdata
try {
const resp = await twitterApp.get('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
accessTokens.access_token, accessTokens.access_token_secret)
twdata = JSON.parse(resp[0])
} catch (e) {
console.error(e)
return { error: 'Failed to verify user credentials.' }
}
const uid = twdata.id_str
const cleanedData = Object.assign(twdata, {
username: twdata.screen_name,
display_name: twdata.name,
email: twdata.email || ''
})
return Common.callback('twitter', uid, user, ipAddress, cleanedData, Twitter.getAvatar)
}
}
export class Google {
static async getAvatar (rawData) {
let profilepic = null
if (rawData.image) {
const imgdata = await Image.downloadImage(rawData.image)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
return profilepic
}
static async callback (user, data, ipAddress) {
let uid
try {
const test = await httpGET('https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data.id_token)
if (!test) throw new Error('No response!')
const jsondata = JSON.parse(test)
if (!jsondata || !jsondata.email || !jsondata.name) throw new Error('Please allow Basic Profile and Email.')
if (jsondata.email !== data.email || jsondata.name !== data.name) throw new Error('Conflicting data. Please try again!')
if (new Date(parseInt(jsondata.exp) * 1000) < Date.now()) throw new Error('Expired token! Please try again!')
uid = jsondata.sub
} catch (e) {
return { error: e.message }
}
const cleanedData = Object.assign(data, {
username: data.name,
display_name: data.name,
email: data.email || ''
})
return Common.callback('google', uid, user, ipAddress, cleanedData, Google.getAvatar)
}
}
export class Discord {
static async getAvatar (rawData) {
let profilepic = null
const aviSnowflake = rawData.avatar
if (aviSnowflake) {
try {
const avpt = await Image.downloadImage('https://cdn.discordapp.com/avatars/' + rawData.id + '/' + aviSnowflake + '.png')
if (avpt && avpt.fileName) {
profilepic = avpt.fileName
}
} catch (e) {
profilepic = null
}
}
return profilepic
}
static oauth2App () {
if (discordApp) return
discordApp = new oauth.PromiseOAuth2(
config.external.discord.api,
config.external.discord.api_secret,
'https://discordapp.com/api/',
'oauth2/authorize',
'oauth2/token'
)
discordApp.useAuthorizationHeaderforGET(true)
}
static getAuthorizeURL (req) {
if (!discordApp) Discord.oauth2App()
const state = Common.stateGenerator(req)
const redirectUri = config.server.domain + '/api/external/discord/callback'
const params = {
client_id: config.external.discord.api,
redirect_uri: redirectUri,
scope: 'identify email',
response_type: 'code',
state: state
}
const url = discordApp.getAuthorizeUrl(params)
return { error: null, state: state, url: url }
}
static async getAccessToken (code) {
if (!discordApp) Discord.oauth2App()
const redirectUri = config.server.domain + '/api/external/discord/callback'
let tokens
try {
tokens = await discordApp.getOAuthAccessToken(code, { grant_type: 'authorization_code', redirect_uri: redirectUri })
} catch (e) {
console.error(e)
return { error: 'No Authorization' }
}
if (!tokens.length) return { error: 'No Tokens' }
tokens = tokens[2]
return { error: null, accessToken: tokens.access_token }
}
static async callback (user, accessToken, ipAddress) {
if (!discordApp) Discord.oauth2App()
let ddata
try {
const resp = await discordApp.get('https://discordapp.com/api/users/@me', accessToken)
ddata = JSON.parse(resp)
} catch (e) {
console.error(e)
return { error: 'Could not get user information' }
}
const uid = ddata.id
// Create a new user
const cleanedData = Object.assign(ddata, {
display_name: ddata.username,
email: ddata.email || ''
})
return Common.callback('discord', uid, user, ipAddress, cleanedData, Discord.getAvatar)
}
}

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
}

File diff suppressed because it is too large Load Diff

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,126 +37,127 @@ 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)
if (!news.length) return [] if (!news.length) return []
const articles = [] const articles = []
for (const i in news) { for (const i in news) {
const entry = news[i] const entry = news[i]
articles.push(await cleanArticle(entry)) articles.push(await cleanArticle(entry))
} }
return articles return articles
}, }
listNews: async (page) => {
let count = await Models.News.query().count('id as ids')
if (page < 1) page = 1
if (!count.length || !count[0].ids || isNaN(page)) { export async function listNews (page) {
return { error: 'No news' } let count = await Models.News.query().count('id as ids')
} if (page < 1) page = 1
count = count[0].ids if (!count.length || !count[0].ids || isNaN(page)) {
const paginated = API.Pagination(perPage, parseInt(count), page) return { error: 'No news' }
const news = await Models.News.query().orderBy('created_at', 'desc').offset(paginated.offset).limit(perPage) }
const articles = [] count = count[0].ids
for (const i in news) { const paginated = Pagination(perPage, parseInt(count), page)
const entry = news[i] const news = await Models.News.query().orderBy('created_at', 'desc').offset(paginated.offset).limit(perPage)
articles.push(await cleanArticle(entry)) const articles = []
} for (const i in news) {
const entry = news[i]
return { articles.push(await cleanArticle(entry))
page: paginated, }
articles: articles
}
},
article: async (id) => {
let article = await Models.News.query().where('id', id)
if (!article.length) return {}
article = article[0]
return cleanArticle(article) return {
}, page: paginated,
compose: async (user, body) => { articles: articles
const article = {
title: body.title,
content: body.content,
tags: body.tags || '',
user_id: user.id,
created_at: new Date(),
updated_at: new Date()
}
const result = await Models.News.query().insert(article)
result.slug = slugify(result.title)
return result
},
edit: async (id, body) => {
const patch = {
content: body.content,
updated_at: new Date()
}
const result = await Models.News.query().patchAndFetchById(id, patch)
if (!result) throw new Error('Something went wrong.')
return {}
},
generateFeed: async () => {
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 cleanNews = []
for (const i in posts) {
cleanNews.push(await cleanArticle(posts[i]))
}
feed = new Feed({
title: 'Icy Network News',
description: 'Icy Network News',
id: config.server.domain + '/news',
link: config.server.domain + '/news',
image: config.server.domain + '/static/image/icynet-icon.png',
favicon: config.server.domain + '/favicon.ico',
copyright: '2020 Icy Network',
updated: new Date(),
feedLinks: {
json: config.server.domain + '/news/feed.json',
atom: config.server.domain + '/news/atom.xml'
},
author: {
name: 'Icy Network',
email: 'icynet@icynet.eu',
link: config.server.domain
}
})
for (const i in cleanNews) {
const post = cleanNews[i]
feed.addItem({
title: post.title,
id: post.id,
link: `${config.server.domain}/news/${post.id}-${slugify(post.title)}`,
description: post.description,
content: post.content,
author: [{
name: post.author.display_name,
email: post.author.email
}],
date: new Date(post.updated_at)
})
}
return feed
} }
} }
module.exports = News export async function article (id) {
let article = await Models.News.query().where('id', id)
if (!article.length) return {}
article = article[0]
return cleanArticle(article)
}
export async function compose (user, body) {
const article = {
title: body.title,
content: body.content,
tags: body.tags || '',
user_id: user.id,
created_at: new Date(),
updated_at: new Date()
}
const result = await Models.News.query().insert(article)
result.slug = slugify(result.title)
return result
}
export async function edit (id, body) {
const patch = {
content: body.content,
updated_at: new Date()
}
const result = await Models.News.query().patchAndFetchById(id, patch)
if (!result) throw new Error('Something went wrong.')
return {}
}
export async function generateFeed () {
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 cleanNews = []
for (const i in posts) {
cleanNews.push(await cleanArticle(posts[i]))
}
feed = new Feed({
title: 'Icy Network News',
description: 'Icy Network News',
id: config.server.domain + '/news',
link: config.server.domain + '/news',
image: config.server.domain + '/static/image/icynet-icon.png',
favicon: config.server.domain + '/favicon.ico',
copyright: '2020 Icy Network',
updated: new Date(),
feedLinks: {
json: config.server.domain + '/news/feed.json',
atom: config.server.domain + '/news/atom.xml'
},
author: {
name: 'Icy Network',
email: 'icynet@icynet.eu',
link: config.server.domain
}
})
for (const i in cleanNews) {
const post = cleanNews[i]
feed.addItem({
title: post.title,
link: `${config.server.domain}/news/${post.id}-${slugify(post.title)}`,
description: post.description,
content: post.content,
author: [{
name: post.author.display_name,
email: post.author.email,
link: config.server.domain
}],
date: new Date(post.updated_at)
})
}
return feed
}

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,293 +1,294 @@
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'] }
export const accessToken = {
ttl: config.oauth2.access_token_life,
getToken: (object) => {
if (object) return object.token
return null
}, },
accessToken: { create: async (userId, clientId, scope, ttl) => {
ttl: config.oauth2.access_token_life, const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
getToken: (object) => { const expr = new Date(Date.now() + ttl * 1000)
if (object) return object.token
return null
},
create: async (userId, clientId, scope, ttl) => {
const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
const expr = new Date(Date.now() + ttl * 1000)
if (typeof scope === 'object') { if (typeof scope === 'object') {
scope = scope.join(' ') scope = scope.join(' ')
}
// Delete already existing tokens with this exact user id, client id and scope, because it will
// eventually pile up and flood the database.
await Models.OAuth2AccessToken.query().delete().where('user_id', userId)
.andWhere('client_id', clientId)
const obj = {
token,
scope,
user_id: userId,
client_id: clientId,
expires_at: expr,
created_at: new Date()
}
const res = await Models.OAuth2AccessToken.query().insert(obj)
if (!res) return null
return res.token
},
fetchByToken: async (token) => {
if (typeof token === 'object') {
return token
}
token = await Models.OAuth2AccessToken.query().where('token', token)
if (!token.length) return null
return token[0]
},
checkTTL: (object) => {
return (object.expires_at > Date.now())
},
getTTL: (object) => {
return (object.expires_at - Date.now())
},
fetchByUserIdClientId: async (userId, clientId) => {
const tkn = await Models.OAuth2AccessToken.query().where('user_id', userId).andWhere('client_id', clientId)
if (!tkn.length) return null
return tkn[0]
} }
// Delete already existing tokens with this exact user id, client id and scope, because it will
// eventually pile up and flood the database.
await Models.OAuth2AccessToken.query().delete().where('user_id', userId)
.andWhere('client_id', clientId)
const obj = {
token,
scope,
user_id: userId,
client_id: clientId,
expires_at: expr,
created_at: new Date()
}
const res = await Models.OAuth2AccessToken.query().insert(obj)
if (!res) return null
return res.token
}, },
client: { fetchByToken: async (token) => {
getId: (client) => { if (typeof token === 'object') {
return client.id return token
},
fetchById: async (id) => {
const client = await Models.OAuth2Client.query().where('id', id)
if (!client.length) return null
return client[0]
},
checkSecret: (client, secret) => {
return client.secret === secret
},
checkGrantType: (client, grant) => {
if (client.grants.indexOf(grant) !== -1) {
return true
}
return false
},
getRedirectUri: (client) => {
return client.redirect_url
},
checkRedirectUri: (client, redirectUri) => {
return (redirectUri.indexOf(OAuthDB.client.getRedirectUri(client)) === 0 &&
redirectUri.replace(OAuthDB.client.getRedirectUri(client), '').indexOf('#') === -1)
},
transformScope: (scope) => {
if (!scope) return []
if (typeof scope === 'object') {
return scope
}
scope = scope.trim()
if (scope.indexOf(',') !== -1) {
scope = scope.split(',')
} else {
scope = scope.split(' ')
}
return scope
},
checkScope: (client, scope) => {
if (!scope) return []
if (typeof scope === 'string') {
scope = OAuthDB.client.transformScope(scope)
}
const clientScopes = client.scope.split(' ')
for (const i in scope) {
if (clientScopes.indexOf(scope[i]) === -1) {
return false
}
}
return scope
} }
token = await Models.OAuth2AccessToken.query().where('token', token)
if (!token.length) return null
return token[0]
}, },
code: { checkTTL: (object) => {
ttl: config.oauth2.code_life, return (object.expires_at > Date.now())
create: async (userId, clientId, scope, ttl, special = false) => {
const code = crypto.randomBytes(config.oauth2.token_length).toString('hex')
const expr = new Date(Date.now() + ttl * 1000)
if (typeof scope === 'object') {
scope = scope.join(' ')
}
// Delete already existing codes with this exact user id, client id and scope, because it will
// eventually pile up and flood the database, especially when they were never used.
await Models.OAuth2Code.query().delete().where('user_id', userId).andWhere('client_id', clientId)
const obj = {
code,
scope,
user_id: userId,
client_id: clientId,
expires_at: expr,
created_at: new Date()
}
await Models.OAuth2Code.query().insert(obj)
return obj.code
},
fetchByCode: async (code) => {
code = await Models.OAuth2Code.query().where('code', code)
if (!code.length) return null
return code[0]
},
removeByCode: async (code) => {
if (typeof code === 'object') {
code = code.code
}
return Models.OAuth2Code.query().delete().where('code', code)
},
getUserId: (code) => {
return code.user_id
},
getClientId: (code) => {
return code.client_id
},
getScope: (code) => {
return code.scope
},
checkTTL: (code) => {
return (code.expires_at > Date.now())
}
}, },
refreshToken: { getTTL: (object) => {
create: async (userId, clientId, scope) => { return (object.expires_at - Date.now())
const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
if (typeof scope === 'object') {
scope = scope.join(' ')
}
const obj = {
token,
scope,
user_id: userId,
client_id: clientId,
created_at: new Date()
}
await Models.OAuth2RefreshToken.query().insert(obj)
return obj.token
},
fetchByToken: async (token) => {
token = await Models.OAuth2RefreshToken.query().where('token', token)
if (!token.length) return null
return token[0]
},
removeByUserIdClientId: async (userId, clientId) => {
return Models.OAuth2RefreshToken.query().delete().where('user_id', userId)
.andWhere('client_id', clientId)
},
removeByRefreshToken: async (token) => {
return Models.OAuth2RefreshToken.query().delete().where('token', token)
},
getUserId: (refreshToken) => {
return refreshToken.user_id
},
getClientId: (refreshToken) => {
return refreshToken.client_id
},
getScope: (refreshToken) => {
return refreshToken.scope
}
}, },
user: { fetchByUserIdClientId: async (userId, clientId) => {
getId: (user) => { const tkn = await Models.OAuth2AccessToken.query().where('user_id', userId).andWhere('client_id', clientId)
return user.id
},
fetchById: Users.User.get,
fetchByUsername: Users.User.get,
checkPassword: Users.User.Login.password,
fetchFromRequest: async (req) => {
if (!req.session.user) return null
const banStatus = await Users.User.getBanStatus(req.session.user.id)
if (banStatus.length) { if (!tkn.length) return null
delete req.session.user
return null
}
return req.session.user return tkn[0]
},
consented: async (userId, clientId, scope) => {
if (typeof scope === 'object') {
scope = scope.join(' ')
}
const authorized = await Models.OAuth2AuthorizedClient.query().where('user_id', userId)
if (!authorized.length) return false
let correct = false
for (const i in authorized) {
if (authorized[i].client_id === clientId) {
correct = authorized[i]
}
}
if (correct) {
if (correct.scope !== scope) {
await Models.OAuth2AuthorizedClient.query().delete().where('user_id', userId)
.andWhere('client_id', correct.client_id)
return false
}
correct = true
}
return correct
},
consent: async (userId, clientId, scope) => {
if (!config.oauth2.save_decision) return true
if (typeof scope === 'object') {
scope = scope.join(' ')
}
const obj = {
scope,
user_id: userId,
client_id: clientId,
created_at: new Date()
}
await Models.OAuth2AuthorizedClient.query().insert(obj)
return true
}
} }
} }
module.exports = OAuthDB export const client = {
getId: (c) => {
return c.id
},
fetchById: async (id) => {
const c = await Models.OAuth2Client.query().where('id', id)
if (!c.length) return null
return c[0]
},
checkSecret: (c, secret) => {
return c.secret === secret
},
checkGrantType: (c, grant) => {
if (c.grants.indexOf(grant) !== -1) {
return true
}
return false
},
getRedirectUri: (c) => {
return c.redirect_url
},
checkRedirectUri: (c, redirectUri) => {
return (redirectUri.indexOf(client.getRedirectUri(c)) === 0 &&
redirectUri.replace(client.getRedirectUri(c), '').indexOf('#') === -1)
},
transformScope: (scope) => {
if (!scope) return []
if (typeof scope === 'object') {
return scope
}
scope = scope.trim()
if (scope.indexOf(',') !== -1) {
scope = scope.split(',')
} else {
scope = scope.split(' ')
}
return scope
},
checkScope: (c, scope) => {
if (!scope) return []
if (typeof scope === 'string') {
scope = c.transformScope(scope)
}
const clientScopes = c.scope.split(' ')
for (const i in scope) {
if (clientScopes.indexOf(scope[i]) === -1) {
return false
}
}
return scope
}
}
export const code = {
ttl: config.oauth2.code_life,
create: async (userId, clientId, scope, ttl, special = false) => {
const newCode = crypto.randomBytes(config.oauth2.token_length).toString('hex')
const expr = new Date(Date.now() + ttl * 1000)
if (typeof scope === 'object') {
scope = scope.join(' ')
}
// Delete already existing codes with this exact user id, client id and scope, because it will
// eventually pile up and flood the database, especially when they were never used.
await Models.OAuth2Code.query().delete().where('user_id', userId).andWhere('client_id', clientId)
const obj = {
code: newCode,
scope,
user_id: userId,
client_id: clientId,
expires_at: expr,
created_at: new Date()
}
await Models.OAuth2Code.query().insert(obj)
return obj.code
},
fetchByCode: async (c) => {
c = await Models.OAuth2Code.query().where('code', c)
if (!c.length) return null
return c[0]
},
removeByCode: async (c) => {
if (typeof c === 'object') {
c = c.code
}
return Models.OAuth2Code.query().delete().where('code', c)
},
getUserId: (c) => {
return c.user_id
},
getClientId: (c) => {
return c.client_id
},
getScope: (c) => {
return c.scope
},
checkTTL: (c) => {
return (c.expires_at > Date.now())
}
}
export const refreshToken = {
create: async (userId, clientId, scope) => {
const token = crypto.randomBytes(config.oauth2.token_length).toString('hex')
if (typeof scope === 'object') {
scope = scope.join(' ')
}
const obj = {
token,
scope,
user_id: userId,
client_id: clientId,
created_at: new Date()
}
await Models.OAuth2RefreshToken.query().insert(obj)
return obj.token
},
fetchByToken: async (token) => {
token = await Models.OAuth2RefreshToken.query().where('token', token)
if (!token.length) return null
return token[0]
},
removeByUserIdClientId: async (userId, clientId) => {
return Models.OAuth2RefreshToken.query().delete().where('user_id', userId)
.andWhere('client_id', clientId)
},
removeByRefreshToken: async (token) => {
return Models.OAuth2RefreshToken.query().delete().where('token', token)
},
getUserId: (t) => {
return t.user_id
},
getClientId: (t) => {
return t.client_id
},
getScope: (t) => {
return t.scope
}
}
export const user = {
getId: (u) => {
return u.id
},
fetchById: User.get,
fetchByUsername: User.get,
checkPassword: Login.password,
fetchFromRequest: async (req) => {
if (!req.session.user) return null
const banStatus = await User.getBanStatus(req.session.user.id)
if (banStatus.length) {
delete req.session.user
return null
}
return req.session.user
},
consented: async (userId, clientId, scope) => {
if (typeof scope === 'object') {
scope = scope.join(' ')
}
const authorized = await Models.OAuth2AuthorizedClient.query().where('user_id', userId)
if (!authorized.length) return false
let correct = false
for (const i in authorized) {
if (authorized[i].client_id === clientId) {
correct = authorized[i]
}
}
if (correct) {
if (correct.scope !== scope) {
await Models.OAuth2AuthorizedClient.query().delete().where('user_id', userId)
.andWhere('client_id', correct.client_id)
return false
}
correct = true
}
return correct
},
consent: async (userId, clientId, scope) => {
if (!config.oauth2.save_decision) return true
if (typeof scope === 'object') {
scope = scope.join(' ')
}
const obj = {
scope,
user_id: userId,
client_id: clientId,
created_at: new Date()
}
await Models.OAuth2AuthorizedClient.query().insert(obj)
return true
}
}

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,17 +19,17 @@ 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")
button.btn.btn-primary(type="submit") Log in button.btn.btn-primary(type="submit") Log in
| or | or
a(href="/register") Create an account a(href="/register") Create an account
| · | ·
a(href="/login/reset") Forgot password? a(href="/login/reset") Forgot password?
if auth if auth
| · | ·
a.show-more(href="#",data-toggle="#extlogins") More options.. a.show-more(href="#",data-toggle="#extlogins") More options..
.show-more-cnt.mt-4#extlogins .show-more-cnt.mt-4#extlogins
if !registrations if !registrations

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: {