Merge pull request #2 from IcyNet/dev

Some internal changes
This commit is contained in:
Evert Prants 2017-12-01 21:50:51 +02:00 committed by GitHub
commit 5575e9e4f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 603 additions and 507 deletions

26
package-lock.json generated
View File

@ -2104,6 +2104,16 @@
"version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", "version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
"integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0="
}, },
"fs-extra": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz",
"integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=",
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "4.0.0",
"universalify": "0.1.1"
}
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2269,8 +2279,7 @@
"graceful-fs": { "graceful-fs": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
"dev": true
}, },
"graceful-readlink": { "graceful-readlink": {
"version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
@ -2822,6 +2831,14 @@
"version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
}, },
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "4.1.11"
}
},
"jsonify": { "jsonify": {
"version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
@ -5082,6 +5099,11 @@
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
"dev": true "dev": true
}, },
"universalify": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"unpipe": { "unpipe": {
"version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="

View File

@ -41,6 +41,7 @@
"express": "^4.15.3", "express": "^4.15.3",
"express-rate-limit": "^2.9.0", "express-rate-limit": "^2.9.0",
"express-session": "^1.15.3", "express-session": "^1.15.3",
"fs-extra": "^4.0.2",
"gm": "^1.23.0", "gm": "^1.23.0",
"knex": "^0.13.0", "knex": "^0.13.0",
"multiparty": "^4.1.3", "multiparty": "^4.1.3",

View File

@ -1,17 +0,0 @@
import path from 'path'
import Promise from 'bluebird'
import fs from 'fs'
const access = Promise.promisify(fs.access)
async function exists (fpath) {
try {
await access(path.resolve(fpath))
} catch (e) {
return false
}
return true
}
module.exports = exists

View File

@ -1,9 +1,7 @@
import config from './load-config' import config from './load-config'
import path from 'path' import path from 'path'
import util from 'util' import util from 'util'
import fs from 'fs-extra'
import Promise from 'bluebird'
const fs = Promise.promisifyAll(require('fs'))
let lfs let lfs
@ -21,16 +19,15 @@ function dateFormat (date) {
} }
// Console.log/error/warn "middleware" - add timestamp and write to file // Console.log/error/warn "middleware" - add timestamp and write to file
function stampAndWrite (fnc, prfx, message) { function stampAndWrite (fnc, color, prfx, message) {
let prefix = '[' + prfx + '] [' + dateFormat(new Date()) + '] ' let prefix = '[' + prfx + '] [' + dateFormat(new Date()) + '] '
message = prefix + message message = color + prefix + message
message = message.replace(/\\u001b/g, '\x1b')
if (lfs) { if (lfs) {
lfs.write(message + '\n') lfs.write(message.replace(/(\u001b\[\d\d?m)/g, '') + '\n')
} }
message = message.replace(/\\u001b/g, '\x1b')
fnc.call(this, message) fnc.call(this, message)
} }
@ -38,26 +35,26 @@ function stampAndWrite (fnc, prfx, message) {
const realConsoleLog = console.log const realConsoleLog = console.log
console.log = function () { console.log = function () {
let message = util.format.apply(null, arguments) let message = util.format.apply(null, arguments)
stampAndWrite.call(this, realConsoleLog, 'info', message) stampAndWrite.call(this, realConsoleLog, '', 'info', message)
} }
const realConsoleWarn = console.warn const realConsoleWarn = console.warn
console.warn = function () { console.warn = function () {
let message = util.format.apply(null, arguments) let message = util.format.apply(null, arguments)
stampAndWrite.call(this, realConsoleWarn, 'warn', message) stampAndWrite.call(this, realConsoleWarn, '\x1b[33m', 'warn', message)
} }
const realConsoleError = console.error const realConsoleError = console.error
console.error = function () { console.error = function () {
let message = util.format.apply(null, arguments) let message = util.format.apply(null, arguments)
stampAndWrite.call(this, realConsoleError, ' err', message) stampAndWrite.call(this, realConsoleError, '\x1b[31m', ' err', message)
} }
async function initializeLogger () { async function initializeLogger () {
let logPath = path.resolve(config.logger.file) let logPath = path.resolve(config.logger.file)
try { try {
await fs.accessAsync(logPath, fs.W_OK) await fs.access(logPath, fs.W_OK)
lfs = fs.createWriteStream(logPath, {flags: 'a'}) lfs = fs.createWriteStream(logPath, {flags: 'a'})
} catch (e) { } catch (e) {
lfs = null lfs = null

View File

@ -84,7 +84,7 @@ const API = {
getAllUsers: async function (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' } return { error: 'No users found in database' }
} }
count = count[0].ids count = count[0].ids
@ -108,7 +108,7 @@ const API = {
getAllClients: async function (page) { getAllClients: async function (page) {
let count = await Models.OAuth2Client.query().count('id as ids') let count = await Models.OAuth2Client.query().count('id as ids')
if (!count.length || !count[0]['ids'] || isNaN(page)) { if (!count.length || !count[0]['ids'] || isNaN(page)) {
return { error: 'No clients found' } return { error: 'No clients' }
} }
count = count[0].ids count = count[0].ids
@ -141,7 +141,7 @@ const API = {
] ]
data = dataFilter(data, fields, ['scope', 'verified']) data = dataFilter(data, fields, ['scope', 'verified'])
if (!data) return { error: 'Missing fields' } if (!data) throw new Error('Missing fields')
data.verified = (data.verified != null ? 1 : 0) data.verified = (data.verified != null ? 1 : 0)
@ -149,20 +149,20 @@ const API = {
await Models.OAuth2Client.query().patchAndFetchById(id, data) await Models.OAuth2Client.query().patchAndFetchById(id, data)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
} catch (e) { } catch (e) {
return { error: 'No such client' } throw new Error('No such client')
} }
return {} return {}
}, },
// Create a new secret for a client // Create a new secret for a client
newSecret: async function (id) { newSecret: async function (id) {
if (isNaN(id)) return { error: 'Invalid client ID' } if (isNaN(id)) throw new Error('Invalid client ID')
let secret = Users.Hash(16) let secret = Users.Hash(16)
try { try {
await Models.OAuth2Client.query().patchAndFetchById(id, {secret: secret}) await Models.OAuth2Client.query().patchAndFetchById(id, {secret: secret})
} catch (e) { } catch (e) {
return { error: 'No such client' } throw new Error('No such client')
} }
return {} return {}
@ -174,7 +174,7 @@ const API = {
] ]
data = dataFilter(data, fields, ['scope']) data = dataFilter(data, fields, ['scope'])
if (!data) return { error: 'Missing fields' } if (!data) throw new Error('Missing fields')
let obj = Object.assign({ let obj = Object.assign({
secret: Users.Hash(16), secret: Users.Hash(16),
@ -187,7 +187,7 @@ const API = {
}, },
// Remove a client and all associated data // Remove a client and all associated data
removeClient: async function (id) { removeClient: async function (id) {
if (isNaN(id)) return {error: 'Invalid ID number'} if (isNaN(id)) throw new Error('Invalid ID number')
await Models.OAuth2Client.query().delete().where('id', id) await Models.OAuth2Client.query().delete().where('id', id)
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
await Models.OAuth2AccessToken.query().delete().where('client_id', id) await Models.OAuth2AccessToken.query().delete().where('client_id', id)
@ -198,7 +198,7 @@ const API = {
getAllBans: async function (page) { getAllBans: async function (page) {
let count = await Models.Ban.query().count('id as ids') let count = await Models.Ban.query().count('id as ids')
if (!count.length || !count[0]['ids'] || isNaN(page)) { if (!count.length || !count[0]['ids'] || isNaN(page)) {
return {error: 'No bans on record'} return { error: 'No bans on record' }
} }
count = count[0].ids count = count[0].ids
@ -225,12 +225,12 @@ const API = {
addBan: async function (data, adminId) { addBan: async function (data, adminId) {
let user = await Users.User.get(parseInt(data.user_id)) let user = await Users.User.get(parseInt(data.user_id))
if (!user) return {error: 'No such user.'} if (!user) throw new Error('No such user.')
if (user.id === adminId) return {error: 'Cannot ban yourself!'} if (user.id === adminId) throw new Error('Cannot ban yourself!')
let admin = await Users.User.get(adminId) let admin = await Users.User.get(adminId)
if (user.nw_privilege > admin.nw_privilege) return {error: 'Cannot ban user.'} if (user.nw_privilege > admin.nw_privilege) throw new Error('Cannot ban user.')
let banAdd = { let banAdd = {
reason: data.reason || 'Unspecified ban', reason: data.reason || 'Unspecified ban',

View File

@ -1,6 +1,7 @@
import {EmailTemplate} from 'email-templates' import {EmailTemplate} from 'email-templates'
import path from 'path' import path from 'path'
import nodemailer from 'nodemailer' import nodemailer from 'nodemailer'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
const templateDir = path.join(__dirname, '../../', 'templates') const templateDir = path.join(__dirname, '../../', 'templates')

View File

@ -1,20 +1,27 @@
import qs from 'querystring'
import oauth from 'oauth-libre'
import uuidV1 from 'uuid/v1'
import crypto from 'crypto'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import http from '../../scripts/http' import http from '../../scripts/http'
import models from './models' import models from './models'
import Image from './image'
import UAPI from './index' import UAPI from './index'
import qs from 'querystring'
import oauth from 'oauth-libre'
import path from 'path'
import url from 'url'
import uuidV1 from 'uuid/v1'
const imgdir = path.join(__dirname, '../../', 'usercontent', 'images') const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address']
let twitterApp let twitterApp
let discordApp let discordApp
const API = { const API = {
Common: { Common: {
// Generate a hash based on the current session
stateGenerator: (req) => {
let sessionCrypto = req.session.id + ':' + config.server.session_secret
return crypto.createHash('sha256').update(sessionCrypto).digest('hex')
},
// Find an user with an external ID
getExternal: async (service, identifier) => { getExternal: async (service, identifier) => {
let extr = await models.External.query().where('service', service).andWhere('identifier', identifier) let extr = await models.External.query().where('service', service).andWhere('identifier', identifier)
if (!extr || !extr.length) return null if (!extr || !extr.length) return null
@ -30,10 +37,12 @@ const API = {
return extr return extr
}, },
// Get user ban status
getBan: async (user, ipAddress) => { getBan: async (user, ipAddress) => {
let banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null) let banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null)
return banList return banList
}, },
// Create a new `external` instance for a user
new: async (service, identifier, user) => { new: async (service, identifier, user) => {
let data = { let data = {
user_id: user.id, user_id: user.id,
@ -45,6 +54,7 @@ const API = {
await await models.External.query().insert(data) await await models.External.query().insert(data)
return true return true
}, },
// Create a new user
newUser: async (service, identifier, data) => { newUser: async (service, identifier, data) => {
let udataLimited = Object.assign({ let udataLimited = Object.assign({
activated: 1, activated: 1,
@ -53,27 +63,38 @@ const API = {
uuid: uuidV1() uuid: uuidV1()
}, data) }, 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 // Check if the username is already taken
if (await UAPI.User.get(udataLimited.username) != null) { if (await UAPI.User.get(udataLimited.username) != null || udataLimited.username.length < 4) {
udataLimited.username = udataLimited.username + UAPI.Hash(4) udataLimited.username = udataLimited.username + UAPI.Hash(4)
} }
// Check if the email given to us is already registered, if so, // Check if the email given to us is already registered, if so,
// associate an external node with the user bearing the email // tell them to log in first.
if (udataLimited.email && udataLimited.email !== '') { if (udataLimited.email && udataLimited.email !== '') {
let getByEmail = await UAPI.User.get(udataLimited.email) let getByEmail = await UAPI.User.get(udataLimited.email)
if (getByEmail) { if (getByEmail) {
await API.Common.new(service, identifier, 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.')
return {error: null, user: getByEmail}
} }
} }
// Create a new user based on the information we got from Facebook // Create a new user based on the information we got from an external service
let newUser = await models.User.query().insert(udataLimited) let newUser = await models.User.query().insert(udataLimited)
await API.Common.new(service, identifier, newUser) await API.Common.new(service, identifier, newUser)
return newUser return newUser
}, },
// Remove an `external` object (thus unlinking from a service)
remove: async (user, service) => { remove: async (user, service) => {
user = await UAPI.User.ensureObject(user, ['password']) user = await UAPI.User.ensureObject(user, ['password'])
let userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id) let userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
@ -89,30 +110,85 @@ const API = {
return models.External.query().delete().where('user_id', user.id).andWhere('service', service) return models.External.query().delete().where('user_id', user.id).andWhere('service', service)
}, },
saveAvatar: async (avatarUrl) => { // Common code for all auth callbacks
if (!avatarUrl) return null callback: async (identifier, uid, user, ipAddress, remoteData, avatarFunc) => {
let imageName = 'download-' + UAPI.Hash(12) let exists = await API.Common.getExternal(identifier, uid)
let uridata = url.parse(avatarUrl)
let pathdata = path.parse(uridata.path)
imageName += pathdata.ext || '.png' if (user) {
// Get bans for user
let bans = await API.Common.getBan(user)
if (bans.length) return { banned: bans, ip: false }
try { if (exists) return {error: null, user: user}
await http.Download(avatarUrl, path.join(imgdir, imageName))
} catch (e) { await API.Common.new(identifier, uid, user)
return null return {error: null, user: user}
} }
return {fileName: imageName} // Callback succeeded with user id and the external table exists, we log in the user
if (exists) {
// Get bans for user
let 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
let 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
let newUData = Object.assign({
email: remoteData.email || '',
avatar_file: avatar,
ip_address: ipAddress
}, remoteData)
// Remove unnecessary fields
for (let 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: { Facebook: {
callback: async (user, data) => { getAvatar: async (rawData) => {
if (!data.authResponse || data.status !== 'connected') { let profilepic = null
if (rawData.picture) {
if (rawData.picture.is_silhouette === false && rawData.picture.url) {
let imgdata = await Image.downloadImage(rawData.picture.url)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
}
return profilepic
},
callback: async (user, authResponse, ipAddress) => {
if (!authResponse) {
return {error: 'No Authorization'} return {error: 'No Authorization'}
} }
let uid = data.authResponse.userID let uid = authResponse.userID
if (!uid) { if (!uid) {
return {error: 'No Authorization'} return {error: 'No Authorization'}
} }
@ -120,7 +196,7 @@ const API = {
// Get facebook user information in order to create a new user or verify // Get facebook user information in order to create a new user or verify
let fbdata let fbdata
let intel = { let intel = {
access_token: data.authResponse.accessToken, access_token: authResponse.accessToken,
fields: 'name,email,picture,short_name' fields: 'name,email,picture,short_name'
} }
@ -135,57 +211,28 @@ const API = {
return {error: fbdata.error.message} return {error: fbdata.error.message}
} }
let exists = await API.Common.getExternal('fb', uid) let cleanedData = Object.assign(fbdata, {
if (user) {
// Get bans for user
let 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('fb', 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
let 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
let bans = await API.Common.getBan(null, data.ip_address)
if (bans.length) return { banned: bans, ip: true }
// Determine profile picture
let profilepic = null
if (fbdata.picture) {
if (fbdata.picture.is_silhouette === false && fbdata.picture.url) {
let imgdata = await API.Common.saveAvatar(fbdata.picture.url)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
}
let newUData = {
username: fbdata.short_name || 'FB' + UAPI.Hash(4), username: fbdata.short_name || 'FB' + UAPI.Hash(4),
display_name: fbdata.name, display_name: fbdata.name,
email: fbdata.email || '', email: fbdata.email || ''
avatar_file: profilepic, })
ip_address: data.ip_address
}
let newUser = await API.Common.newUser('fb', uid, newUData) return API.Common.callback('facebook', uid, user, ipAddress, cleanedData, API.Facebook.getAvatar)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
} }
}, },
Twitter: { Twitter: {
getAvatar: async function (rawData) {
let profilepic = null
if (rawData.profile_image_url_https) {
let imgdata = await Image.downloadImage(rawData.profile_image_url_https)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
return profilepic
},
oauthApp: function () { oauthApp: function () {
if (!twitterApp) { if (!twitterApp) {
let redirectUri = config.server.domain + '/api/external/twitter/callback' let redirectUri = config.server.domain + '/api/external/twitter/callback'
@ -243,56 +290,28 @@ const API = {
} }
let uid = twdata.id_str let uid = twdata.id_str
let exists = await API.Common.getExternal('twitter', uid)
if (user) { let cleanedData = Object.assign(twdata, {
// Get bans for user username: twdata.screen_name,
let bans = await API.Common.getBan(user) display_name: twdata.name,
if (bans.length) return { banned: bans, ip: false } email: twdata.email || ''
})
if (exists) return {error: null, user: user} return API.Common.callback('twitter', uid, user, ipAddress, cleanedData, API.Twitter.getAvatar)
}
await API.Common.new('twitter', uid, user) },
return {error: null, user: user} Google: {
} getAvatar: async (rawData) => {
// Callback succeeded with user id and the external table exists, we log in the user
if (exists) {
// Get bans for user
let 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
let bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Determine profile picture
let profilepic = null let profilepic = null
if (twdata.profile_image_url_https) { if (rawData.image) {
let imgdata = await API.Common.saveAvatar(twdata.profile_image_url_https) let imgdata = await Image.downloadImage(rawData.image)
if (imgdata && imgdata.fileName) { if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName profilepic = imgdata.fileName
} }
} }
// Create a new user return profilepic
let newUData = { },
username: twdata.screen_name,
display_name: twdata.name,
email: twdata.email || '',
avatar_file: profilepic,
ip_address: ipAddress
}
let newUser = await API.Common.newUser('twitter', uid, newUData)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
}
},
Google: {
callback: async (user, data, ipAddress) => { callback: async (user, data, ipAddress) => {
let uid let uid
@ -312,56 +331,32 @@ const API = {
return {error: e.message} return {error: e.message}
} }
let exists = await API.Common.getExternal('google', uid) let cleanedData = Object.assign(data, {
username: data.name,
if (user) {
// Get bans for user
let 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('google', 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
let 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
let bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Determine profile picture
let profilepic = null
if (data.image) {
let imgdata = await API.Common.saveAvatar(data.image)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
// Create a new user
let newUData = {
username: data.name.replace(/\W+/gi, ''),
display_name: data.name, display_name: data.name,
email: data.email || '', email: data.email || ''
avatar_file: profilepic, })
ip_address: ipAddress
}
let newUser = await API.Common.newUser('google', uid, newUData) return API.Common.callback('google', uid, user, ipAddress, cleanedData, API.Google.getAvatar)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
} }
}, },
Discord: { Discord: {
getAvatar: async (rawData) => {
let profilepic = null
let aviSnowflake = rawData.avatar
if (aviSnowflake) {
try {
let 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
},
oauth2App: function () { oauth2App: function () {
if (discordApp) return if (discordApp) return
discordApp = new oauth.PromiseOAuth2( discordApp = new oauth.PromiseOAuth2(
@ -374,9 +369,9 @@ const API = {
discordApp.useAuthorizationHeaderforGET(true) discordApp.useAuthorizationHeaderforGET(true)
}, },
getAuthorizeURL: function () { getAuthorizeURL: function (req) {
if (!discordApp) API.Discord.oauth2App() if (!discordApp) API.Discord.oauth2App()
let state = UAPI.Hash(6) let state = API.Common.stateGenerator(req)
let redirectUri = config.server.domain + '/api/external/discord/callback' let redirectUri = config.server.domain + '/api/external/discord/callback'
const params = { const params = {
@ -421,58 +416,14 @@ const API = {
} }
let uid = ddata.id let uid = ddata.id
let exists = await API.Common.getExternal('discord', uid)
if (user) {
// Get bans for user
let 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('discord', 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
let 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
let bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Download profile picture
let profilepic = null
let aviSnowflake = ddata.avatar
if (aviSnowflake) {
try {
let avpt = await API.Common.saveAvatar('https://cdn.discordapp.com/avatars/' + ddata.id + '/' + aviSnowflake + '.png')
if (avpt && avpt.fileName) {
profilepic = avpt.fileName
}
} catch (e) {
profilepic = null
}
}
// Create a new user // Create a new user
let newUData = { let cleanedData = Object.assign(ddata, {
username: ddata.username.replace(/\W+/gi, '_'),
display_name: ddata.username, display_name: ddata.username,
email: ddata.email || '', email: ddata.email || ''
avatar_file: profilepic, })
ip_address: ipAddress
}
let newUser = await API.Common.newUser('discord', uid, newUData) return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
} }
} }
} }

View File

@ -1,9 +1,11 @@
import gm from 'gm' import gm from 'gm'
import url from 'url'
import path from 'path' import path from 'path'
import crypto from 'crypto' import uuid from 'uuid/v4'
import Promise from 'bluebird'
const fs = Promise.promisifyAll(require('fs')) import http from '../../scripts/http'
const fs = require('fs-extra')
const uploads = path.join(__dirname, '../../', 'usercontent') const uploads = path.join(__dirname, '../../', 'usercontent')
const images = path.join(uploads, 'images') const images = path.join(uploads, 'images')
@ -42,8 +44,8 @@ function saneFields (fields) {
} }
async function bailOut (file, error) { async function bailOut (file, error) {
await fs.unlinkAsync(file) await fs.unlink(file)
return { error: error } throw new Error(error)
} }
async function imageBase64 (baseObj) { async function imageBase64 (baseObj) {
@ -53,7 +55,7 @@ async function imageBase64 (baseObj) {
if (!imgData) return null if (!imgData) return null
if (!imageTypes[imgData.type]) return null if (!imageTypes[imgData.type]) return null
let imageName = 'base64-' + crypto.randomBytes(12).toString('hex') let imageName = 'base64-' + uuid()
let ext = imageTypes[imgData.type] || '.png' let ext = imageTypes[imgData.type] || '.png'
imageName += ext imageName += ext
@ -61,7 +63,7 @@ async function imageBase64 (baseObj) {
let fpath = path.join(images, imageName) let fpath = path.join(images, imageName)
try { try {
await fs.writeFileAsync(fpath, imgData.data) await fs.writeFile(fpath, imgData.data)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return null return null
@ -70,8 +72,27 @@ async function imageBase64 (baseObj) {
return {file: fpath} return {file: fpath}
} }
async function downloadImage (imgUrl, designation) {
if (!imgUrl) return null
if (!designation) designation = 'download'
let imageName = designation + '-' + uuid()
let uridata = url.parse(imgUrl)
let pathdata = path.parse(uridata.path)
imageName += pathdata.ext || '.png'
try {
await http.Download(imgUrl, path.join(images, imageName))
} catch (e) {
return null
}
return imageName
}
async function uploadImage (identifier, fields, files) { async function uploadImage (identifier, fields, files) {
if (!files.image) return {error: 'No image file'} if (!files.image) throw new Error('No image file')
let file = files.image[0] let file = files.image[0]
if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max') if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max')
@ -132,7 +153,7 @@ async function uploadImage (identifier, fields, files) {
}) })
}) })
await fs.unlinkAsync(file) await fs.unlink(file)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return bailOut(file, 'An error occured while cropping.') return bailOut(file, 'An error occured while cropping.')
@ -142,6 +163,7 @@ async function uploadImage (identifier, fields, files) {
} }
module.exports = { module.exports = {
downloadImage: downloadImage,
uploadImage: uploadImage, uploadImage: uploadImage,
imageBase64: imageBase64, imageBase64: imageBase64,
types: imageTypes types: imageTypes

View File

@ -1,17 +1,15 @@
import path from 'path' import path from 'path'
import cprog from 'child_process' import cprog from 'child_process'
import config from '../../scripts/load-config'
import http from '../../scripts/http'
import exists from '../../scripts/existsSync'
import models from './models'
import crypto from 'crypto' import crypto from 'crypto'
import notp from 'notp' import notp from 'notp'
import base32 from 'thirty-two' import base32 from 'thirty-two'
import emailer from './emailer'
import uuidV1 from 'uuid/v1' import uuidV1 from 'uuid/v1'
import fs from 'fs-extra'
import Promise from 'bluebird' import config from '../../scripts/load-config'
const fs = Promise.promisifyAll(require('fs')) import http from '../../scripts/http'
import models from './models'
import emailer from './emailer'
const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
@ -178,7 +176,7 @@ const API = {
}, },
update: async function (user, data) { update: async function (user, data) {
user = await API.User.ensureObject(user) user = await API.User.ensureObject(user)
if (!user) return {error: 'No such user.'} if (!user) throw new Error('No such user.')
data = Object.assign({ data = Object.assign({
updated_at: new Date() updated_at: new Date()
@ -191,20 +189,20 @@ const API = {
let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images') let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
let pathOf = path.join(uploadsDir, fileName) let pathOf = path.join(uploadsDir, fileName)
if (!await exists(pathOf)) { if (!await fs.exists(pathOf)) {
return {error: 'No such file'} throw new Error('No such file')
} }
// Delete previous upload // Delete previous upload
if (user.avatar_file != null) { if (user.avatar_file != null) {
let file = path.join(uploadsDir, user.avatar_file) let file = path.join(uploadsDir, user.avatar_file)
if (await exists(file)) { if (await fs.exists(file)) {
await fs.unlinkAsync(file) await fs.unlink(file)
} }
} }
await API.User.update(user, {avatar_file: fileName}) await API.User.update(user, {avatar_file: fileName})
return { file: fileName } return fileName
}, },
removeAvatar: async function (user) { removeAvatar: async function (user) {
user = await API.User.ensureObject(user, ['avatar_file']) user = await API.User.ensureObject(user, ['avatar_file'])
@ -212,8 +210,8 @@ const API = {
if (!user.avatar_file) return {} if (!user.avatar_file) return {}
let file = path.join(uploadsDir, user.avatar_file) let file = path.join(uploadsDir, user.avatar_file)
if (await exists(file)) { if (await fs.exists(file)) {
await fs.unlinkAsync(file) await fs.unlink(file)
} }
return API.User.update(user, {avatar_file: null}) return API.User.update(user, {avatar_file: null})
@ -252,14 +250,18 @@ const API = {
return bcryptTask({task: 'compare', password: password, hash: user.password}) return bcryptTask({task: 'compare', password: password, hash: user.password})
}, },
activationToken: async function (token) { activationToken: async function (token) {
let getToken = await models.Token.query().where('token', token) let getToken = await models.Token.query().where('token', token).andWhere('type', 1)
if (!getToken || !getToken.length) return false if (!getToken || !getToken.length) return false
let user = await API.User.get(getToken[0].user_id) getToken = getToken[0]
if (getToken.expires_at && new Date(getToken.expires_at).getTime() < Date.now()) return false
let user = await API.User.get(getToken.user_id)
if (!user) return false if (!user) return false
await models.User.query().patchAndFetchById(user.id, {activated: 1}) await models.User.query().patchAndFetchById(user.id, {activated: 1})
await models.Token.query().delete().where('id', getToken[0].id) await models.Token.query().delete().where('id', getToken.id)
return true return true
}, },
totpTokenRequired: async function (user) { totpTokenRequired: async function (user) {
@ -359,12 +361,12 @@ const API = {
let userTest = await API.User.get(regdata.username) let userTest = await API.User.get(regdata.username)
if (userTest) { if (userTest) {
return {error: 'This username is already taken!'} throw new Error('This username is already taken!')
} }
let emailTest = await API.User.get(regdata.email) let emailTest = await API.User.get(regdata.email)
if (emailTest) { if (emailTest) {
return {error: 'This email address is already registered!'} throw new Error('This email address is already registered!')
} }
// Create user // Create user
@ -393,11 +395,75 @@ const API = {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
await models.User.query().delete().where('id', user.id) await models.User.query().delete().where('id', user.id)
return {error: 'Invalid email address!'} throw new Error('Invalid email address!')
} }
} }
return {error: null, user: user} return user
}
},
Reset: {
reset: async function (email, passRequired = true) {
let emailEnabled = config.email && config.email.enabled
if (!emailEnabled) throw new Error('Cannot reset password.')
let user = await API.User.get(email)
if (!user) throw new Error('This email address does not match any user in our database.')
if (!user.password && passRequired) throw new Error('The user associated with this email address has used an external website to log in, thus the password cannot be reset.')
let recentTokens = await models.Token.query().where('user_id', user.id).andWhere('expires_at', '>', new Date()).andWhere('type', 2)
if (recentTokens.length >= 2) {
throw new Error('You\'ve made too many reset requests recently. Please slow down.')
}
let resetToken = API.Hash(16)
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: resetToken,
user_id: user.id,
type: 2
})
// Send Reset Email
console.debug('Reset token:', resetToken)
if (email) {
try {
let em = await emailer.pushMail('reset_password', user.email, {
domain: config.server.domain,
display_name: user.display_name,
reset_token: resetToken
})
console.debug(em)
} catch (e) {
console.error(e)
throw new Error('Invalid email address!')
}
}
return resetToken
},
resetToken: async function (token) {
let getToken = await models.Token.query().where('token', token).andWhere('type', 2)
if (!getToken || !getToken.length) return null
getToken = getToken[0]
if (getToken.expires_at && new Date(getToken.expires_at).getTime() < Date.now()) return null
let user = await API.User.get(getToken.user_id)
if (!user) return null
return user
},
changePassword: async function (user, password, token) {
let hashed = await API.User.Register.hashPassword(password)
await models.User.query().patchAndFetchById(user.id, {password: hashed, updated_at: new Date()})
await models.Token.query().delete().where('token', token)
return true
} }
}, },
OAuth2: { OAuth2: {
@ -468,7 +534,6 @@ const API = {
console.debug('IPN Verified Notification:', body) console.debug('IPN Verified Notification:', body)
} }
// TODO: add database field for this
if (body.txn_id) { if (body.txn_id) {
if (txnStore.indexOf(body.txn_id) !== -1) return true if (txnStore.indexOf(body.txn_id) !== -1) return true
txnStore.push(body.txn_id) txnStore.push(body.txn_id)

View File

@ -1,6 +1,7 @@
import crypto from 'crypto'
import API from './index' import API from './index'
import Model from './models' import Model from './models'
import crypto from 'crypto'
const mAPI = { const mAPI = {
getToken: async function (user) { getToken: async function (user) {

View File

@ -53,7 +53,7 @@ const News = {
if (page < 1) page = 1 if (page < 1) page = 1
if (!count.length || !count[0]['ids'] || isNaN(page)) { if (!count.length || !count[0]['ids'] || isNaN(page)) {
return {error: 'No articles found'} return { error: 'No news' }
} }
count = count[0].ids count = count[0].ids
@ -100,7 +100,7 @@ const News = {
} }
let result = await Models.News.query().patchAndFetchById(id, patch) let result = await Models.News.query().patchAndFetchById(id, patch)
if (!result) return {error: 'Something went wrong.'} if (!result) throw new Error('Something went wrong.')
return {} return {}
} }
} }

View File

@ -15,7 +15,7 @@ const args = {
function spawnWorkers () { function spawnWorkers () {
let workerCount = config.server.workers === 0 ? cpuCount : config.server.workers let workerCount = config.server.workers === 0 ? cpuCount : config.server.workers
console.log('Spinning up ' + workerCount + ' worker process' + (workerCount !== 1 ? 'es' : '')) console.log('Spinning up %d worker process%s', workerCount, (workerCount !== 1 ? 'es' : ''))
for (let i = 0; i < workerCount; i++) { for (let i = 0; i < workerCount; i++) {
spawnWorker() spawnWorker()
@ -68,14 +68,14 @@ function spawnWorker (oldWorker) {
w.process.stderr.on('data', (data) => { w.process.stderr.on('data', (data) => {
console.log(w.process.pid, data.toString().trim()) console.log(w.process.pid, data.toString().trim())
}) })
args.verbose && console.log('Starting worker process ' + w.process.pid + '...') args.verbose && console.log('Starting worker process %d...', w.process.pid)
w.on('message', (message) => { w.on('message', (message) => {
if (message === 'started') { if (message === 'started') {
workers.push(w) workers.push(w)
args.verbose && console.log('Started worker process ' + w.process.pid) args.verbose && console.log('Started worker process', w.process.pid)
if (oldWorker) { if (oldWorker) {
args.verbose && console.log('Stopping worker process ' + oldWorker.process.pid) args.verbose && console.log('Stopping worker process', oldWorker.process.pid)
oldWorker.send('stop') oldWorker.send('stop')
} }
} else { } else {
@ -99,7 +99,7 @@ cluster.setupMaster({
cluster.on('exit', (worker, code, signal) => { cluster.on('exit', (worker, code, signal) => {
let extra = ((code || '') + ' ' + (signal || '')).trim() let extra = ((code || '') + ' ' + (signal || '')).trim()
console.error('Worker process ' + worker.process.pid + ' exited ' + (extra ? '(' + extra + ')' : '')) console.error('Worker process %d exited %s', worker.process.pid, (extra ? '(' + extra + ')' : ''))
let index = workers.indexOf(worker) let index = workers.indexOf(worker)

View File

@ -1,8 +1,9 @@
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 {User} from '../api'
import API from '../api/admin' import API from '../api/admin'
import {User} from '../api'
const router = express.Router() const router = express.Router()
const apiRouter = express.Router() const apiRouter = express.Router()
@ -128,10 +129,7 @@ apiRouter.post('/client/new', wrap(async (req, res) => {
return res.status(400).jsonp({error: 'Invalid session'}) return res.status(400).jsonp({error: 'Invalid session'})
} }
let update = await API.createClient(req.body, req.session.user) await API.createClient(req.body, req.session.user)
if (update.error) {
return res.status(400).jsonp({error: update.error})
}
res.status(204).end() res.status(204).end()
})) }))
@ -145,10 +143,7 @@ apiRouter.post('/client/update', wrap(async (req, res) => {
return res.status(400).jsonp({error: 'Invalid session'}) return res.status(400).jsonp({error: 'Invalid session'})
} }
let update = await API.updateClient(id, req.body) await API.updateClient(id, req.body)
if (update.error) {
return res.status(400).jsonp({error: update.error})
}
res.status(204).end() res.status(204).end()
})) }))
@ -160,9 +155,6 @@ apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => {
} }
let client = await API.newSecret(id) let client = await API.newSecret(id)
if (client.error) {
return res.status(400).jsonp({error: client.error})
}
res.jsonp(client) res.jsonp(client)
})) }))
@ -174,9 +166,6 @@ apiRouter.post('/client/delete/:id', wrap(async (req, res) => {
} }
let client = await API.removeClient(id) let client = await API.removeClient(id)
if (client.error) {
return res.status(400).jsonp({error: client.error})
}
res.jsonp(client) res.jsonp(client)
})) }))
@ -203,9 +192,6 @@ apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => {
} }
let ban = await API.removeBan(id) let ban = await API.removeBan(id)
if (ban.error) {
return res.status(400).jsonp({error: ban.error})
}
res.jsonp(ban) res.jsonp(ban)
})) }))
@ -217,16 +203,13 @@ apiRouter.post('/ban', wrap(async (req, res) => {
} }
let result = await API.addBan(req.body, req.session.user.id) let result = await API.addBan(req.body, req.session.user.id)
if (result.error) {
return res.status(400).jsonp({error: result.error})
}
res.jsonp(result) res.jsonp(result)
})) }))
apiRouter.use((err, req, res, next) => { apiRouter.use((err, req, res, next) => {
console.error(err) console.error(err)
return res.status(500).jsonp({error: 'Internal server error'}) return res.status(400).jsonp({error: err.message})
}) })
router.use('/api', apiRouter) router.use('/api', apiRouter)

View File

@ -1,12 +1,13 @@
import express from 'express' import express from 'express'
import RateLimit from 'express-rate-limit' import RateLimit from 'express-rate-limit'
import multiparty from 'multiparty' 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 API from '../api'
import News from '../api/news'
import Image from '../api/image'
import APIExtern from '../api/external' import APIExtern from '../api/external'
import Image from '../api/image'
import News from '../api/news'
import API from '../api'
let router = express.Router() let router = express.Router()
let dev = process.env.NODE_ENV !== 'production' let dev = process.env.NODE_ENV !== 'production'
@ -79,16 +80,34 @@ function JsonData (req, res, error, redirect = '/') {
res.jsonp({error: error, redirect: redirect}) res.jsonp({error: error, redirect: redirect})
} }
// Common middleware for all external account unlinks
function removeAuthMiddleware (identifier) {
return wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, identifier)
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
})
}
/** FACEBOOK LOGIN /** FACEBOOK LOGIN
* Ajax POST only <in-page javascript handeled> * Ajax POST only <in-page javascript handeled>
* No tokens saved in configs, everything works out-of-the-box * No tokens saved in configs, everything works out-of-the-box
*/ */
router.post('/external/facebook/callback', wrap(async (req, res, next) => { router.post('/external/facebook/callback', wrap(async (req, res, next) => {
if (!config.facebook || !config.facebook.client) return next() if (!config.facebook || !config.facebook.client) return next()
let sane = objectAssembler(req.body)
sane.ip_address = req.realIP
let response = await APIExtern.Facebook.callback(req.session.user, sane) // Fix up the retarded object Facebook sends us
let sane = objectAssembler(req.body)
if (!sane || !sane.authResponse) {
return next()
}
let response = await APIExtern.Facebook.callback(req.session.user, sane.authResponse, req.realIP)
if (response.banned) { if (response.banned) {
return JsonData(req, res, 'You are banned.') return JsonData(req, res, 'You are banned.')
@ -107,16 +126,7 @@ router.post('/external/facebook/callback', wrap(async (req, res, next) => {
JsonData(req, res, null, '/login') JsonData(req, res, null, '/login')
})) }))
router.get('/external/facebook/remove', wrap(async (req, res) => { router.get('/external/facebook/remove', removeAuthMiddleware('facebook'))
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'fb')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
/** TWITTER LOGIN /** TWITTER LOGIN
* OAuth1.0a flows * OAuth1.0a flows
@ -172,17 +182,7 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
res.render('redirect', {url: uri}) res.render('redirect', {url: uri})
})) }))
router.get('/external/twitter/remove', wrap(async (req, res) => { router.get('/external/twitter/remove', removeAuthMiddleware('twitter'))
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'twitter')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
/** DISCORD LOGIN /** DISCORD LOGIN
* OAuth2 flows * OAuth2 flows
@ -191,22 +191,16 @@ router.get('/external/twitter/remove', wrap(async (req, res) => {
router.get('/external/discord/login', wrap(async (req, res) => { router.get('/external/discord/login', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/') if (!config.discord || !config.discord.api) return res.redirect('/')
let infos = APIExtern.Discord.getAuthorizeURL() let infos = APIExtern.Discord.getAuthorizeURL(req)
req.session.discord_auth = {
state: infos.state
}
res.redirect(infos.url) res.redirect(infos.url)
})) }))
router.get('/external/discord/callback', wrap(async (req, res) => { router.get('/external/discord/callback', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/login') if (!config.discord || !config.discord.api) return res.redirect('/login')
if (!req.session.discord_auth) return res.redirect('/login')
let code = req.query.code let code = req.query.code
let state = req.query.state let state = req.query.state
let da = req.session.discord_auth
let uri = '/login' let uri = '/login'
if (!code) { if (!code) {
@ -214,7 +208,7 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
return res.redirect(uri) return res.redirect(uri)
} }
if (!state || state !== da.state) { if (!state || state !== APIExtern.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)
} }
@ -245,29 +239,7 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
res.render('redirect', {url: uri}) res.render('redirect', {url: uri})
})) }))
router.get('/external/discord/remove', wrap(async (req, res) => { router.get('/external/discord/remove', removeAuthMiddleware('discord'))
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'discord')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
router.get('/external/discord/remove', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'discord')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
/** GOOGLE LOGIN /** GOOGLE LOGIN
* Google Token Verification * Google Token Verification
@ -303,17 +275,7 @@ router.post('/external/google/callback', wrap(async (req, res) => {
JsonData(req, res, null, '/login') JsonData(req, res, null, '/login')
})) }))
router.get('/external/google/remove', wrap(async (req, res) => { router.get('/external/google/remove', removeAuthMiddleware('google'))
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'google')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
/* ======== /* ========
* NEWS * NEWS
@ -356,9 +318,10 @@ router.post('/news/edit/:id', wrap(async (req, res, next) => {
return res.status(400).jsonp({error: 'Content is required.'}) return res.status(400).jsonp({error: 'Content is required.'})
} }
let result = await News.edit(id, req.body) try {
if (result.error) { await News.edit(id, req.body)
return res.status(400).jsonp({error: result.error}) } catch (e) {
return res.status(400).jsonp({error: e.message})
} }
res.status(204).end() res.status(204).end()
@ -402,19 +365,19 @@ async function promiseForm (req) {
router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => { router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
let data = await promiseForm(req) let data = await promiseForm(req)
let result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
if (result.error) { let avatarFile
return res.status(400).jsonp({error: result.error})
try {
let result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
avatarFile = await API.User.changeAvatar(req.session.user, result.file)
} catch (e) {
return res.status(400).jsonp({error: e.message})
} }
let avatarUpdate = await API.User.changeAvatar(req.session.user, result.file) if (avatarFile) {
if (avatarUpdate.error) { req.session.user.avatar_file = avatarFile
return res.status(400).jsonp({error: avatarUpdate.error})
}
if (avatarUpdate.file) {
req.session.user.avatar_file = avatarUpdate.file
} }
res.status(200).jsonp({}) res.status(200).jsonp({})

View File

@ -1,10 +1,10 @@
import fs from 'fs' import fs from 'fs-extra'
import path from 'path' 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 exists from '../../scripts/existsSync'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import http from '../../scripts/http' import http from '../../scripts/http'
import API from '../api' import API from '../api'
@ -79,7 +79,7 @@ router.use(wrap(async (req, res, next) => {
} }
// Update user session // Update user session
let udata = await API.User.get(req.session.user.id) let udata = await API.User.get(req.session.user)
setSession(req, udata) setSession(req, udata)
// Update IP address // Update IP address
@ -135,8 +135,31 @@ function formKeep (req, res, next) {
next() next()
} }
// Password reset request endpoint
router.get('/login/reset', extraButtons, (req, res) => {
if (req.session.user) return redirectLogin(req, res)
res.render('user/reset_password', {sent: req.query.success != null})
})
// Password reset endpoint (emailed link)
router.get('/reset/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let success = await API.User.Reset.resetToken(token)
if (!success) {
req.flash('message', {error: true, text: 'Invalid or expired reset token.'})
res.redirect('/login')
return
}
res.render('user/password_new', {token: true})
}))
router.get('/login', extraButtons, (req, res) => { router.get('/login', extraButtons, (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) {
req.session.redirectUri = req.query.returnTo req.session.redirectUri = req.query.returnTo
} }
@ -154,6 +177,21 @@ router.get('/register', extraButtons, formKeep, (req, res) => {
res.render('user/register') res.render('user/register')
}) })
// User activation endpoint (emailed link)
router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let success = await API.User.Login.activationToken(token)
if (!success) {
req.flash('message', {error: true, text: 'Invalid or expired activation token.'})
} else {
req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'})
}
res.redirect('/login')
}))
// 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) => {
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user) let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
@ -205,9 +243,9 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
} }
if (config.facebook && config.facebook.client) { if (config.facebook && config.facebook.client) {
if (!socialStatus.enabled.fb) { if (!socialStatus.enabled.facebook) {
res.locals.facebook_auth = config.facebook.client res.locals.facebook_auth = config.facebook.client
} else if (socialStatus.source !== 'fb') { } else if (socialStatus.source !== 'facebook') {
res.locals.facebook_auth = false res.locals.facebook_auth = false
} }
} }
@ -270,23 +308,31 @@ function formError (req, res, error, redirect) {
// Make sure characters are UTF-8 // Make sure characters are UTF-8
function cleanString (input) { function cleanString (input) {
let output = '' let output = ''
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
output += input.charCodeAt(i) <= 127 ? input.charAt(i) : '' output += input.charCodeAt(i) <= 127 ? input.charAt(i) : ''
} }
return output return output
} }
// Enabling 2fa // Make sure CSRF tokens are present and valid in every form
router.post('/user/two-factor', wrap(async (req, res, next) => { function csrfValidation (req, res, next) {
if (!req.session.user) return next()
if (!req.body.code) {
return formError(req, res, 'You need to enter the code.')
}
if (req.body.csrf !== req.session.csrf) { if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.') return formError(req, res, 'Invalid session! Try reloading the page.')
} }
next()
}
// Enabling 2fa
router.post('/user/two-factor', csrfValidation, wrap(async (req, res, next) => {
if (!req.session.user) return next()
if (!req.body.code) {
return formError(req, res, 'You need to enter the code.')
}
let verified = await API.User.Login.totpCheck(req.session.user, req.body.code) let verified = await API.User.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.')
@ -296,11 +342,8 @@ router.post('/user/two-factor', wrap(async (req, res, next) => {
})) }))
// Disabling 2fa // Disabling 2fa
router.post('/user/two-factor/disable', wrap(async (req, res, next) => { router.post('/user/two-factor/disable', csrfValidation, wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
if (!req.body.password) { if (!req.body.password) {
return formError(req, res, 'Please enter your password.') return formError(req, res, 'Please enter your password.')
@ -315,17 +358,15 @@ router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
})) }))
// Verify 2FA for login // Verify 2FA for login
router.post('/login/verify', wrap(async (req, res, next) => { router.post('/login/verify', csrfValidation, wrap(async (req, res, next) => {
if (req.session.user) return next() if (req.session.user) return next()
if (req.session.totp_check === null) return res.redirect('/login') if (req.session.totp_check === null) return res.redirect('/login')
if (!req.body.code && !req.body.recovery) { if (!req.body.code && !req.body.recovery) {
return formError(req, res, 'You need to enter the code.') return formError(req, res, 'You need to enter the code.')
} }
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
let totpCheck = await API.User.Login.totpCheck(req.session.totp_check, req.body.code, req.body.recovery || false) let totpCheck = await API.User.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!')
@ -339,16 +380,13 @@ router.post('/login/verify', wrap(async (req, res, next) => {
})) }))
// Log the user in. Limited resource // Log the user in. Limited resource
router.post('/login', accountLimiter, wrap(async (req, res, next) => { router.post('/login', accountLimiter, csrfValidation, wrap(async (req, res, next) => {
if (req.session.user) return next() if (req.session.user) return next()
if (!req.body.username || !req.body.password || req.body.username === '') { if (!req.body.username || !req.body.password || req.body.username === '') {
return res.redirect('/login') return res.redirect('/login')
} }
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
let user = await API.User.get(req.body.username) let user = await API.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.')
@ -358,6 +396,7 @@ router.post('/login', accountLimiter, wrap(async (req, res, next) => {
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 (user.activated === 0) return formError(req, res, 'Please activate your account first.')
if (user.locked === 1) return formError(req, res, 'This account has been locked.') if (user.locked === 1) return formError(req, res, 'This account has been locked.')
// Check if the user is banned // Check if the user is banned
@ -389,15 +428,72 @@ router.post('/login', accountLimiter, wrap(async (req, res, next) => {
res.redirect(uri) res.redirect(uri)
})) }))
// Protected & Limited resource: Account registration // Password reset
router.post('/register', accountLimiter, wrap(async (req, res, next) => { router.post('/login/reset', accountLimiter, csrfValidation, wrap(async (req, res, next) => {
if (req.session.user) return next() if (req.session.user) return next()
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
return formError(req, res, 'Please fill in all the fields.') if (!req.body.email) {
return formError(req, res, 'You need to enter your email address.')
} }
if (req.body.csrf !== req.session.csrf) { let email = req.body.email
return formError(req, res, 'Invalid session! Try reloading the page.') let validEmail = await API.User.Register.validateEmail(email)
if (!validEmail) {
return formError(req, res, 'You need to enter a valid email address.')
}
try {
await API.User.Reset.reset(email)
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')
} catch (e) {
return formError(req, res, e.message)
}
}))
// Password reset endpoint (emailed link)
router.post('/reset/:token', csrfValidation, wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let user = await API.User.Reset.resetToken(token)
if (!user) {
req.flash('message', {error: true, text: 'Invalid or expired reset token.'})
res.redirect('/login')
return
}
// 4th Check: Password length
let password = req.body.password
if (!password || password.length < 8) {
return formError(req, res, 'Invalid password! Please use at least 8 characters!')
}
// 5th Check: Password match
let passwordAgain = req.body.password_repeat
if (!passwordAgain || password !== passwordAgain) {
return formError(req, res, 'Passwords do not match!')
}
try {
await API.User.Reset.changePassword(user, password, token)
console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP)
req.flash('message', {error: false, text: 'Your password has been changed successfully. You may now log in!'})
res.redirect('/login')
} catch (e) {
return formError(req, res, e.message)
}
}))
// Protected & Limited resource: Account registration
router.post('/register', accountLimiter, csrfValidation, wrap(async (req, res, next) => {
if (req.session.user) return next()
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
return formError(req, res, 'Please fill in all the fields.')
} }
// Ban check // Ban check
@ -458,18 +554,19 @@ router.post('/register', accountLimiter, wrap(async (req, res, next) => {
// Hash the password // Hash the password
let hash = await API.User.Register.hashPassword(password) let hash = await API.User.Register.hashPassword(password)
let newUser
// Attempt to create the user // Attempt to create the user
let newUser = await API.User.Register.newAccount({ try {
username: username, newUser = await API.User.Register.newAccount({
display_name: cleanString(displayName), username: username,
password: hash, display_name: cleanString(displayName),
email: email, password: hash,
ip_address: req.realIP email: email,
}) ip_address: req.realIP
})
if (!newUser || newUser.error != null) { } catch (e) {
return formError(req, res, newUser.error) return formError(req, res, e.message)
} }
// Do not include activation link message when the user is already activated // Do not include activation link message when the user is already activated
@ -483,20 +580,16 @@ router.post('/register', accountLimiter, wrap(async (req, res, next) => {
})) }))
// Change display name // Change display name
router.post('/user/manage', wrap(async (req, res, next) => { router.post('/user/manage', csrfValidation, wrap(async (req, res, next) => {
if (!req.session.user) return next() if (!req.session.user) return next()
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
if (!req.body.display_name) { if (!req.body.display_name) {
return formError(req, res, 'Display Name cannot be blank.') return formError(req, res, 'Display Name cannot be blank.')
} }
let displayName = req.body.display_name let displayName = req.body.display_name
if (!displayName || !displayName.match(/^([^\\`]{3,32})$/i)) { if (!displayName || !displayName.match(/^([^\\`]{3,32})$/i)) {
return formError(req, res, 'Invalid display name!') return formError(req, res, 'Invalid Display Name!')
} }
displayName = cleanString(displayName) displayName = cleanString(displayName)
@ -506,12 +599,12 @@ router.post('/user/manage', wrap(async (req, res, next) => {
return res.redirect('/user/manage') return res.redirect('/user/manage')
} }
let success = await API.User.update(req.session.user, { try {
display_name: displayName await API.User.update(req.session.user, {
}) display_name: displayName
})
if (success.error) { } catch (e) {
return formError(req, res, success.error) return formError(req, res, e.message)
} }
req.session.user.display_name = displayName req.session.user.display_name = displayName
@ -521,13 +614,9 @@ router.post('/user/manage', wrap(async (req, res, next) => {
})) }))
// Change user password // Change user password
router.post('/user/manage/password', accountLimiter, 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()
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
if (!req.body.password_old) { if (!req.body.password_old) {
return formError(req, res, 'Please enter your current password.') return formError(req, res, 'Please enter your current password.')
} }
@ -550,12 +639,12 @@ router.post('/user/manage/password', accountLimiter, wrap(async (req, res, next)
password = await API.User.Register.hashPassword(password) password = await API.User.Register.hashPassword(password)
let success = await API.User.update(user, { try {
password: password await API.User.update(user, {
}) password: password
})
if (success.error) { } catch (e) {
return formError(req, res, success.error) return formError(req, res, e.message)
} }
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)
@ -572,13 +661,9 @@ router.post('/user/manage/password', accountLimiter, wrap(async (req, res, next)
})) }))
// Change email address // Change email address
router.post('/user/manage/email', accountLimiter, 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()
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
let user = await API.User.get(req.session.user) let user = await API.User.get(req.session.user)
let email = req.body.email let email = req.body.email
let newEmail = req.body.email_new let newEmail = req.body.email_new
@ -613,15 +698,14 @@ router.post('/user/manage/email', accountLimiter, wrap(async (req, res, next) =>
return formError(req, res, 'This email is already taken.') return formError(req, res, 'This email is already taken.')
} }
let success = await API.User.update(user, { try {
email: newEmail await API.User.update(user, {
}) email: newEmail
})
if (success.error) { } catch (e) {
return formError(req, res, success.error) return formError(req, res, e.message)
} }
// TODO: Send necessary emails
console.warn('[SECURITY AUDIT] User \'%s\' email has been changed from %s', user.username, req.realIP) console.warn('[SECURITY AUDIT] User \'%s\' email has been changed from %s', user.username, req.realIP)
req.session.user.email = newEmail req.session.user.email = newEmail
@ -640,7 +724,7 @@ router.post('/user/manage/email', accountLimiter, wrap(async (req, res, next) =>
const docsDir = path.join(__dirname, '../../documents') const docsDir = path.join(__dirname, '../../documents')
router.get('/docs/:name', wrap(async (req, res, next) => { router.get('/docs/:name', wrap(async (req, res, next) => {
let doc = path.join(docsDir, req.params.name + '.html') let doc = path.join(docsDir, req.params.name + '.html')
if (!await exists(docsDir) || !await exists(doc)) { if (!await fs.exists(docsDir) || !await fs.exists(doc)) {
return next() return next()
} }
@ -667,18 +751,16 @@ router.get('/news/compose', newsPrivilege, formKeep, (req, res) => {
res.render('news/composer') res.render('news/composer')
}) })
router.post('/news/compose', newsPrivilege, wrap(async (req, res) => { router.post('/news/compose', newsPrivilege, csrfValidation, wrap(async (req, res) => {
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
if (!req.body.title || !req.body.content) { if (!req.body.title || !req.body.content) {
return formError(req, res, 'Required fields missing!') return formError(req, res, 'Required fields missing!')
} }
let result = await News.compose(req.session.user, req.body) let result
if (result.error) { try {
return formError(req, res, result.error) result = await News.compose(req.session.user, req.body)
} catch (e) {
return formError(req, res, e.message)
} }
res.redirect('/news/' + result.id + '-' + result.slug) res.redirect('/news/' + result.id + '-' + result.slug)
@ -733,21 +815,6 @@ router.get('/logout', (req, res) => {
res.redirect('/') res.redirect('/')
}) })
// User activation endpoint (emailed link)
router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let success = await API.User.Login.activationToken(token)
if (!success) {
req.flash('message', {error: true, text: 'Invalid or expired activation token.'})
} else {
req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'})
}
res.redirect('/login')
}))
router.use('/api', apiRouter) router.use('/api', apiRouter)
router.use('/admin', adminRouter) router.use('/admin', adminRouter)
router.use('/mc', mcRouter) router.use('/mc', mcRouter)

View File

@ -1,4 +1,5 @@
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 Minecraft from '../api/minecraft' import Minecraft from '../api/minecraft'

View File

@ -1,5 +1,6 @@
import express from 'express' import express from 'express'
import uapi from '../api'
import UAPI from '../api'
import OAuth2 from '../api/oauth2' import OAuth2 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'
@ -31,7 +32,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) => {
let accessToken = req.oauth2.accessToken let accessToken = req.oauth2.accessToken
let user = await uapi.User.get(accessToken.user_id) let user = await UAPI.User.get(accessToken.user_id)
if (!user) { if (!user) {
return res.status(404).jsonp({ return res.status(404).jsonp({

View File

@ -68,7 +68,7 @@ module.exports = (args) => {
app.set('views', path.join(__dirname, '../views')) app.set('views', path.join(__dirname, '../views'))
if (args.dev) { if (args.dev) {
console.log('Worker is in development mode') console.warn('Worker is in development mode')
// Dev logger // Dev logger
const morgan = require('morgan') const morgan = require('morgan')

View File

@ -12,15 +12,20 @@ function buildTemplateScript (id, ctx) {
function paginationButton (pages) { function paginationButton (pages) {
var html = '<div class="pgn">' var html = '<div class="pgn">'
html += '<span class="pagenum">Page ' + pages.page + ' of ' + pages.pages + '</span>' html += '<span class="pagenum">Page ' + pages.page + ' of ' + pages.pages + '</span>'
if (pages.page > 1) { if (pages.page > 1) {
html += '<div class="button" data-page="' + (pages.page - 1) + '">Previous</div>' html += '<div class="button" data-page="' + (pages.page - 1) + '">Previous</div>'
} }
for (var i = 0; i < pages.pages; i++) { for (var i = 0; i < pages.pages; i++) {
html += '<div class="button" data-page="' + (i + 1) + '">' + (i + 1) + '</div>' html += '<div class="button' + (i + 1 === pages.page ? ' active' : '') + '" data-page="' + (i + 1) + '">' + (i + 1) + '</div>'
} }
if (pages.pages > pages.page) { if (pages.pages > pages.page) {
html += '<div class="button" data-page="' + (pages.page + 1) + '">Next</div>' html += '<div class="button" data-page="' + (pages.page + 1) + '">Next</div>'
} }
html += '<span class="pagenum total">(' + pages.total + ' Total Entries)</span>'
html += '</div>' html += '</div>'
return html return html
} }
@ -271,11 +276,9 @@ $(document).ready(function () {
setInterval(function () { setInterval(function () {
$.get({ $.get({
url: '/admin/access', url: '/admin/access'
success: function (data) { }).fail(function () {
if (data && data.access) return window.location.reload()
window.location.reload()
}
}) })
}, 30000) }, 30000)
}) })

View File

@ -206,6 +206,8 @@ input:not([type="submit"])
font-size: 120% font-size: 120%
margin: 10px 0 margin: 10px 0
cursor: pointer cursor: pointer
&.active
background-color: #ddd
.button .button
display: inline-block display: inline-block
@ -254,6 +256,9 @@ input:not([type="submit"])
.pagenum .pagenum
display: inline-block display: inline-block
padding: 10px padding: 10px
&.total
color: #a9a9a9
font-style: italic
.dlbtn .dlbtn
display: block display: block

View File

@ -0,0 +1,6 @@
h1 Hello, #{display_name}!
p You've requested to reset your password on Icy Network.
p Click on or copy the following link into your URL bar in order to reset your Icy Network account password:
a.activate(href=domain + "/reset/" + reset_token, target="_blank", rel="nofollow")= domain + "/reset/" + reset_token
p If you did not request a password reset on Icy Network, please ignore this email.
small This email has been sent to you because of an action performed on the IcyNet.eu website.

View File

@ -0,0 +1 @@
|Icy Network - Password reset request

View File

@ -23,5 +23,7 @@ block body
input(type="password", name="password", id="password") input(type="password", name="password", id="password")
input(type="submit", value="Log in") input(type="submit", value="Log in")
a#create(href="/register") Create an account a#create(href="/register") Create an account
span.divider &middot;
a#create(href="/login/reset") Forgot password?
.right .right
include ../includes/external.pug include ../includes/external.pug

View File

@ -19,8 +19,6 @@ block body
if !token if !token
label(for="password_old") Current Password label(for="password_old") Current Password
input(type="password", name="password_old", id="password_old") input(type="password", name="password_old", id="password_old")
else
input(type="hidden", name="token", value=token)
label(for="password") New Password label(for="password") New Password
input(type="password", name="password", id="password") input(type="password", name="password", id="password")
label(for="password_repeat") Repeat New Password label(for="password_repeat") Repeat New Password

View File

@ -0,0 +1,23 @@
extends ../layout.pug
block title
|Icy Network - Reset Password
block body
.wrapper
.boxcont
.box#totpcheck
h1 Reset your password
p Enter your email below in order to reset your password.
if message.text
if message.error
.message.error
span #{message.text}
else
.message
span #{message.text}
if !sent
form#loginForm(method="POST", action=post)
input(type="hidden", name="csrf", value=csrf)
label(for="email") Email Address
input(type="email", name="email", id="email")
input(type="submit", value="Continue")