-
+
+
+
+
{{ profile_first }} {{ profile_last }}
@@ -140,6 +143,11 @@ const template = `
ref="app_password_modal"
@modal-success="load_app_passwords"
>
+
`
@@ -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
}
diff --git a/app/assets/app/dash/profile/form/ProfilePhotoUploader.component.js b/app/assets/app/dash/profile/form/ProfilePhotoUploader.component.js
new file mode 100644
index 0000000..24d4898
--- /dev/null
+++ b/app/assets/app/dash/profile/form/ProfilePhotoUploader.component.js
@@ -0,0 +1,60 @@
+import { Component } from '../../../../lib/vues6/vues6.js'
+
+const template = `
+
+`
+
+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)
+ }
+ }
+}
diff --git a/app/assets/humans.txt b/app/assets/humans.txt
new file mode 100644
index 0000000..e32af51
--- /dev/null
+++ b/app/assets/humans.txt
@@ -0,0 +1 @@
+Stock profile photo thanks to: https://www.flaticon.com/authors/vitaly-gorbachev
diff --git a/app/assets/less/dashboard.less b/app/assets/less/dashboard.less
index 55b7ee5..16daa29 100644
--- a/app/assets/less/dashboard.less
+++ b/app/assets/less/dashboard.less
@@ -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;
+ }
+}
diff --git a/app/assets/people.png b/app/assets/people.png
new file mode 100644
index 0000000..179d043
Binary files /dev/null and b/app/assets/people.png differ
diff --git a/app/assets/profile.jpg b/app/assets/profile.jpg
deleted file mode 100644
index b48af6d..0000000
Binary files a/app/assets/profile.jpg and /dev/null differ
diff --git a/app/controllers/api/v1/Profile.controller.js b/app/controllers/api/v1/Profile.controller.js
index f2ad0a5..39923e6 100644
--- a/app/controllers/api/v1/Profile.controller.js
+++ b/app/controllers/api/v1/Profile.controller.js
@@ -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
diff --git a/app/models/auth/User.model.js b/app/models/auth/User.model.js
index 2284f10..0a051b7 100644
--- a/app/models/auth/User.model.js
+++ b/app/models/auth/User.model.js
@@ -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)
}
diff --git a/app/routing/middleware/upload/UploadFile.middleware.js b/app/routing/middleware/upload/UploadFile.middleware.js
new file mode 100644
index 0000000..53fe9fb
--- /dev/null
+++ b/app/routing/middleware/upload/UploadFile.middleware.js
@@ -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
diff --git a/app/routing/routers/api/v1/profile.routes.js b/app/routing/routers/api/v1/profile.routes.js
index d6630a5..30eb0b5 100644
--- a/app/routing/routers/api/v1/profile.routes.js
+++ b/app/routing/routers/api/v1/profile.routes.js
@@ -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: {
diff --git a/config/server.config.js b/config/server.config.js
index 261299a..e00919d 100644
--- a/config/server.config.js
+++ b/config/server.config.js
@@ -35,12 +35,14 @@ const server_config = {
},
uploads: {
+ enable: true,
+ allowed_path: /./,
/*
* Used by flitter-upload. Path for uploaded files.
* Should be relative to the application root.
*/
- destination: './uploads'
+ destination: './tmp.uploads',
},
ssl: {
diff --git a/config/upload.config.js b/config/upload.config.js
new file mode 100644
index 0000000..0b1a3cf
--- /dev/null
+++ b/config/upload.config.js
@@ -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
diff --git a/package.json b/package.json
index 1644c6e..cc9246c 100644
--- a/package.json
+++ b/package.json
@@ -23,10 +23,10 @@
"flitter-flap": "^0.5.2",
"flitter-forms": "^0.8.1",
"flitter-less": "^0.5.3",
- "flitter-upload": "^0.8.0",
+ "flitter-upload": "^0.8.1",
"is-absolute-url": "^3.0.3",
"ldapjs": "^1.0.2",
- "libflitter": "^0.51.0",
+ "libflitter": "^0.52.0",
"moment": "^2.24.0",
"mongodb": "^3.5.6",
"qrcode": "^1.4.4",
diff --git a/tmp.uploads/.gitkeep b/tmp.uploads/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/yarn.lock b/yarn.lock
index eb75bc2..b2c7e5a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1871,10 +1871,10 @@ flitter-orm@^0.3.1:
sinon "^9.0.0"
uuid "^3.4.0"
-flitter-upload@^0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/flitter-upload/-/flitter-upload-0.8.0.tgz#653086f5daa7400305dddb0b0993cebed3e2506f"
- integrity sha512-yRhQy/pI7WbXvwcG4zp7eXotj4nkJl/vfitO4L0MAIXGMroduLFOFNwcSYB3xuWGI68i+wJzBvzVmtX46x4Gww==
+flitter-upload@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/flitter-upload/-/flitter-upload-0.8.1.tgz#c69c2b16f8f5227532fe28373de4c1f8094fcdf6"
+ integrity sha512-ASLIAibIriSOes3zODcD40+Ly5mGATvAVf41bOZDTRjnTT6ZG8PTbV3EaU+WerPtIO0cj8ccPiusR8X/nPkKeA==
dependencies:
es6-promisify "^6.0.1"
mkdirp "^1.0.3"
@@ -2743,10 +2743,10 @@ leven@^1.0.2:
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
-libflitter@^0.51.0:
- version "0.51.0"
- resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.51.0.tgz#dbe92f1a7bf7fad4edd4fa05de8c1fa3140975fa"
- integrity sha512-O/HNbpyXzZaop2Np8J5ETRWWZ1Vv7eBd5RzYCVIfQS5DqCn+UsQX0sR1HfE3d1TcH0ype3n7KjcRBva3MgjkgQ==
+libflitter@^0.52.0:
+ version "0.52.0"
+ resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.52.0.tgz#c9279d5cff93fd6f24b89f46c2add2f0c3e3b0f1"
+ integrity sha512-+YR+rww1i7NREfXPJwoxc0yhpmNbMYozgcAed6yfkxK59JS9DBM7cUWwV+dxG0UlFilOxZBGXdj/oKiIs+huvQ==
dependencies:
colors "^1.3.3"
connect-mongodb-session "^2.2.0"