Add support for profile photos; default image

This commit is contained in:
garrettmills
2020-05-18 22:55:08 -05:00
parent 2b2e7d2ebe
commit b8a0e957bb
18 changed files with 227 additions and 17 deletions

View File

@@ -3,6 +3,7 @@ import NavBarComponent from './dash/NavBar.component.js'
import MessageContainerComponent from './dash/message/MessageContainer.component.js'
import EditProfileComponent from './dash/profile/EditProfile.component.js'
import AppPasswordFormComponent from './dash/profile/form/AppPassword.component.js'
import ProfilePhotoUploaderComponent from './dash/profile/form/ProfilePhotoUploader.component.js'
import ListingComponent from './cobalt/Listing.component.js'
import FormComponent from './cobalt/Form.component.js'
@@ -13,6 +14,7 @@ const dash_components = {
MessageContainerComponent,
EditProfileComponent,
AppPasswordFormComponent,
ProfilePhotoUploaderComponent,
ListingComponent,
FormComponent,

View File

@@ -13,7 +13,10 @@ const template = `
<div class="card-body">
<div class="row">
<div class="col-8 offset-2 col-sm-4 offset-sm-0">
<img src="/assets/profile.jpg" alt="Profile Image" class="img-fluid">
<img src="/api/v1/profile/me/photo" alt="Profile Image" ref="photo" class="img-fluid">
<div class="overlay">
<button class="btn btn-outline-light" @click="on_profile_change_click">Change</button>
</div>
</div>
<div class="col-sm-8 offset-sm-0 col-12 text-sm-left text-center pad-top">
<div class="card-title"><h3>{{ profile_first }} {{ profile_last }}</h3></div>
@@ -140,6 +143,11 @@ const template = `
ref="app_password_modal"
@modal-success="load_app_passwords"
></coreid-form-app-password>
<coreid-profile-photo-uploader
ref="profile_photo_uploader"
:user-id="user_id"
@upload="on_profile_photo_upload"
></coreid-profile-photo-uploader>
</div>
`
@@ -187,6 +195,17 @@ export default class EditProfileComponent extends Component {
}
}
on_profile_change_click() {
this.$refs.profile_photo_uploader.show()
}
async on_profile_photo_upload() {
this.$refs.profile_photo_uploader.close()
let src = this.$refs.photo.src
if ( src.indexOf('?') > 0 ) src = src.split('?')[0]
this.$refs.photo.src = `${src}?i=${(new Date).getTime()}`
}
valid_email() {
return this.$refs.email_input.validity.valid
}

View File

@@ -0,0 +1,60 @@
import { Component } from '../../../../lib/vues6/vues6.js'
const template = `
<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true" ref="modal">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content" v-if="ready">
<div class="modal-header">
<h5 class="modal-title">Change Profile Photo</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<input type="file" name="image" ref="input" required>
</div>
<div class="modal-footer">
<button
class="btn btn-primary"
@click="do_upload"
>Change</button>
</div>
</div>
</div>
</div>
`
export default class ProfilePhotoUploaderComponent extends Component {
static get selector() { return 'coreid-profile-photo-uploader' }
static get template() { return template }
static get params() { return [] }
ready = false
show() {
this.ready = true
this.$nextTick(() => {
$(this.$refs.modal).modal()
})
}
close() {
$(this.$refs.modal).modal('hide')
}
async do_upload() {
if ( this.$refs.input.files.length < 1 ) return
const data = new FormData()
data.append('photo', this.$refs.input.files[0])
try {
await axios.post(`/api/v1/profile/me/photo`, data, { // TODO support passed-in user_id
headers: {
'Content-Type': 'multipart/form-data',
}
})
this.$emit('upload')
} catch (e) {
console.error(e)
}
}
}

1
app/assets/humans.txt Normal file
View File

@@ -0,0 +1 @@
Stock profile photo thanks to: https://www.flaticon.com/authors/vitaly-gorbachev

View File

@@ -61,3 +61,24 @@ body {
.pad-top {
padding-top: 30px;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
flex-direction: row;
display: flex;
align-items: center;
justify-content: center;
background: rgba(20, 20, 20, 0.4);
opacity: 0;
transition: all 0.1s linear;
&:hover {
opacity: 1;
}
}

BIN
app/assets/people.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,9 +1,10 @@
const { Controller } = require('libflitter')
const Validator = require('email-validator')
const path = require('path')
class ProfileController extends Controller {
static get services() {
return [...super.services, 'models']
return [...super.services, 'models', 'utility']
}
async fetch(req, res, next) {
@@ -72,6 +73,45 @@ class ProfileController extends Controller {
return res.api()
}
async update_photo(req, res, next) {
const User = this.models.get('auth:User')
let user
if ( req.params.user_id === 'me' ) user = req.user
else user = await User.findById(req.params.user_id)
if ( !user )
return res.status(404)
.message('No user found with the specified ID.')
.api()
if ( !req?.uploads?.photo )
return res.status(400)
.message('Missing required field: file')
.api()
user.photo_file_id = req.uploads.photo.id
await user.save()
return res.api()
}
async get_photo(req, res, next) {
const User = this.models.get('auth:User')
let user
if ( req.params.user_id === 'me' ) user = req.user
else user = await User.findById(req.params.user_id)
if ( !user )
return res.status(404)
.message('No user found with the specified ID.')
.api()
const photo = await user.photo()
if ( photo ) return photo.send(res)
// The user does not have a profile. Send the default.
return res.sendFile(this.utility.path('app/assets/people.png'))
}
}
module.exports = exports = ProfileController

View File

@@ -34,9 +34,15 @@ class User extends AuthUser {
mfa_enabled: {type: Boolean, default: false},
mfa_enable_date: Date,
create_date: {type: Date, default: () => new Date},
photo_file_id: String,
}}
}
async photo() {
const File = this.models.get('upload::File')
return File.findById(this.photo_file_id)
}
has_authorized(client) {
return this.app_authorizations.some(x => x.client_id === client.id)
}

View File

@@ -0,0 +1,12 @@
const Middleware = require('flitter-upload/middleware/UploadFile')
/*
* Middleware to upload the files included in the request
* to the default file store backend. Stores instances of
* the "upload::File" model in "request.uploads".
*/
class UploadFile extends Middleware {
}
module.exports = exports = UploadFile

View File

@@ -10,6 +10,18 @@ const profile_routes = {
['middleware::api:Permission', { check: 'v1:profile:get' }],
'controller::api:v1:Profile.fetch',
],
'/:user_id/photo': [
['middleware::api:Permission', { check: 'v1:profile:photo:get' }],
'controller::api:v1:Profile.get_photo',
],
},
post: {
'/:user_id/photo': [
['middleware::api:Permission', { check: 'v1:profile:photo:update' }],
['middleware::upload:UploadFile', { tag: 'v1:profile:photo' }],
'controller::api:v1:Profile.update_photo',
],
},
patch: {