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=",
|
||||
"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": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||
@ -2152,6 +2162,14 @@
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
||||
@ -2410,6 +2428,41 @@
|
||||
"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": {
|
||||
"version": "4.1.11",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
|
||||
@ -3823,6 +3884,11 @@
|
||||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
|
||||
|
@ -40,7 +40,9 @@
|
||||
"express": "^4.15.3",
|
||||
"express-rate-limit": "^2.9.0",
|
||||
"express-session": "^1.15.3",
|
||||
"gm": "^1.23.0",
|
||||
"knex": "^0.13.0",
|
||||
"multiparty": "^4.1.3",
|
||||
"mysql": "^2.13.0",
|
||||
"nodemailer": "^4.0.1",
|
||||
"notp": "^2.0.3",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import url from 'url'
|
||||
import qs from 'querystring'
|
||||
import fs from 'fs'
|
||||
|
||||
function HTTP_GET (link, headers = {}, lback) {
|
||||
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 = {
|
||||
GET: HTTP_GET,
|
||||
POST: HTTP_POST
|
||||
POST: HTTP_POST,
|
||||
Download: Download
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import models from './models'
|
||||
import UAPI from './index'
|
||||
import qs from 'querystring'
|
||||
import oauth from 'oauth-libre'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import url from 'url'
|
||||
|
||||
let twitterApp
|
||||
let discordApp
|
||||
@ -50,6 +53,23 @@ const API = {
|
||||
}
|
||||
|
||||
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: {
|
||||
@ -96,11 +116,13 @@ const API = {
|
||||
}
|
||||
|
||||
// Determine profile picture
|
||||
let profilepic = ''
|
||||
let profilepic = null
|
||||
if (fbdata.picture) {
|
||||
if (fbdata.picture.is_silhouette === false && fbdata.picture.url) {
|
||||
// TODO: Download the profile image and save it locally
|
||||
profilepic = fbdata.picture.url
|
||||
let imgdata = await API.Common.saveAvatar(fbdata.picture.url)
|
||||
if (imgdata && imgdata.fileName) {
|
||||
profilepic = imgdata.fileName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,10 +233,12 @@ const API = {
|
||||
}
|
||||
|
||||
// Determine profile picture
|
||||
let profilepic = ''
|
||||
let profilepic = null
|
||||
if (twdata.profile_image_url_https) {
|
||||
// TODO: Download the profile image and save it locally
|
||||
profilepic = twdata.profile_image_url_https
|
||||
let imgdata = await API.Common.saveAvatar(twdata.profile_image_url_https)
|
||||
if (imgdata && imgdata.fileName) {
|
||||
profilepic = imgdata.fileName
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
@ -326,8 +350,7 @@ const API = {
|
||||
}
|
||||
|
||||
// Determine profile picture
|
||||
let profilepic = ''
|
||||
// TODO: Download the profile image and save it locally
|
||||
let profilepic = null
|
||||
|
||||
// Create a new user
|
||||
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 base32 from 'thirty-two'
|
||||
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,}))$/
|
||||
|
||||
@ -119,6 +120,38 @@ const API = {
|
||||
|
||||
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: {
|
||||
password: async function (user, password) {
|
||||
user = await API.User.ensureObject(user, ['password'])
|
||||
|
@ -1,9 +1,12 @@
|
||||
import express from 'express'
|
||||
import RateLimit from 'express-rate-limit'
|
||||
import multiparty from 'multiparty'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
import APIExtern from '../api/external'
|
||||
import API from '../api'
|
||||
import News from '../api/news'
|
||||
import Image from '../api/image'
|
||||
import APIExtern from '../api/external'
|
||||
|
||||
let router = express.Router()
|
||||
|
||||
@ -13,6 +16,12 @@ let apiLimiter = new RateLimit({
|
||||
delayMs: 0
|
||||
})
|
||||
|
||||
let uploadLimiter = new RateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
delayMs: 0
|
||||
})
|
||||
|
||||
router.use(apiLimiter)
|
||||
|
||||
// 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)
|
||||
}))
|
||||
|
||||
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
|
||||
router.use((req, res) => {
|
||||
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]
|
||||
}
|
||||
|
||||
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.')
|
||||
}
|
||||
|
||||
let user = req.session.user
|
||||
let user = await API.User.get(req.session.user)
|
||||
let email = req.body.email
|
||||
let newEmail = req.body.email_new
|
||||
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.')
|
||||
}
|
||||
|
||||
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.')
|
||||
}
|
||||
|
||||
if (user.password != null && user.password !== '') {
|
||||
if (!password) {
|
||||
return formError(req, res, 'Enter a password.')
|
||||
}
|
||||
|
||||
let passwordMatch = await API.User.Login.password(user, password)
|
||||
if (!passwordMatch) {
|
||||
return formError(req, res, 'The password you provided is incorrect.')
|
||||
}
|
||||
}
|
||||
|
||||
let emailValid = API.User.Register.validateEmail(newEmail)
|
||||
if (!emailValid) {
|
||||
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, {
|
||||
email: newEmail
|
||||
})
|
||||
|
@ -61,6 +61,7 @@ module.exports = (args) => {
|
||||
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('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
|
||||
app.use('/usercontent', express.static(path.join(__dirname, '../usercontent'), { maxAge: staticAge }))
|
||||
|
||||
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 () {
|
||||
var FB = window.FB
|
||||
FB.getLoginStatus(function (response) {
|
||||
|
@ -485,6 +485,40 @@ input.invalid
|
||||
cursor: pointer
|
||||
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)
|
||||
.navigator
|
||||
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")
|
||||
label(for="email_new") New Email Address
|
||||
input(type="email", name="email_new", id="email_new")
|
||||
if password
|
||||
label(for="password") Password
|
||||
input(type="password", name="password", id="password")
|
||||
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)
|
||||
label(for="display_name") 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")
|
||||
.right
|
||||
h3 Social Media Accounts
|
||||
|
Reference in New Issue
Block a user