Add support for profile photos; default image
This commit is contained in:
parent
2b2e7d2ebe
commit
b8a0e957bb
6
.gitignore
vendored
6
.gitignore
vendored
@ -144,3 +144,9 @@ fabric.properties
|
|||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
test*
|
test*
|
||||||
|
|
||||||
|
# Uploaded files
|
||||||
|
tmp.uploads/*
|
||||||
|
!tmp.uploads/.gitkeep
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
- Tagline bug - cannot save with empty text
|
- Tagline bug - cannot save with empty text
|
||||||
- Profile photos
|
|
||||||
- Allow uploading/changing
|
|
||||||
- Default photo
|
|
||||||
- Expose photo endpoint for public services
|
|
||||||
- App setup wizard
|
- App setup wizard
|
||||||
- SAML IAM handling
|
- SAML IAM handling
|
||||||
- LDAP IAM handling
|
- LDAP IAM handling
|
||||||
|
@ -3,6 +3,7 @@ import NavBarComponent from './dash/NavBar.component.js'
|
|||||||
import MessageContainerComponent from './dash/message/MessageContainer.component.js'
|
import MessageContainerComponent from './dash/message/MessageContainer.component.js'
|
||||||
import EditProfileComponent from './dash/profile/EditProfile.component.js'
|
import EditProfileComponent from './dash/profile/EditProfile.component.js'
|
||||||
import AppPasswordFormComponent from './dash/profile/form/AppPassword.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 ListingComponent from './cobalt/Listing.component.js'
|
||||||
import FormComponent from './cobalt/Form.component.js'
|
import FormComponent from './cobalt/Form.component.js'
|
||||||
@ -13,6 +14,7 @@ const dash_components = {
|
|||||||
MessageContainerComponent,
|
MessageContainerComponent,
|
||||||
EditProfileComponent,
|
EditProfileComponent,
|
||||||
AppPasswordFormComponent,
|
AppPasswordFormComponent,
|
||||||
|
ProfilePhotoUploaderComponent,
|
||||||
|
|
||||||
ListingComponent,
|
ListingComponent,
|
||||||
FormComponent,
|
FormComponent,
|
||||||
|
@ -13,7 +13,10 @@ const template = `
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8 offset-2 col-sm-4 offset-sm-0">
|
<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>
|
||||||
<div class="col-sm-8 offset-sm-0 col-12 text-sm-left text-center pad-top">
|
<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>
|
<div class="card-title"><h3>{{ profile_first }} {{ profile_last }}</h3></div>
|
||||||
@ -140,6 +143,11 @@ const template = `
|
|||||||
ref="app_password_modal"
|
ref="app_password_modal"
|
||||||
@modal-success="load_app_passwords"
|
@modal-success="load_app_passwords"
|
||||||
></coreid-form-app-password>
|
></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>
|
</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() {
|
valid_email() {
|
||||||
return this.$refs.email_input.validity.valid
|
return this.$refs.email_input.validity.valid
|
||||||
}
|
}
|
||||||
|
@ -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">×</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
1
app/assets/humans.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Stock profile photo thanks to: https://www.flaticon.com/authors/vitaly-gorbachev
|
@ -61,3 +61,24 @@ body {
|
|||||||
.pad-top {
|
.pad-top {
|
||||||
padding-top: 30px;
|
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
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 |
@ -1,9 +1,10 @@
|
|||||||
const { Controller } = require('libflitter')
|
const { Controller } = require('libflitter')
|
||||||
const Validator = require('email-validator')
|
const Validator = require('email-validator')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
class ProfileController extends Controller {
|
class ProfileController extends Controller {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'models']
|
return [...super.services, 'models', 'utility']
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(req, res, next) {
|
async fetch(req, res, next) {
|
||||||
@ -72,6 +73,45 @@ class ProfileController extends Controller {
|
|||||||
return res.api()
|
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
|
module.exports = exports = ProfileController
|
||||||
|
@ -34,9 +34,15 @@ class User extends AuthUser {
|
|||||||
mfa_enabled: {type: Boolean, default: false},
|
mfa_enabled: {type: Boolean, default: false},
|
||||||
mfa_enable_date: Date,
|
mfa_enable_date: Date,
|
||||||
create_date: {type: Date, default: () => new 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) {
|
has_authorized(client) {
|
||||||
return this.app_authorizations.some(x => x.client_id === client.id)
|
return this.app_authorizations.some(x => x.client_id === client.id)
|
||||||
}
|
}
|
||||||
|
12
app/routing/middleware/upload/UploadFile.middleware.js
Normal file
12
app/routing/middleware/upload/UploadFile.middleware.js
Normal 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
|
@ -10,6 +10,18 @@ const profile_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:profile:get' }],
|
['middleware::api:Permission', { check: 'v1:profile:get' }],
|
||||||
'controller::api:v1:Profile.fetch',
|
'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: {
|
patch: {
|
||||||
|
@ -35,12 +35,14 @@ const server_config = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
uploads: {
|
uploads: {
|
||||||
|
enable: true,
|
||||||
|
allowed_path: /./,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used by flitter-upload. Path for uploaded files.
|
* Used by flitter-upload. Path for uploaded files.
|
||||||
* Should be relative to the application root.
|
* Should be relative to the application root.
|
||||||
*/
|
*/
|
||||||
destination: './uploads'
|
destination: './tmp.uploads',
|
||||||
},
|
},
|
||||||
|
|
||||||
ssl: {
|
ssl: {
|
||||||
|
33
config/upload.config.js
Normal file
33
config/upload.config.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* flitter-upload configuration
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* Specifies the configuration for various uploader aspects. Mainly,
|
||||||
|
* contains the configuration for the different file upload backends.
|
||||||
|
*/
|
||||||
|
const upload_config = {
|
||||||
|
/*
|
||||||
|
* The name of the upload backend to use by default.
|
||||||
|
*/
|
||||||
|
default_store: 'flitter',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stores available to the uploader.
|
||||||
|
*/
|
||||||
|
stores: {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Example of the basic, filesystem-backed uploader.
|
||||||
|
* The name of the store is arbitrary. Here, it's called 'flitter'.
|
||||||
|
*/
|
||||||
|
flitter: {
|
||||||
|
// This is a filesystem backed 'FlitterStore'
|
||||||
|
type: 'FlitterStore',
|
||||||
|
|
||||||
|
// Destination for uploaded files. Will be relative to the root
|
||||||
|
// path of the application.
|
||||||
|
destination: './uploads',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = upload_config
|
@ -23,10 +23,10 @@
|
|||||||
"flitter-flap": "^0.5.2",
|
"flitter-flap": "^0.5.2",
|
||||||
"flitter-forms": "^0.8.1",
|
"flitter-forms": "^0.8.1",
|
||||||
"flitter-less": "^0.5.3",
|
"flitter-less": "^0.5.3",
|
||||||
"flitter-upload": "^0.8.0",
|
"flitter-upload": "^0.8.1",
|
||||||
"is-absolute-url": "^3.0.3",
|
"is-absolute-url": "^3.0.3",
|
||||||
"ldapjs": "^1.0.2",
|
"ldapjs": "^1.0.2",
|
||||||
"libflitter": "^0.51.0",
|
"libflitter": "^0.52.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"mongodb": "^3.5.6",
|
"mongodb": "^3.5.6",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
|
0
tmp.uploads/.gitkeep
Normal file
0
tmp.uploads/.gitkeep
Normal file
16
yarn.lock
16
yarn.lock
@ -1871,10 +1871,10 @@ flitter-orm@^0.3.1:
|
|||||||
sinon "^9.0.0"
|
sinon "^9.0.0"
|
||||||
uuid "^3.4.0"
|
uuid "^3.4.0"
|
||||||
|
|
||||||
flitter-upload@^0.8.0:
|
flitter-upload@^0.8.1:
|
||||||
version "0.8.0"
|
version "0.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/flitter-upload/-/flitter-upload-0.8.0.tgz#653086f5daa7400305dddb0b0993cebed3e2506f"
|
resolved "https://registry.yarnpkg.com/flitter-upload/-/flitter-upload-0.8.1.tgz#c69c2b16f8f5227532fe28373de4c1f8094fcdf6"
|
||||||
integrity sha512-yRhQy/pI7WbXvwcG4zp7eXotj4nkJl/vfitO4L0MAIXGMroduLFOFNwcSYB3xuWGI68i+wJzBvzVmtX46x4Gww==
|
integrity sha512-ASLIAibIriSOes3zODcD40+Ly5mGATvAVf41bOZDTRjnTT6ZG8PTbV3EaU+WerPtIO0cj8ccPiusR8X/nPkKeA==
|
||||||
dependencies:
|
dependencies:
|
||||||
es6-promisify "^6.0.1"
|
es6-promisify "^6.0.1"
|
||||||
mkdirp "^1.0.3"
|
mkdirp "^1.0.3"
|
||||||
@ -2743,10 +2743,10 @@ leven@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
||||||
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
||||||
|
|
||||||
libflitter@^0.51.0:
|
libflitter@^0.52.0:
|
||||||
version "0.51.0"
|
version "0.52.0"
|
||||||
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.51.0.tgz#dbe92f1a7bf7fad4edd4fa05de8c1fa3140975fa"
|
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.52.0.tgz#c9279d5cff93fd6f24b89f46c2add2f0c3e3b0f1"
|
||||||
integrity sha512-O/HNbpyXzZaop2Np8J5ETRWWZ1Vv7eBd5RzYCVIfQS5DqCn+UsQX0sR1HfE3d1TcH0ype3n7KjcRBva3MgjkgQ==
|
integrity sha512-+YR+rww1i7NREfXPJwoxc0yhpmNbMYozgcAed6yfkxK59JS9DBM7cUWwV+dxG0UlFilOxZBGXdj/oKiIs+huvQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
colors "^1.3.3"
|
colors "^1.3.3"
|
||||||
connect-mongodb-session "^2.2.0"
|
connect-mongodb-session "^2.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user