avatars and some fixes
This commit is contained in:
parent
4c7f72f65c
commit
ab06f1a4cd
66
package-lock.json
generated
66
package-lock.json
generated
@ -141,12 +141,22 @@
|
|||||||
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
|
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"array-parallel": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz",
|
||||||
|
"integrity": "sha1-j3hTCJJu1apHjEfmTRszS2wMlH0="
|
||||||
|
},
|
||||||
"array-reduce": {
|
"array-reduce": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||||
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"array-series": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz",
|
||||||
|
"integrity": "sha1-3103v8XC7wdV4qpPkv6ufUtaly8="
|
||||||
|
},
|
||||||
"array-union": {
|
"array-union": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||||
@ -2152,6 +2162,14 @@
|
|||||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fd-slicer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
|
||||||
|
"requires": {
|
||||||
|
"pend": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"figures": {
|
"figures": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
||||||
@ -2410,6 +2428,41 @@
|
|||||||
"pinkie-promise": "2.0.1"
|
"pinkie-promise": "2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"gm": {
|
||||||
|
"version": "1.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gm/-/gm-1.23.0.tgz",
|
||||||
|
"integrity": "sha1-gKL+nL8TFRUCSEZERlhGEmn1JmE=",
|
||||||
|
"requires": {
|
||||||
|
"array-parallel": "0.1.3",
|
||||||
|
"array-series": "0.1.5",
|
||||||
|
"cross-spawn": "4.0.2",
|
||||||
|
"debug": "2.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
|
||||||
|
"integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=",
|
||||||
|
"requires": {
|
||||||
|
"lru-cache": "4.1.1",
|
||||||
|
"which": "https://registry.npmjs.org/which/-/which-1.2.14.tgz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
|
||||||
|
"requires": {
|
||||||
|
"ms": "0.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
|
||||||
|
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@ -3509,6 +3562,14 @@
|
|||||||
"version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
},
|
},
|
||||||
|
"multiparty": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.3.tgz",
|
||||||
|
"integrity": "sha1-PEPH/LGJbhdGBDap3Qtu8WaOT5Q=",
|
||||||
|
"requires": {
|
||||||
|
"fd-slicer": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"mute-stream": {
|
"mute-stream": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
|
||||||
@ -3823,6 +3884,11 @@
|
|||||||
"sha.js": "2.4.8"
|
"sha.js": "2.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
|
||||||
|
},
|
||||||
"performance-now": {
|
"performance-now": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
|
||||||
|
@ -40,7 +40,9 @@
|
|||||||
"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",
|
||||||
|
"gm": "^1.23.0",
|
||||||
"knex": "^0.13.0",
|
"knex": "^0.13.0",
|
||||||
|
"multiparty": "^4.1.3",
|
||||||
"mysql": "^2.13.0",
|
"mysql": "^2.13.0",
|
||||||
"nodemailer": "^4.0.1",
|
"nodemailer": "^4.0.1",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import url from 'url'
|
import url from 'url'
|
||||||
import qs from 'querystring'
|
import qs from 'querystring'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
function HTTP_GET (link, headers = {}, lback) {
|
function HTTP_GET (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
|
||||||
@ -105,7 +106,24 @@ function HTTP_POST (link, headers = {}, data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function Download (url, dest) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let file = fs.createWriteStream(dest)
|
||||||
|
let protocol = url.indexOf('https:') === 0 ? require('https') : require('http')
|
||||||
|
protocol.get(url, function (response) {
|
||||||
|
response.pipe(file)
|
||||||
|
file.on('finish', function () {
|
||||||
|
file.close(resolve)
|
||||||
|
})
|
||||||
|
}).on('error', function (err) {
|
||||||
|
fs.unlink(dest)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
GET: HTTP_GET,
|
GET: HTTP_GET,
|
||||||
POST: HTTP_POST
|
POST: HTTP_POST,
|
||||||
|
Download: Download
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ import models from './models'
|
|||||||
import UAPI from './index'
|
import UAPI from './index'
|
||||||
import qs from 'querystring'
|
import qs from 'querystring'
|
||||||
import oauth from 'oauth-libre'
|
import oauth from 'oauth-libre'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import url from 'url'
|
||||||
|
|
||||||
let twitterApp
|
let twitterApp
|
||||||
let discordApp
|
let discordApp
|
||||||
@ -50,6 +53,23 @@ 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) => {
|
||||||
|
if (!avatarUrl) return null
|
||||||
|
let imgdir = path.join(__dirname, '../../', 'usercontent', 'images')
|
||||||
|
let imageName = 'download-' + UAPI.Hash(12)
|
||||||
|
let uridata = url.parse(avatarUrl)
|
||||||
|
let pathdata = path.parse(uridata.path)
|
||||||
|
|
||||||
|
imageName += pathdata.ext || '.png'
|
||||||
|
|
||||||
|
try {
|
||||||
|
await http.Download(avatarUrl, path.join(imgdir, imageName))
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {fileName: imageName}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Facebook: {
|
Facebook: {
|
||||||
@ -96,11 +116,13 @@ const API = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine profile picture
|
// Determine profile picture
|
||||||
let profilepic = ''
|
let profilepic = null
|
||||||
if (fbdata.picture) {
|
if (fbdata.picture) {
|
||||||
if (fbdata.picture.is_silhouette === false && fbdata.picture.url) {
|
if (fbdata.picture.is_silhouette === false && fbdata.picture.url) {
|
||||||
// TODO: Download the profile image and save it locally
|
let imgdata = await API.Common.saveAvatar(fbdata.picture.url)
|
||||||
profilepic = fbdata.picture.url
|
if (imgdata && imgdata.fileName) {
|
||||||
|
profilepic = imgdata.fileName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,10 +233,12 @@ const API = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine profile picture
|
// Determine profile picture
|
||||||
let profilepic = ''
|
let profilepic = null
|
||||||
if (twdata.profile_image_url_https) {
|
if (twdata.profile_image_url_https) {
|
||||||
// TODO: Download the profile image and save it locally
|
let imgdata = await API.Common.saveAvatar(twdata.profile_image_url_https)
|
||||||
profilepic = twdata.profile_image_url_https
|
if (imgdata && imgdata.fileName) {
|
||||||
|
profilepic = imgdata.fileName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new user
|
// Create a new user
|
||||||
@ -326,8 +350,7 @@ const API = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine profile picture
|
// Determine profile picture
|
||||||
let profilepic = ''
|
let profilepic = null
|
||||||
// TODO: Download the profile image and save it locally
|
|
||||||
|
|
||||||
// Create a new user
|
// Create a new user
|
||||||
let udataLimited = {
|
let udataLimited = {
|
||||||
|
109
server/api/image.js
Normal file
109
server/api/image.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import gm from 'gm'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import Promise from 'bluebird'
|
||||||
|
|
||||||
|
const fsBlue = Promise.promisifyAll(fs)
|
||||||
|
|
||||||
|
const uploads = path.join(__dirname, '../../', 'usercontent')
|
||||||
|
const maxFileSize = 1000000
|
||||||
|
const imageTypes = {
|
||||||
|
'image/png': '.png',
|
||||||
|
'image/jpg': '.jpg',
|
||||||
|
'image/jpeg': '.jpeg'
|
||||||
|
}
|
||||||
|
|
||||||
|
function saneFields (fields) {
|
||||||
|
let out = {}
|
||||||
|
|
||||||
|
for (let i in fields) {
|
||||||
|
let entry = fields[i]
|
||||||
|
if (typeof entry === 'object' && entry.length === 1 && !isNaN(parseInt(entry[0]))) {
|
||||||
|
out[i] = parseInt(entry[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bailOut (file, error) {
|
||||||
|
await fsBlue.unlinkAsync(file)
|
||||||
|
return { error: error }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage (username, fields, files) {
|
||||||
|
let directory = path.join(uploads, 'images')
|
||||||
|
if (!files.image) return {error: 'No image file'}
|
||||||
|
|
||||||
|
let file = files.image[0]
|
||||||
|
if (file.size > maxFileSize) return bailOut(file.path, 'Image is too large! 1 MB max')
|
||||||
|
|
||||||
|
fields = saneFields(fields)
|
||||||
|
|
||||||
|
// Get file info, generate a file name
|
||||||
|
let fileHash = crypto.randomBytes(12).toString('hex')
|
||||||
|
let contentType = file.headers['content-type']
|
||||||
|
if (!contentType) return bailOut(file.path, 'Invalid of missing content-type header')
|
||||||
|
|
||||||
|
file = file.path
|
||||||
|
|
||||||
|
// Make sure content type is allowed
|
||||||
|
let match = false
|
||||||
|
for (let i in imageTypes) {
|
||||||
|
if (i === contentType) {
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!match) return bailOut(file, 'Invalid image type. Only PNG, JPG and JPEG files are allowed.')
|
||||||
|
let extension = imageTypes[contentType]
|
||||||
|
let fileName = username + '-' + fileHash + extension
|
||||||
|
|
||||||
|
// Check for cropping
|
||||||
|
if (fields.x == null || fields.y == null || fields.width == null || fields.height == null) {
|
||||||
|
return bailOut(file, 'Images can only be cropped on the server side due to security reasons.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.x < 0 || fields.y < 0 || fields.x > fields.width + fields.x || fields.y > fields.height + fields.y) {
|
||||||
|
return bailOut(file, 'Impossible crop.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 1 : 1 aspect ratio
|
||||||
|
if (Math.floor(fields.width / fields.height) !== 1) {
|
||||||
|
return bailOut(file, 'Avatars can only have an aspect ratio of 1:1')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.scaleX) {
|
||||||
|
fields.x *= fields.scaleX
|
||||||
|
fields.width *= fields.scaleX
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.scaleY) {
|
||||||
|
fields.y *= fields.scaleY
|
||||||
|
fields.height *= fields.scaleY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crop
|
||||||
|
try {
|
||||||
|
await new Promise(function (resolve, reject) {
|
||||||
|
gm(file)
|
||||||
|
.crop(fields.width, fields.height, fields.x, fields.y)
|
||||||
|
.write(path.join(directory, fileName), (err) => {
|
||||||
|
if (err) return reject(err)
|
||||||
|
resolve(fileName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await fs.unlinkAsync(file)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return bailOut(file, 'An error occured while cropping.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {file: fileName}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
uploadImage: uploadImage
|
||||||
|
}
|
@ -6,6 +6,7 @@ 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 emailer from './emailer'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
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,}))$/
|
||||||
|
|
||||||
@ -119,6 +120,38 @@ const API = {
|
|||||||
|
|
||||||
return models.User.query().patchAndFetchById(user.id, data)
|
return models.User.query().patchAndFetchById(user.id, data)
|
||||||
},
|
},
|
||||||
|
changeAvatar: async function (user, fileName) {
|
||||||
|
user = await API.User.ensureObject(user, ['avatar_file'])
|
||||||
|
let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
|
||||||
|
let pathOf = path.join(uploadsDir, fileName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(pathOf)) {
|
||||||
|
return {error: 'No such file'}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete previous upload
|
||||||
|
if (user.avatar_file != null) {
|
||||||
|
let file = path.join(uploadsDir, user.avatar_file)
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
fs.unlinkSync(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await API.User.update(user, {avatar_file: fileName})
|
||||||
|
return { file: fileName }
|
||||||
|
},
|
||||||
|
removeAvatar: async function (user) {
|
||||||
|
user = await API.User.ensureObject(user, ['avatar_file'])
|
||||||
|
let uploadsDir = path.join(__dirname, '../../', 'usercontent', 'images')
|
||||||
|
if (!user.avatar_file) return {}
|
||||||
|
|
||||||
|
let file = path.join(uploadsDir, user.avatar_file)
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
fs.unlinkSync(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return API.User.update(user, {avatar_file: null})
|
||||||
|
},
|
||||||
Login: {
|
Login: {
|
||||||
password: async function (user, password) {
|
password: async function (user, password) {
|
||||||
user = await API.User.ensureObject(user, ['password'])
|
user = await API.User.ensureObject(user, ['password'])
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
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 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 API from '../api'
|
||||||
import News from '../api/news'
|
import News from '../api/news'
|
||||||
|
import Image from '../api/image'
|
||||||
|
import APIExtern from '../api/external'
|
||||||
|
|
||||||
let router = express.Router()
|
let router = express.Router()
|
||||||
|
|
||||||
@ -13,6 +16,12 @@ let apiLimiter = new RateLimit({
|
|||||||
delayMs: 0
|
delayMs: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let uploadLimiter = new RateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 10,
|
||||||
|
delayMs: 0
|
||||||
|
})
|
||||||
|
|
||||||
router.use(apiLimiter)
|
router.use(apiLimiter)
|
||||||
|
|
||||||
// Turn things like 'key1[key2]': 'value' into key1: {key2: 'value'} because facebook
|
// Turn things like 'key1[key2]': 'value' into key1: {key2: 'value'} because facebook
|
||||||
@ -277,6 +286,46 @@ router.get('/news', wrap(async (req, res) => {
|
|||||||
res.jsonp(articles)
|
res.jsonp(articles)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
async function promiseForm (req) {
|
||||||
|
let form = new multiparty.Form()
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
form.parse(req, async (err, fields, files) => {
|
||||||
|
if (err) return reject(err)
|
||||||
|
resolve({fields: fields, files: files})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => {
|
||||||
|
if (!req.session.user) return next()
|
||||||
|
let data = await promiseForm(req)
|
||||||
|
let result = await Image.uploadImage(req.session.user.username, data.fields, data.files)
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return res.status(400).jsonp({error: result.error})
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarUpdate = await API.User.changeAvatar(req.session.user, result.file)
|
||||||
|
if (avatarUpdate.error) {
|
||||||
|
return res.status(400).jsonp({error: avatarUpdate.error})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarUpdate.file) {
|
||||||
|
req.session.user.avatar_file = avatarUpdate.file
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).jsonp({})
|
||||||
|
}))
|
||||||
|
|
||||||
|
router.post('/avatar/remove', wrap(async (req, res, next) => {
|
||||||
|
if (!req.session.user) return next()
|
||||||
|
|
||||||
|
await API.User.removeAvatar(req.session.user)
|
||||||
|
req.session.user.avatar_file = null
|
||||||
|
|
||||||
|
res.status(200).jsonp({done: true})
|
||||||
|
}))
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
router.use((req, res) => {
|
router.use((req, res) => {
|
||||||
res.status(404).jsonp({error: 'Not found'})
|
res.status(404).jsonp({error: 'Not found'})
|
||||||
|
@ -192,7 +192,9 @@ router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
|
|||||||
obfuscated = rep + '@' + split[1]
|
obfuscated = rep + '@' + split[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('email_change', {email: obfuscated})
|
let socialStatus = await API.User.socialStatus(req.session.user)
|
||||||
|
|
||||||
|
res.render('email_change', {email: obfuscated, password: socialStatus.password})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -519,29 +521,40 @@ router.post('/user/manage/email', wrap(async (req, res, next) => {
|
|||||||
return formError(req, res, 'Invalid session! Try reloading the page.')
|
return formError(req, res, 'Invalid session! Try reloading the page.')
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = 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
|
||||||
let password = req.body.password
|
let password = req.body.password
|
||||||
|
|
||||||
if (!password || !newEmail || (!email && user.email != null)) {
|
if (!newEmail || (!email && user.email !== '')) {
|
||||||
return formError(req, res, 'Please fill in all of the fields.')
|
return formError(req, res, 'Please fill in all of the fields.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session.user.email != null && email !== user.email) {
|
if (req.session.user.email !== '' && email !== user.email) {
|
||||||
return formError(req, res, 'The email you provided is incorrect.')
|
return formError(req, res, 'The email you provided is incorrect.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.password != null && user.password !== '') {
|
||||||
|
if (!password) {
|
||||||
|
return formError(req, res, 'Enter a password.')
|
||||||
|
}
|
||||||
|
|
||||||
let passwordMatch = await API.User.Login.password(user, password)
|
let passwordMatch = await API.User.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.')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let emailValid = API.User.Register.validateEmail(newEmail)
|
let emailValid = API.User.Register.validateEmail(newEmail)
|
||||||
if (!emailValid) {
|
if (!emailValid) {
|
||||||
return formError(req, res, 'Invalid email address.')
|
return formError(req, res, 'Invalid email address.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let emailTaken = await API.User.get(newEmail)
|
||||||
|
if (emailTaken) {
|
||||||
|
return formError(req, res, 'This email is already taken.')
|
||||||
|
}
|
||||||
|
|
||||||
let success = await API.User.update(user, {
|
let success = await API.User.update(user, {
|
||||||
email: newEmail
|
email: newEmail
|
||||||
})
|
})
|
||||||
|
@ -61,6 +61,7 @@ module.exports = (args) => {
|
|||||||
app.use('/style', express.static(path.join(__dirname, '../build/style'), { maxAge: staticAge }))
|
app.use('/style', express.static(path.join(__dirname, '../build/style'), { maxAge: staticAge }))
|
||||||
app.use('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
|
app.use('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
|
||||||
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
|
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
|
||||||
|
app.use('/usercontent', express.static(path.join(__dirname, '../usercontent'), { maxAge: staticAge }))
|
||||||
|
|
||||||
app.use(routes)
|
app.use(routes)
|
||||||
|
|
||||||
|
@ -131,6 +131,24 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($('#newAvatar').length) {
|
||||||
|
$('#newAvatar').click(function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
window.Dialog.openPartial('Change Avatar', 'avatar')
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#removeAvatar').click(function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/api/avatar/remove',
|
||||||
|
success: function (data) {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
window.checkLoginState = function () {
|
window.checkLoginState = function () {
|
||||||
var FB = window.FB
|
var FB = window.FB
|
||||||
FB.getLoginStatus(function (response) {
|
FB.getLoginStatus(function (response) {
|
||||||
|
@ -485,6 +485,40 @@ input.invalid
|
|||||||
cursor: pointer
|
cursor: pointer
|
||||||
padding: 5px
|
padding: 5px
|
||||||
|
|
||||||
|
.cropbox
|
||||||
|
padding: 10px
|
||||||
|
.preview
|
||||||
|
max-width: 160px
|
||||||
|
max-height: 160px
|
||||||
|
.buttons .button
|
||||||
|
margin-right: 5px
|
||||||
|
|
||||||
|
.avatarCont
|
||||||
|
height: 180px
|
||||||
|
.avatar
|
||||||
|
float: left
|
||||||
|
.options
|
||||||
|
margin-left: 170px
|
||||||
|
a
|
||||||
|
display: block
|
||||||
|
|
||||||
|
.avatar
|
||||||
|
width: 160px
|
||||||
|
height: 160px
|
||||||
|
position: relative
|
||||||
|
border: 1px solid #ddd
|
||||||
|
img
|
||||||
|
position: absolute
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
margin: auto
|
||||||
|
.noavatar
|
||||||
|
font-size: 120px
|
||||||
|
text-align: center
|
||||||
|
line-height: 160px
|
||||||
|
color: #fff
|
||||||
|
background-color: #d0d0d0
|
||||||
|
|
||||||
@media all and (max-width: 800px)
|
@media all and (max-width: 800px)
|
||||||
.navigator
|
.navigator
|
||||||
padding: 0 10px
|
padding: 0 10px
|
||||||
|
3
usercontent/.gitignore
vendored
Normal file
3
usercontent/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
!images/
|
2
usercontent/images/.gitignore
vendored
Normal file
2
usercontent/images/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
@ -23,6 +23,7 @@ block body
|
|||||||
input(type="email", name="email", id="email")
|
input(type="email", name="email", id="email")
|
||||||
label(for="email_new") New Email Address
|
label(for="email_new") New Email Address
|
||||||
input(type="email", name="email_new", id="email_new")
|
input(type="email", name="email_new", id="email_new")
|
||||||
|
if password
|
||||||
label(for="password") Password
|
label(for="password") Password
|
||||||
input(type="password", name="password", id="password")
|
input(type="password", name="password", id="password")
|
||||||
input(type="submit", value="Change")
|
input(type="submit", value="Change")
|
||||||
|
6
views/includes/avatar.pug
Normal file
6
views/includes/avatar.pug
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.avatar
|
||||||
|
if user.avatar_file
|
||||||
|
img(src="/usercontent/images/" + user.avatar_file)
|
||||||
|
else
|
||||||
|
.noavatar
|
||||||
|
i.fa.fa-fw.fa-user
|
@ -1 +1,155 @@
|
|||||||
img(src=user.avatar_file)
|
.rel.cropbox
|
||||||
|
link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/cropper/2.3.4/cropper.min.css")
|
||||||
|
script(src="https://cdnjs.cloudflare.com/ajax/libs/cropper/2.3.4/cropper.min.js")
|
||||||
|
.otherdata
|
||||||
|
h3 Current Avatar
|
||||||
|
.avatar
|
||||||
|
include ../includes/avatar.pug
|
||||||
|
.inputting
|
||||||
|
h3 Upload new
|
||||||
|
.message.error
|
||||||
|
input(type="file", id="fileinput")
|
||||||
|
.editor(style="display: none")
|
||||||
|
h3 Crop the image
|
||||||
|
img.preview(id="image")
|
||||||
|
.buttons
|
||||||
|
.button#done Done
|
||||||
|
.button#cancel Cancel
|
||||||
|
.button#upload Upload Now
|
||||||
|
|
||||||
|
script.
|
||||||
|
window.jQuery = $
|
||||||
|
function message (msg) {
|
||||||
|
$('.message').text(msg)
|
||||||
|
$('.message').show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataURItoBlob (dataURI) {
|
||||||
|
// convert base64 to raw binary data held in a string
|
||||||
|
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
|
||||||
|
var byteString = atob(dataURI.split(',')[1])
|
||||||
|
|
||||||
|
// separate out the mime component
|
||||||
|
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
|
||||||
|
|
||||||
|
// write the bytes of the string to an ArrayBuffer
|
||||||
|
var ab = new ArrayBuffer(byteString.length)
|
||||||
|
|
||||||
|
// create a view into the buffer
|
||||||
|
var ia = new Uint8Array(ab)
|
||||||
|
|
||||||
|
// set the bytes of the buffer to the correct values
|
||||||
|
for (var i = 0; i < byteString.length; i++) {
|
||||||
|
ia[i] = byteString.charCodeAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the ArrayBuffer to a blob, and you're done
|
||||||
|
var blob = new Blob([ab], {type: mimeString})
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
|
||||||
|
function cropReady() {
|
||||||
|
let cropargs = $('#image').cropper('getData')
|
||||||
|
let cropimage = $('#image').cropper('getCroppedCanvas')
|
||||||
|
|
||||||
|
$('#upload').show()
|
||||||
|
$('#done').hide()
|
||||||
|
$('.preview').attr('src', cropimage.toDataURL())
|
||||||
|
$('.preview').show()
|
||||||
|
$('#image').cropper('destroy')
|
||||||
|
|
||||||
|
let called = false
|
||||||
|
$('#upload').click(function (e) {
|
||||||
|
if (called) return
|
||||||
|
called = true
|
||||||
|
$('#upload').hide()
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append('image', dataURItoBlob(fr.result))
|
||||||
|
|
||||||
|
for (let i in cropargs) {
|
||||||
|
formData.append(i, cropargs[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/api/avatar',
|
||||||
|
data: formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: function (data) {
|
||||||
|
window.Dialog.close()
|
||||||
|
window.location.reload()
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
if (err.responseJSON && err.responseJSON.error) {
|
||||||
|
message(err.responseJSON.error)
|
||||||
|
}
|
||||||
|
$('#cancel').click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready (blob) {
|
||||||
|
let match = blob.match(/data:image\/(\w+);/)
|
||||||
|
if (!match) {
|
||||||
|
return message('Not an image file!')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match[1] !== 'png' && match[1] !== 'jpg' && match[1] !== 'jpeg') {
|
||||||
|
return message('Unsupported image file')
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#image').attr('src', fr.result).hide()
|
||||||
|
$('.inputting').hide()
|
||||||
|
$('.otherdata').hide()
|
||||||
|
$('#upload').hide()
|
||||||
|
$('#done').show()
|
||||||
|
$('.editor').show()
|
||||||
|
$('#image').cropper({
|
||||||
|
aspectRatio: 1 / 1,
|
||||||
|
minContainerHeight: 512,
|
||||||
|
minContainerWidth: 512,
|
||||||
|
viewMode: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect() {
|
||||||
|
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
|
||||||
|
return message('The File APIs are not fully supported in this browser.')
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = document.getElementById('fileinput')
|
||||||
|
if (!input.files) {
|
||||||
|
message('This browser doesn\'t seem to support the `files` property of file inputs.')
|
||||||
|
} else if (!input.files[0]) {
|
||||||
|
message('Please select a file.')
|
||||||
|
} else {
|
||||||
|
file = input.files[0]
|
||||||
|
fr = new FileReader()
|
||||||
|
fr.readAsDataURL(file)
|
||||||
|
fr.addEventListener('load', function (e) {
|
||||||
|
ready(fr.result)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#fileinput').on('change', function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleFileSelect()
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#cancel').click(function (e) {
|
||||||
|
$('.inputting').show()
|
||||||
|
$('.otherdata').show()
|
||||||
|
$('.editor').hide()
|
||||||
|
$('#image').cropper('destroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#done').click(function (e) {
|
||||||
|
cropReady()
|
||||||
|
})
|
||||||
|
|
||||||
|
$('.message').hide()
|
||||||
|
|
||||||
|
@ -21,6 +21,13 @@ block body
|
|||||||
input(type="text", name="username", id="username", value=user.username, disabled)
|
input(type="text", name="username", id="username", value=user.username, disabled)
|
||||||
label(for="display_name") Display Name
|
label(for="display_name") Display Name
|
||||||
input(type="text", name="display_name", id="display_name", value=user.display_name)
|
input(type="text", name="display_name", id="display_name", value=user.display_name)
|
||||||
|
label(for="display_name") Avatar
|
||||||
|
.avatarCont
|
||||||
|
include includes/avatar.pug
|
||||||
|
.options
|
||||||
|
a#newAvatar(href='#') Change Avatar
|
||||||
|
if user.avatar_file
|
||||||
|
a#removeAvatar(href='#') Remove Avatar
|
||||||
input(type="submit", value="Save Settings")
|
input(type="submit", value="Save Settings")
|
||||||
.right
|
.right
|
||||||
h3 Social Media Accounts
|
h3 Social Media Accounts
|
||||||
|
Reference in New Issue
Block a user