diff --git a/client/web/admin/src/components/CUploader.vue b/client/web/admin/src/components/CUploader.vue
index 281888285..1285620d0 100644
--- a/client/web/admin/src/components/CUploader.vue
+++ b/client/web/admin/src/components/CUploader.vue
@@ -11,7 +11,7 @@
@vdropzone-error="onError"
@vdropzone-upload-progress="onUploadProgress"
>
-
+
{{ $t('internal.signup.enabled') }}
+
+ {{ $t('internal.profile-avatar.enabled') }}
+
+
+
+
+ {{ $t('title') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('general:label.delete') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/admin/src/views/System/User/Editor.vue b/client/web/admin/src/views/System/User/Editor.vue
index 4a9b1617d..17134bb02 100644
--- a/client/web/admin/src/views/System/User/Editor.vue
+++ b/client/web/admin/src/views/System/User/Editor.vue
@@ -52,6 +52,18 @@
@sessionsRevoke="onSessionsRevoke"
/>
+
+
{
+ this.fetchUser()
+ this.$auth.handle()
+ this.animateSuccess('info')
+ this.toastSuccess(this.$t('notification:user.avatarUpload.success'))
+ })
+ .catch(this.toastErrorHandler(this.$t('notification:user.avatarUpload.error')))
+ .finally(() => {
+ this.info.processing = false
+ })
+ },
+
/**
* Handles user delete event, calls user delete API endpoint
* and handles response & errors
@@ -451,6 +487,22 @@ export default {
this.decLoader()
})
},
+
+ onAvatarUpload () {
+ this.fetchUser()
+ },
+
+ onResetAvatar () {
+ const userID = this.userID
+
+ this.$SystemAPI.userDeleteAvatar({ userID })
+ .then(() => {
+ this.fetchUser()
+
+ this.toastSuccess(this.$t('notification:user.avatarDelete.success'))
+ })
+ .catch(this.toastErrorHandler(this.$t('notification:user.avatarDelete.error')))
+ },
},
}
diff --git a/lib/js/src/api-clients/system.ts b/lib/js/src/api-clients/system.ts
index 5e24a6001..2f5012109 100644
--- a/lib/js/src/api-clients/system.ts
+++ b/lib/js/src/api-clients/system.ts
@@ -1478,6 +1478,96 @@ export default class System {
return `/users/${userID}/credentials/${credentialsID}`
}
+ // User's profile avatar
+ async userProfileAvatar (a: KV, extra: AxiosRequestConfig = {}): Promise {
+ const {
+ userID,
+ upload,
+ width,
+ height,
+ } = (a as KV) || {}
+ if (!userID) {
+ throw Error('field userID is empty')
+ }
+ const cfg: AxiosRequestConfig = {
+ ...extra,
+ method: 'post',
+ url: this.userProfileAvatarEndpoint({
+ userID,
+ }),
+ }
+ cfg.data = {
+ upload,
+ width,
+ height,
+ }
+ return this.api().request(cfg).then(result => stdResolve(result))
+ }
+
+ userProfileAvatarEndpoint (a: KV): string {
+ const {
+ userID,
+ } = a || {}
+ return `/users/${userID}/avatar`
+ }
+
+ // User profile avatar initial
+ async userProfileAvatarInitial (a: KV, extra: AxiosRequestConfig = {}): Promise {
+ const {
+ userID,
+ avatarColor,
+ avatarBgColor,
+ } = (a as KV) || {}
+ if (!userID) {
+ throw Error('field userID is empty')
+ }
+ const cfg: AxiosRequestConfig = {
+ ...extra,
+ method: 'post',
+ url: this.userProfileAvatarInitialEndpoint({
+ userID,
+ }),
+ }
+ cfg.data = {
+ avatarColor,
+ avatarBgColor,
+ }
+ return this.api().request(cfg).then(result => stdResolve(result))
+ }
+
+ userProfileAvatarInitialEndpoint (a: KV): string {
+ const {
+ userID,
+ } = a || {}
+ return `/users/${userID}/avatar-initial`
+ }
+
+ // delete user's profile avatar
+ async userDeleteAvatar (a: KV, extra: AxiosRequestConfig = {}): Promise {
+ const {
+ userID,
+ } = (a as KV) || {}
+ if (!userID) {
+ throw Error('field userID is empty')
+ }
+ const cfg: AxiosRequestConfig = {
+ ...extra,
+ method: 'delete',
+ url: this.userDeleteAvatarEndpoint({
+ userID,
+ }),
+ }
+
+ return this.api().request(cfg).then(result => stdResolve(result))
+ }
+
+ userDeleteAvatarEndpoint (a: KV): string {
+ const {
+ userID,
+ } = a || {}
+ return `/users/${userID}/avatar`
+ }
+
// Export users
async userExport (a: KV, extra: AxiosRequestConfig = {}): Promise {
const {
diff --git a/lib/js/src/system/types/user.ts b/lib/js/src/system/types/user.ts
index e9654bc49..0de36438b 100644
--- a/lib/js/src/system/types/user.ts
+++ b/lib/js/src/system/types/user.ts
@@ -11,6 +11,10 @@ interface PartialUser extends Partial
@@ -277,6 +290,17 @@ $nav-user-icon-size: 50px;
overflow-y: auto;
}
+.avatar {
+ border-radius: 50%;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ &:hover {
+ opacity: 0.8;
+ }
+}
+
.spacer {
min-width: 0px;
-webkit-transition: min-width 0.15s ease-in-out;
diff --git a/lib/vue/src/plugins/auth.ts b/lib/vue/src/plugins/auth.ts
index e9d9ac645..b7350e3af 100644
--- a/lib/vue/src/plugins/auth.ts
+++ b/lib/vue/src/plugins/auth.ts
@@ -2,6 +2,7 @@ import axios, { AxiosInstance } from 'axios'
import { Make } from '../libs/url'
import { system } from '@cortezaproject/corteza-js'
import { PluginFunction } from 'vue'
+import { User } from '@cortezaproject/corteza-js/dist/system'
const accessToken = Symbol('accessToken')
const user = Symbol('user')
@@ -44,6 +45,7 @@ interface OAuth2TokenResponse {
handle?: string;
email?: string;
preferred_language?: string;
+ avatarID?: string;
}
interface PluginOpts {
@@ -577,6 +579,8 @@ export class Auth {
u.meta.preferredLanguage = oa2tkn.preferred_language
}
+ u.meta.avatarID = oa2tkn.avatarID
+
this[accessToken] = oa2tkn.access_token
this[user] = u
diff --git a/locale/en/corteza-server/auth/profile.yaml b/locale/en/corteza-server/auth/profile.yaml
index daa353b8b..f2dd41c15 100644
--- a/locale/en/corteza-server/auth/profile.yaml
+++ b/locale/en/corteza-server/auth/profile.yaml
@@ -11,6 +11,14 @@ template:
handle:
label: Handle
placeholder: Short name, nickname or handle
+ avatar:
+ label: Avatar
+ delete: Delete
+ upload: Upload avatar
+ avatar-initial:
+ label: Avatar initial
+ color: Avatar text color
+ background-color: Avatar background color
preferred-language:
label: Preferred language
buttons:
@@ -18,3 +26,5 @@ template:
alerts:
profile-updated: Profile successfully updated
profile-update-fail: Could not update profile due to input errors
+ profile-avatar-uploaded: Profile avatar successfully uploaded
+ profile-avatar-deleted: Profile avatar successfully deleted
diff --git a/locale/en/corteza-webapp-admin/notification.yaml b/locale/en/corteza-webapp-admin/notification.yaml
index 5f800cf8a..725f27b7f 100644
--- a/locale/en/corteza-webapp-admin/notification.yaml
+++ b/locale/en/corteza-webapp-admin/notification.yaml
@@ -43,6 +43,12 @@ user:
error: User suspension failed
import:
success: Users imported successfully
+ avatarUpload:
+ success: Avatar uploaded
+ error: Avatar upload failed
+ avatarDelete:
+ success: Avatar deleted
+ error: Avatar delete failed
role:
fetch:
error: Role fetch failed
diff --git a/locale/en/corteza-webapp-admin/system.settings.yaml b/locale/en/corteza-webapp-admin/system.settings.yaml
index 35c93c839..c5e4e2794 100644
--- a/locale/en/corteza-webapp-admin/system.settings.yaml
+++ b/locale/en/corteza-webapp-admin/system.settings.yaml
@@ -27,6 +27,8 @@ editor:
split-credentials-check:
description: 'Split login into two steps: collect the email input first and show the input for the password on the 2nd screen. Automatically forward user to external identity provider when user does not have his password set and there is only one IdP present'
label: Enable split-credentials check
+ profile-avatar:
+ enabled: Profile avatar enabled
mail:
title: Authentication email sender mail
from-address: Sender's address
@@ -124,13 +126,13 @@ editor:
label: Permitted roles
bgScreen:
- title: Auth Background Screen
+ title: Auth background screen
- image:
- uploader:
- title: Background Image
- instructions: Click or drop background image here to upload
- uploading: Uploading auth background image
+ image:
+ uploader:
+ title: Background image
+ instructions: Click or drop background image here to upload
+ uploading: Uploading auth background image
- editor:
- title: Styles editor
\ No newline at end of file
+ editor:
+ title: Styles editor
\ No newline at end of file
diff --git a/locale/en/corteza-webapp-admin/system.users.yaml b/locale/en/corteza-webapp-admin/system.users.yaml
index 1ed8f8e5a..feebadbf1 100644
--- a/locale/en/corteza-webapp-admin/system.users.yaml
+++ b/locale/en/corteza-webapp-admin/system.users.yaml
@@ -20,6 +20,17 @@ editor:
undelete: Undelete
unsuspend: Unsuspend
updatedAt: Updated at
+ avatar:
+ title: Profile avatar
+ preview:
+ label: Avatar
+ title: Avatar
+ initial:
+ color: Text color
+ backgroundColor: Background color
+ uploader:
+ instructions: Upload avatar
+ uploading: Uploading user avatar
mfa:
TOTP:
disabled:
diff --git a/server/.env.example b/server/.env.example
index 914ae043e..192251c7b 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -1290,3 +1290,32 @@
# Type: string
# Default:
# DISCOVERY_BASE_URL=
+
+###############################################################################
+###############################################################################
+# attachment
+#
+
+###############################################################################
+# Avatar image maximum upload size, default value is 1MB
+# Type: int64
+# Default:
+# ATTACHMENT_AVATAR_MAX_FILE_SIZE=
+
+###############################################################################
+# Avatar initials font file path
+# Type: string
+# Default: ./auth/assets/public/fonts/poppins/Poppins-Regular.ttf
+# AVATAR_INITIALS_FONT_PATH=./auth/assets/public/fonts/poppins/Poppins-Regular.ttf
+
+###############################################################################
+# Avatar initials background color
+# Type: string
+# Default: #F3F3F3
+# AVATAR_INITIALS_BACKGROUND_COLOR=#F3F3F3
+
+###############################################################################
+# Avatar initials text color
+# Type: string
+# Default: #162425
+# AVATAR_INITIALS_COLOR=#162425
diff --git a/server/app/app.cue b/server/app/app.cue
index 5562b6db1..27744b159 100644
--- a/server/app/app.cue
+++ b/server/app/app.cue
@@ -40,6 +40,7 @@ corteza: schema.#platform & {
options.websocket,
options.workflow,
options.discovery,
+ options.attachment,
]
// platform resources
diff --git a/server/app/boot_levels.go b/server/app/boot_levels.go
index 64b6c87d9..5ee1d3a3d 100644
--- a/server/app/boot_levels.go
+++ b/server/app/boot_levels.go
@@ -365,14 +365,15 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
// Note: this is a legacy approach, all services from all 3 apps
// will most likely be merged in the future
err = sysService.Initialize(ctx, app.Log, app.Store, app.WsServer, sysService.Config{
- ActionLog: app.Opt.ActionLog,
- Discovery: app.Opt.Discovery,
- Storage: app.Opt.ObjStore,
- Template: app.Opt.Template,
- DB: app.Opt.DB,
- Auth: app.Opt.Auth,
- RBAC: app.Opt.RBAC,
- Limit: app.Opt.Limit,
+ ActionLog: app.Opt.ActionLog,
+ Discovery: app.Opt.Discovery,
+ Storage: app.Opt.ObjStore,
+ Template: app.Opt.Template,
+ DB: app.Opt.DB,
+ Auth: app.Opt.Auth,
+ RBAC: app.Opt.RBAC,
+ Limit: app.Opt.Limit,
+ Attachment: app.Opt.Attachment,
})
if err != nil {
@@ -622,6 +623,7 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
PasswordCreateEnabled: current.Auth.Internal.PasswordCreate.Enabled,
SplitCredentialsCheck: current.Auth.Internal.SplitCredentialsCheck,
ExternalEnabled: current.Auth.External.Enabled,
+ ProfileAvatarEnabled: current.Auth.Internal.ProfileAvatar.Enabled,
MultiFactor: authSettings.MultiFactor{
TOTP: authSettings.TOTP{
Enabled: current.Auth.MultiFactor.TOTP.Enabled,
diff --git a/server/app/options/attachment.cue b/server/app/options/attachment.cue
new file mode 100644
index 000000000..b5173e902
--- /dev/null
+++ b/server/app/options/attachment.cue
@@ -0,0 +1,32 @@
+package options
+
+import (
+ "github.com/cortezaproject/corteza/server/codegen/schema"
+)
+
+attachment: schema.#optionsGroup & {
+ handle: "attachment"
+
+ options: {
+ avatar_max_file_size: {
+ type: "int64"
+ defaultGoExpr: "1000000"
+ description: "Avatar image maximum upload size, default value is 1MB"
+ }
+ avatar_initials_font_path: {
+ defaultValue: "./auth/assets/public/fonts/poppins/Poppins-Regular.ttf"
+ description: "Avatar initials font file path"
+ env: "AVATAR_INITIALS_FONT_PATH"
+ }
+ avatar_initials_background_color: {
+ defaultValue: "#F3F3F3"
+ description: "Avatar initials background color"
+ env: "AVATAR_INITIALS_BACKGROUND_COLOR"
+ }
+ avatar_initials_color: {
+ defaultValue: "#162425"
+ description: "Avatar initials text color"
+ env: "AVATAR_INITIALS_COLOR"
+ }
+ }
+}
diff --git a/server/auth/assets/public/script.js b/server/auth/assets/public/script.js
index e926e1c94..b7e2db26a 100644
--- a/server/auth/assets/public/script.js
+++ b/server/auth/assets/public/script.js
@@ -11,4 +11,8 @@ $(function () {
})
$('input.mfa-code-mask').mask('000 000')
+
+ $('#avatar').change(function () {
+ $('form').submit();
+ })
})
diff --git a/server/auth/assets/public/style.css b/server/auth/assets/public/style.css
index 18bc0cfce..f4791cd67 100644
--- a/server/auth/assets/public/style.css
+++ b/server/auth/assets/public/style.css
@@ -113,6 +113,10 @@ a:hover {
padding: 1.25rem 0.75rem;
}
+.input-color {
+ padding: 0.125rem 0.25rem;
+}
+
select.form-control {
padding: 0 0.5rem;
}
diff --git a/server/auth/assets/templates/profile.html.tpl b/server/auth/assets/templates/profile.html.tpl
index 33f935d2a..7561a2823 100644
--- a/server/auth/assets/templates/profile.html.tpl
+++ b/server/auth/assets/templates/profile.html.tpl
@@ -1,9 +1,11 @@
{{ template "inc_header.html.tpl" set . "activeNav" "profile" }}
{{ tr "profile.template.title" }}
+
+ {{ if .avatarEnabled }}
+
+
+
+
+

+
+
+
+
+ {{ if .isAvatar }}
+
+ {{ end }}
+
+
+
+
+
+ {{ end }}
+