More avatar providers (#622)
Don't fail if the last avatar file does not exist when deleting it Fix lint Remove old global avatar setting and update docs Generate docs Invalidate the avatar cache when uploading a new one Add debug logs Add caching for upload avatars Add cache locks Fix encoding Resize the uploaded image to a max of 1024 pixels Remove the old uploaded avatar if one already exists Add mimetype check for images Set avatar provider to upload when uploading an avatar Add upload avatar provider Make font size smaller to let the initials still look good in smaller sizes Add debug log Add cache and resizing of initials avatars Make font size depend on avatar size Add drawing initials avatar Add initials provider Make the initials avatar provider the default Add routes Add user avatar settings handler methods Add user avatar provider field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/622
This commit is contained in:
parent
c9117dd037
commit
dfb7730b63
24 changed files with 1287 additions and 33 deletions
2
Makefile
2
Makefile
|
@ -21,7 +21,7 @@ GOFLAGS := -v $(EXTRA_GOFLAGS)
|
|||
|
||||
LDFLAGS := -X "code.vikunja.io/api/pkg/version.Version=$(shell git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')" -X "main.Tags=$(TAGS)"
|
||||
|
||||
PACKAGES ?= $(filter-out code.vikunja.io/api/pkg/integrations,$(shell go list))
|
||||
PACKAGES ?= $(filter-out code.vikunja.io/api/pkg/integrations,$(shell go list all | grep code\.vikunja\.io\/api))
|
||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||
|
||||
TAGS ?=
|
||||
|
|
|
@ -178,10 +178,6 @@ migration:
|
|||
redirecturl:
|
||||
|
||||
avatar:
|
||||
# Switch between avatar providers. Possible values are gravatar and default.
|
||||
# gravatar will fetch the avatar based on the user email.
|
||||
# default will return a default avatar for every request.
|
||||
provider: gravatar
|
||||
# When using gravatar, this is the duration in seconds until a cached gravatar user avatar expires
|
||||
gravatarexpiration: 3600
|
||||
|
||||
|
|
|
@ -221,10 +221,6 @@ migration:
|
|||
redirecturl:
|
||||
|
||||
avatar:
|
||||
# Switch between avatar providers. Possible values are gravatar and default.
|
||||
# gravatar will fetch the avatar based on the user email.
|
||||
# default will return a default avatar for every request.
|
||||
provider: gravatar
|
||||
# When using gravatar, this is the duration in seconds until a cached gravatar user avatar expires
|
||||
gravatarexpiration: 3600
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ This document describes the different errors Vikunja can return.
|
|||
| 1015 | 412 | Totp is already enabled for this user. |
|
||||
| 1016 | 412 | Totp is not enabled for this user. |
|
||||
| 1017 | 412 | The provided Totp passcode is invalid. |
|
||||
| 1018 | 412 | The provided user avatar provider type setting is invalid. |
|
||||
|
||||
### Validation
|
||||
|
||||
|
|
4
go.mod
4
go.mod
|
@ -28,12 +28,15 @@ require (
|
|||
github.com/cweill/gotests v1.5.3
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
|
||||
github.com/gabriel-vasile/mimetype v1.1.1
|
||||
github.com/getsentry/sentry-go v0.7.0
|
||||
github.com/go-redis/redis/v7 v7.4.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.3.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
||||
github.com/imdario/mergo v0.3.10
|
||||
|
@ -64,6 +67,7 @@ require (
|
|||
github.com/swaggo/swag v1.6.7
|
||||
github.com/ulule/limiter/v3 v3.5.0
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
|
||||
golang.org/x/image v0.0.0-20200801110659-972c09e46d76
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
|
|
22
go.sum
22
go.sum
|
@ -118,6 +118,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
|||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
|
@ -134,11 +136,11 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
|
|||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 h1:roDmqJ4Qes7hrDOsWsMCce0vQHz3xiMPjJ9m4c2eeNs=
|
||||
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835/go.mod h1:BjL/N0+C+j9uNX+1xcNuM9vdSIcXCZrQZUYbXOFbgN8=
|
||||
github.com/gabriel-vasile/mimetype v1.1.1 h1:qbN9MPuRf3bstHu9zkI9jDWNfH//9+9kHxr9oRBBBOA=
|
||||
github.com/gabriel-vasile/mimetype v1.1.1/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.6.1 h1:K84dY1/57OtWhdyr5lbU78Q/+qgzkEyGc/ud+Sipi5k=
|
||||
github.com/getsentry/sentry-go v0.6.1/go.mod h1:0yZBuzSvbZwBnvaF9VwZIMen3kXscY8/uasKtAX1qG8=
|
||||
github.com/getsentry/sentry-go v0.7.0 h1:MR2yfR4vFfv/2+iBuSnkdQwVg7N9cJzihZ6KJu7srwQ=
|
||||
github.com/getsentry/sentry-go v0.7.0/go.mod h1:pLFpD2Y5RHIKF9Bw3KH6/68DeN2K/XBJd8awjdPnUwg=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
|
@ -205,6 +207,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
|||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
|
@ -284,8 +288,6 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
|||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc=
|
||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
|
@ -417,10 +419,6 @@ github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
|||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc=
|
||||
github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g=
|
||||
github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
|
||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.7.1 h1:FvD5XTVTDt+KON6oIoOmHq6B6HzGuYEhuTMpEG0yuBQ=
|
||||
github.com/lib/pq v1.7.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
|
@ -610,8 +608,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
@ -703,8 +699,6 @@ golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -713,7 +707,11 @@ golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxT
|
|||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw=
|
||||
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
|
|
@ -117,7 +117,6 @@ const (
|
|||
CorsOrigins Key = `cors.origins`
|
||||
CorsMaxAge Key = `cors.maxage`
|
||||
|
||||
AvatarProvider Key = `avatar.provider`
|
||||
AvatarGravaterExpiration Key = `avatar.gravatarexpiration`
|
||||
|
||||
BackgroundsEnabled Key = `backgrounds.enabled`
|
||||
|
@ -273,7 +272,6 @@ func InitDefaultConfig() {
|
|||
MigrationWunderlistEnable.setDefault(false)
|
||||
MigrationTodoistEnable.setDefault(false)
|
||||
// Avatar
|
||||
AvatarProvider.setDefault("gravatar")
|
||||
AvatarGravaterExpiration.setDefault(3600)
|
||||
// List Backgrounds
|
||||
BackgroundsEnabled.setDefault(true)
|
||||
|
|
|
@ -67,7 +67,12 @@ func (f *File) LoadFileMetaByID() (err error) {
|
|||
}
|
||||
|
||||
// Create creates a new file from an FileHeader
|
||||
func Create(f io.ReadCloser, realname string, realsize uint64, a web.Auth) (file *File, err error) {
|
||||
func Create(f io.Reader, realname string, realsize uint64, a web.Auth) (file *File, err error) {
|
||||
return CreateWithMime(f, realname, realsize, a, "")
|
||||
}
|
||||
|
||||
// CreateWithMime creates a new file from an FileHeader and sets its mime type
|
||||
func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
|
||||
|
||||
// Get and parse the configured file size
|
||||
var maxSize datasize.ByteSize
|
||||
|
@ -84,6 +89,7 @@ func Create(f io.ReadCloser, realname string, realsize uint64, a web.Auth) (file
|
|||
Name: realname,
|
||||
Size: realsize,
|
||||
CreatedByID: a.GetID(),
|
||||
Mime: mime,
|
||||
}
|
||||
|
||||
_, err = x.Insert(file)
|
||||
|
@ -111,6 +117,6 @@ func (f *File) Delete() (err error) {
|
|||
}
|
||||
|
||||
// Save saves a file to storage
|
||||
func (f *File) Save(fcontent io.ReadCloser) error {
|
||||
func (f *File) Save(fcontent io.Reader) error {
|
||||
return afs.WriteReader(f.getFileName(), fcontent)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ type ActiveUser struct {
|
|||
|
||||
type activeUsersMap map[int64]*ActiveUser
|
||||
|
||||
// ActiveUsersMap is the type used to save active users
|
||||
// ActiveUsers is the type used to save active users
|
||||
type ActiveUsers struct {
|
||||
users activeUsersMap
|
||||
mutex *sync.Mutex
|
||||
|
|
50
pkg/migration/20200801183357.go
Normal file
50
pkg/migration/20200801183357.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type user20200801183357 struct {
|
||||
AvatarProvider string `xorm:"varchar(255) null" json:"-"`
|
||||
AvatarFileID int64 `xorn:"null" json:"-"`
|
||||
}
|
||||
|
||||
func (s user20200801183357) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200801183357",
|
||||
Description: "Add avatar provider setting to user",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(user20200801183357{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Cols("avatar_provider").Update(&user20200801183357{AvatarProvider: "initials"})
|
||||
return err
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -445,6 +445,9 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (err error) {
|
|||
|
||||
// Get task attachments
|
||||
attachments, err := getTaskAttachmentsByTaskIDs(taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users of a task
|
||||
// aka the ones who created a task
|
||||
|
|
175
pkg/modules/avatar/initials/initials.go
Normal file
175
pkg/modules/avatar/initials/initials.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package initials
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/disintegration/imaging"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"bytes"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
"golang.org/x/image/math/fixed"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
// Provider represents the provider implementation of the initials provider
|
||||
type Provider struct {
|
||||
}
|
||||
|
||||
var (
|
||||
avatarBgColors = []*color.RGBA{
|
||||
{69, 189, 243, 255},
|
||||
{224, 143, 112, 255},
|
||||
{77, 182, 172, 255},
|
||||
{149, 117, 205, 255},
|
||||
{176, 133, 94, 255},
|
||||
{240, 98, 146, 255},
|
||||
{163, 211, 108, 255},
|
||||
{121, 134, 203, 255},
|
||||
{241, 185, 29, 255},
|
||||
}
|
||||
|
||||
// Contain the created avatars with a size of defaultSize
|
||||
cache = map[int64]*image.RGBA64{}
|
||||
cacheLock = sync.Mutex{}
|
||||
cacheResized = map[string][]byte{}
|
||||
cacheResizedLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
cache = make(map[int64]*image.RGBA64)
|
||||
cacheResized = make(map[string][]byte)
|
||||
}
|
||||
|
||||
const (
|
||||
dpi = 72
|
||||
defaultSize = 1024
|
||||
)
|
||||
|
||||
func drawImage(text rune, bg *color.RGBA) (img *image.RGBA64, err error) {
|
||||
|
||||
size := defaultSize
|
||||
fontSize := float64(size) * 0.8
|
||||
|
||||
// Inspired by https://github.com/holys/initials-avatar
|
||||
|
||||
// Get the font
|
||||
f, err := truetype.Parse(goregular.TTF)
|
||||
if err != nil {
|
||||
return img, err
|
||||
}
|
||||
|
||||
// Build the image background
|
||||
img = image.NewRGBA64(image.Rect(0, 0, size, size))
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
|
||||
|
||||
// Add the text
|
||||
drawer := &font.Drawer{
|
||||
Dst: img,
|
||||
Src: image.White,
|
||||
Face: truetype.NewFace(f, &truetype.Options{
|
||||
Size: fontSize,
|
||||
DPI: dpi,
|
||||
Hinting: font.HintingNone,
|
||||
}),
|
||||
}
|
||||
|
||||
// Font Index
|
||||
fi := f.Index(text)
|
||||
|
||||
// Glyph example: http://www.freetype.org/freetype2/docs/tutorial/metrics.png
|
||||
var gbuf truetype.GlyphBuf
|
||||
fsize := fixed.Int26_6(fontSize * dpi * (64.0 / 72.0))
|
||||
err = gbuf.Load(f, fsize, fi, font.HintingFull)
|
||||
if err != nil {
|
||||
drawer.DrawString("")
|
||||
return img, err
|
||||
}
|
||||
|
||||
// Center
|
||||
dY := (size - int(gbuf.Bounds.Max.Y-gbuf.Bounds.Min.Y)>>6) / 2
|
||||
dX := (size - int(gbuf.Bounds.Max.X-gbuf.Bounds.Min.X)>>6) / 2
|
||||
y := int(gbuf.Bounds.Max.Y>>6) + dY
|
||||
x := 0 - int(gbuf.Bounds.Min.X>>6) + dX
|
||||
|
||||
drawer.Dot = fixed.Point26_6{
|
||||
X: fixed.I(x),
|
||||
Y: fixed.I(y),
|
||||
}
|
||||
drawer.DrawString(string(text))
|
||||
|
||||
return img, err
|
||||
}
|
||||
|
||||
func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
|
||||
var cached bool
|
||||
fullSizeAvatar, cached = cache[u.ID]
|
||||
if !cached {
|
||||
log.Debugf("Initials avatar for user %d not cached, creating...", u.ID)
|
||||
firstRune := []rune(strings.ToUpper(u.Username))[0]
|
||||
bg := avatarBgColors[int(u.ID)%len(avatarBgColors)] // Random color based on the user id
|
||||
|
||||
fullSizeAvatar, err = drawImage(firstRune, bg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheLock.Lock()
|
||||
cache[u.ID] = fullSizeAvatar
|
||||
cacheLock.Unlock()
|
||||
}
|
||||
|
||||
return fullSizeAvatar, err
|
||||
}
|
||||
|
||||
// GetAvatar returns an initials avatar for a user
|
||||
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
|
||||
|
||||
var cached bool
|
||||
cacheKey := strconv.Itoa(int(u.ID)) + "_" + strconv.Itoa(int(size))
|
||||
avatar, cached = cacheResized[cacheKey]
|
||||
if !cached {
|
||||
log.Debugf("Initials avatar for user %d and size %d not cached, creating...", u.ID, size)
|
||||
fullAvatar, err := getAvatarForUser(u)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
img := imaging.Resize(fullAvatar, int(size), int(size), imaging.Lanczos)
|
||||
buf := &bytes.Buffer{}
|
||||
err = png.Encode(buf, img)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
avatar = buf.Bytes()
|
||||
cacheResizedLock.Lock()
|
||||
cacheResized[cacheKey] = avatar
|
||||
cacheResizedLock.Unlock()
|
||||
} else {
|
||||
log.Debugf("Serving initials avatar for user %d and size %d from cache", u.ID, size)
|
||||
}
|
||||
|
||||
return avatar, "image/png", err
|
||||
}
|
98
pkg/modules/avatar/upload/upload.go
Normal file
98
pkg/modules/avatar/upload/upload.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/disintegration/imaging"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// This is a map with a map so we're able to clear all cached avatar (in all sizes) for one user at once
|
||||
// The first map has as key the user id, the second one has the size as key
|
||||
resizedCache = map[int64]map[int64][]byte{}
|
||||
resizedCacheLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
resizedCache = make(map[int64]map[int64][]byte)
|
||||
}
|
||||
|
||||
// Provider represents the upload avatar provider
|
||||
type Provider struct {
|
||||
}
|
||||
|
||||
// GetAvatar returns an uploaded user avatar
|
||||
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
|
||||
|
||||
a, cached := resizedCache[u.ID]
|
||||
if cached {
|
||||
if a != nil && a[size] != nil {
|
||||
log.Debugf("Serving uploaded avatar for user %d and size %d from cache.", u.ID, size)
|
||||
return a[size], "", nil
|
||||
}
|
||||
// This means we have a map for the user, but nothing in it.
|
||||
if a == nil {
|
||||
resizedCache[u.ID] = make(map[int64][]byte)
|
||||
}
|
||||
} else {
|
||||
// Nothing ever cached for this user so we need to create the size map to avoid panics
|
||||
resizedCache[u.ID] = make(map[int64][]byte)
|
||||
}
|
||||
|
||||
log.Debugf("Uploaded avatar for user %d and size %d not cached, resizing and caching.", u.ID, size)
|
||||
|
||||
// If we get this far, the avatar is either not cached at all or not in this size
|
||||
f := &files.File{ID: u.AvatarFileID}
|
||||
if err := f.LoadFileByID(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(f.File)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
resizedImg := imaging.Resize(img, 0, int(size), imaging.Lanczos)
|
||||
buf := &bytes.Buffer{}
|
||||
if err := png.Encode(buf, resizedImg); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
avatar, err = ioutil.ReadAll(buf)
|
||||
resizedCacheLock.Lock()
|
||||
resizedCache[u.ID][size] = avatar
|
||||
resizedCacheLock.Unlock()
|
||||
return avatar, f.Mime, err
|
||||
}
|
||||
|
||||
// InvalidateCache invalidates the avatar cache for a user
|
||||
func InvalidateCache(u *user.User) {
|
||||
resizedCacheLock.Lock()
|
||||
delete(resizedCache, u.ID)
|
||||
resizedCacheLock.Unlock()
|
||||
}
|
|
@ -25,9 +25,12 @@ import (
|
|||
v1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/web"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/labstack/echo/v4"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BackgroundProvider represents a thing which holds a background provider
|
||||
|
@ -132,7 +135,18 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
|
|||
}
|
||||
defer src.Close()
|
||||
|
||||
f, err := files.Create(src, file.Filename, uint64(file.Size), auth)
|
||||
// Validate we're dealing with an image
|
||||
mime, err := mimetype.DetectReader(src)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if !strings.HasPrefix(mime.String(), "image") {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{Message: "Uploaded file is no image."})
|
||||
}
|
||||
_, _ = src.Seek(0, io.SeekStart)
|
||||
|
||||
// Save the file
|
||||
f, err := files.CreateWithMime(src, file.Filename, uint64(file.Size), auth, mime.String())
|
||||
if err != nil {
|
||||
if files.IsErrFileIsTooLarge(err) {
|
||||
return echo.ErrBadRequest
|
||||
|
|
|
@ -43,6 +43,7 @@ func (p *Provider) Search(search string, page int64) (result []*background.Image
|
|||
// @Param background formData string true "The file as single file."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.Message "The background was set successfully."
|
||||
// @Failure 400 {object} models.Message "File is no image."
|
||||
// @Failure 403 {object} models.Message "No access to the list."
|
||||
// @Failure 403 {object} models.Message "File too large."
|
||||
// @Failure 404 {object} models.Message "The list does not exist."
|
||||
|
|
|
@ -17,16 +17,27 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/avatar"
|
||||
"code.vikunja.io/api/pkg/modules/avatar/empty"
|
||||
"code.vikunja.io/api/pkg/modules/avatar/gravatar"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/modules/avatar/initials"
|
||||
"code.vikunja.io/api/pkg/modules/avatar/upload"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
"bytes"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/labstack/echo/v4"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetAvatar returns a user's avatar
|
||||
|
@ -45,7 +56,7 @@ func GetAvatar(c echo.Context) error {
|
|||
username := c.Param("username")
|
||||
|
||||
// Get the user
|
||||
user, err := user2.GetUserWithEmail(&user2.User{Username: username})
|
||||
u, err := user.GetUserWithEmail(&user.User{Username: username})
|
||||
if err != nil {
|
||||
log.Errorf("Error getting user for avatar: %v", err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
@ -55,9 +66,13 @@ func GetAvatar(c echo.Context) error {
|
|||
// For now, we only have one avatar provider, in the future there could be multiple which
|
||||
// could be changed based on user settings etc.
|
||||
var avatarProvider avatar.Provider
|
||||
switch config.AvatarProvider.GetString() {
|
||||
switch u.AvatarProvider {
|
||||
case "gravatar":
|
||||
avatarProvider = &gravatar.Provider{}
|
||||
case "initials":
|
||||
avatarProvider = &initials.Provider{}
|
||||
case "upload":
|
||||
avatarProvider = &upload.Provider{}
|
||||
default:
|
||||
avatarProvider = &empty.Provider{}
|
||||
}
|
||||
|
@ -73,11 +88,100 @@ func GetAvatar(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Get the avatar
|
||||
a, mimeType, err := avatarProvider.GetAvatar(user, sizeInt)
|
||||
a, mimeType, err := avatarProvider.GetAvatar(u, sizeInt)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting avatar for user %d: %v", user.ID, err)
|
||||
log.Errorf("Error getting avatar for user %d: %v", u.ID, err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.Blob(http.StatusOK, mimeType, a)
|
||||
}
|
||||
|
||||
// UploadAvatar uploads and sets a user avatar
|
||||
// @Summary Upload a user avatar
|
||||
// @Description Upload a user avatar. This will also set the user's avatar provider to "upload"
|
||||
// @tags user
|
||||
// @Accept mpfd
|
||||
// @Produce json
|
||||
// @Param avatar formData string true "The avatar as single file."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.Message "The avatar was set successfully."
|
||||
// @Failure 400 {object} models.Message "File is no image."
|
||||
// @Failure 403 {object} models.Message "File too large."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /user/settings/avatar/upload [put]
|
||||
func UploadAvatar(c echo.Context) (err error) {
|
||||
|
||||
uc, err := user.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
u, err := user.GetUserByID(uc.ID)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Get + upload the image
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Validate we're dealing with an image
|
||||
mime, err := mimetype.DetectReader(src)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if !strings.HasPrefix(mime.String(), "image") {
|
||||
return c.JSON(http.StatusBadRequest, models.Message{Message: "Uploaded file is no image."})
|
||||
}
|
||||
_, _ = src.Seek(0, io.SeekStart)
|
||||
|
||||
// Remove the old file if one exists
|
||||
if u.AvatarFileID != 0 {
|
||||
f := &files.File{ID: u.AvatarFileID}
|
||||
if err := f.Delete(); err != nil {
|
||||
if !files.IsErrFileDoesNotExist(err) {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
}
|
||||
u.AvatarFileID = 0
|
||||
}
|
||||
|
||||
// Resize the new file to a max height of 1024
|
||||
img, _, err := image.Decode(src)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
resizedImg := imaging.Resize(img, 0, 1024, imaging.Lanczos)
|
||||
buf := &bytes.Buffer{}
|
||||
if err := png.Encode(buf, resizedImg); err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
upload.InvalidateCache(u)
|
||||
|
||||
// Save the file
|
||||
f, err := files.CreateWithMime(buf, file.Filename, uint64(file.Size), u, "image/png")
|
||||
if err != nil {
|
||||
if files.IsErrFileIsTooLarge(err) {
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
u.AvatarFileID = f.ID
|
||||
u.AvatarProvider = "upload"
|
||||
|
||||
if _, err := user.UpdateUser(u); err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "Avatar was uploaded successfully."})
|
||||
}
|
||||
|
|
97
pkg/routes/api/v1/user_settings.go
Normal file
97
pkg/routes/api/v1/user_settings.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserAvatarProvider holds the user avatar provider type
|
||||
type UserAvatarProvider struct {
|
||||
AvatarProvider string `json:"avatar_provider"`
|
||||
}
|
||||
|
||||
// GetUserAvatarProvider returns the currently set user avatar
|
||||
// @Summary Return user avatar setting
|
||||
// @Description Returns the current user's avatar setting.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} UserAvatarProvider
|
||||
// @Failure 400 {object} web.HTTPError "Something's invalid."
|
||||
// @Failure 500 {object} models.Message "Internal server error."
|
||||
// @Router /user/settings/avatar [get]
|
||||
func GetUserAvatarProvider(c echo.Context) error {
|
||||
|
||||
u, err := user2.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
user, err := user2.GetUserWithEmail(u)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
uap := &UserAvatarProvider{AvatarProvider: user.AvatarProvider}
|
||||
return c.JSON(http.StatusOK, uap)
|
||||
}
|
||||
|
||||
// ChangeUserAvatarProvider changes the user's avatar provider
|
||||
// @Summary Set the user's avatar
|
||||
// @Description Changes the user avatar. Valid types are gravatar (uses the user email), upload, initials, default.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param avatar body UserAvatarProvider true "The user's avatar setting"
|
||||
// @Success 200 {object} UserAvatarProvider
|
||||
// @Failure 400 {object} web.HTTPError "Something's invalid."
|
||||
// @Failure 500 {object} models.Message "Internal server error."
|
||||
// @Router /user/settings/avatar [post]
|
||||
func ChangeUserAvatarProvider(c echo.Context) error {
|
||||
|
||||
uap := &UserAvatarProvider{}
|
||||
err := c.Bind(uap)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad avatar type provided.")
|
||||
}
|
||||
|
||||
u, err := user2.GetCurrentUser(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
user, err := user2.GetUserWithEmail(u)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
user.AvatarProvider = uap.AvatarProvider
|
||||
|
||||
_, err = user2.UpdateUser(user)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, &models.Message{Message: "Avatar was changed successfully."})
|
||||
}
|
|
@ -254,6 +254,9 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
u.GET("s", apiv1.UserList)
|
||||
u.POST("/token", apiv1.RenewToken)
|
||||
u.POST("/settings/email", apiv1.UpdateUserEmail)
|
||||
u.GET("/settings/avatar", apiv1.GetUserAvatarProvider)
|
||||
u.POST("/settings/avatar", apiv1.ChangeUserAvatarProvider)
|
||||
u.PUT("/settings/avatar/upload", apiv1.UploadAvatar)
|
||||
|
||||
if config.ServiceEnableTotp.GetBool() {
|
||||
u.GET("/settings/totp", apiv1.UserTOTP)
|
||||
|
|
|
@ -923,6 +923,12 @@ var doc = `{
|
|||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "File is no image.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "File too large.",
|
||||
"schema": {
|
||||
|
@ -1517,6 +1523,70 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/lists/{listID}/duplicate": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Copies the list, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one list to a new namespace. The user needs read access in the list and write access in the namespace of the new list.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"list"
|
||||
],
|
||||
"summary": "Duplicate an existing list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The list ID to duplicate",
|
||||
"name": "listID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The target namespace which should hold the copied list.",
|
||||
"name": "list",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListDuplicate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListDuplicate"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid list duplicate object provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the list or namespace",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/lists/{listID}/tasks": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -5476,6 +5546,150 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/avatar": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns the current user's avatar setting.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Return user avatar setting",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserAvatarProvider"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Something's invalid.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Changes the user avatar. Valid types are gravatar (uses the user email), upload, initials, default.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Set the user's avatar",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The user's avatar setting",
|
||||
"name": "avatar",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserAvatarProvider"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserAvatarProvider"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Something's invalid.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/avatar/upload": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Upload a user avatar. This will also set the user's avatar provider to \"upload\"",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Upload a user avatar",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The avatar as single file.",
|
||||
"name": "avatar",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The avatar was set successfully.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "File is no image.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "File too large.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/email": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -6297,6 +6511,20 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.ListDuplicate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"description": "The copied list",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.List"
|
||||
},
|
||||
"namespace_id": {
|
||||
"description": "The target namespace ID",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ListUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7037,6 +7265,14 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.UserAvatarProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserPassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7048,6 +7284,17 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.legalInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imprint_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"privacy_policy_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.vikunjaInfos": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7066,6 +7313,10 @@ var doc = `{
|
|||
"frontend_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"legal": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/v1.legalInfo"
|
||||
},
|
||||
"link_sharing_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -906,6 +906,12 @@
|
|||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "File is no image.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "File too large.",
|
||||
"schema": {
|
||||
|
@ -1500,6 +1506,70 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/lists/{listID}/duplicate": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Copies the list, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one list to a new namespace. The user needs read access in the list and write access in the namespace of the new list.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"list"
|
||||
],
|
||||
"summary": "Duplicate an existing list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "The list ID to duplicate",
|
||||
"name": "listID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The target namespace which should hold the copied list.",
|
||||
"name": "list",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListDuplicate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ListDuplicate"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid list duplicate object provided.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the list or namespace",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/lists/{listID}/tasks": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -5459,6 +5529,150 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/avatar": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns the current user's avatar setting.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Return user avatar setting",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserAvatarProvider"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Something's invalid.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Changes the user avatar. Valid types are gravatar (uses the user email), upload, initials, default.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Set the user's avatar",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The user's avatar setting",
|
||||
"name": "avatar",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserAvatarProvider"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserAvatarProvider"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Something's invalid.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/avatar/upload": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Upload a user avatar. This will also set the user's avatar provider to \"upload\"",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Upload a user avatar",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The avatar as single file.",
|
||||
"name": "avatar",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The avatar was set successfully.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "File is no image.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "File too large.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/email": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -6279,6 +6493,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.ListDuplicate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"description": "The copied list",
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.List"
|
||||
},
|
||||
"namespace_id": {
|
||||
"description": "The target namespace ID",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ListUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7019,6 +7247,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.UserAvatarProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserPassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7030,6 +7266,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.legalInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imprint_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"privacy_policy_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.vikunjaInfos": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7048,6 +7295,10 @@
|
|||
"frontend_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"legal": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/v1.legalInfo"
|
||||
},
|
||||
"link_sharing_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -320,6 +320,16 @@ definitions:
|
|||
this value.
|
||||
type: string
|
||||
type: object
|
||||
models.ListDuplicate:
|
||||
properties:
|
||||
list:
|
||||
$ref: '#/definitions/models.List'
|
||||
description: The copied list
|
||||
type: object
|
||||
namespace_id:
|
||||
description: The target namespace ID
|
||||
type: integer
|
||||
type: object
|
||||
models.ListUser:
|
||||
properties:
|
||||
created:
|
||||
|
@ -905,6 +915,11 @@ definitions:
|
|||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.UserAvatarProvider:
|
||||
properties:
|
||||
avatar_provider:
|
||||
type: string
|
||||
type: object
|
||||
v1.UserPassword:
|
||||
properties:
|
||||
new_password:
|
||||
|
@ -912,6 +927,13 @@ definitions:
|
|||
old_password:
|
||||
type: string
|
||||
type: object
|
||||
v1.legalInfo:
|
||||
properties:
|
||||
imprint_url:
|
||||
type: string
|
||||
privacy_policy_url:
|
||||
type: string
|
||||
type: object
|
||||
v1.vikunjaInfos:
|
||||
properties:
|
||||
available_migrators:
|
||||
|
@ -924,6 +946,9 @@ definitions:
|
|||
type: array
|
||||
frontend_url:
|
||||
type: string
|
||||
legal:
|
||||
$ref: '#/definitions/v1.legalInfo'
|
||||
type: object
|
||||
link_sharing_enabled:
|
||||
type: boolean
|
||||
max_file_size:
|
||||
|
@ -1578,6 +1603,10 @@ paths:
|
|||
description: The background was set successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"400":
|
||||
description: File is no image.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"403":
|
||||
description: File too large.
|
||||
schema:
|
||||
|
@ -2139,6 +2168,50 @@ paths:
|
|||
summary: Update an existing bucket
|
||||
tags:
|
||||
- task
|
||||
/lists/{listID}/duplicate:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Copies the list, tasks, files, kanban data, assignees, comments,
|
||||
attachments, lables, relations, backgrounds, user/team rights and link shares
|
||||
from one list to a new namespace. The user needs read access in the list and
|
||||
write access in the namespace of the new list.
|
||||
parameters:
|
||||
- description: The list ID to duplicate
|
||||
in: path
|
||||
name: listID
|
||||
required: true
|
||||
type: integer
|
||||
- description: The target namespace which should hold the copied list.
|
||||
in: body
|
||||
name: list
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.ListDuplicate'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The created list.
|
||||
schema:
|
||||
$ref: '#/definitions/models.ListDuplicate'
|
||||
"400":
|
||||
description: Invalid list duplicate object provided.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"403":
|
||||
description: The user does not have access to the list or namespace
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Duplicate an existing list
|
||||
tags:
|
||||
- list
|
||||
/lists/{listID}/tasks:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -4603,6 +4676,99 @@ paths:
|
|||
summary: Request password reset token
|
||||
tags:
|
||||
- user
|
||||
/user/settings/avatar:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the current user's avatar setting.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.UserAvatarProvider'
|
||||
"400":
|
||||
description: Something's invalid.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal server error.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Return user avatar setting
|
||||
tags:
|
||||
- user
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Changes the user avatar. Valid types are gravatar (uses the user
|
||||
email), upload, initials, default.
|
||||
parameters:
|
||||
- description: The user's avatar setting
|
||||
in: body
|
||||
name: avatar
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/v1.UserAvatarProvider'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.UserAvatarProvider'
|
||||
"400":
|
||||
description: Something's invalid.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal server error.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Set the user's avatar
|
||||
tags:
|
||||
- user
|
||||
/user/settings/avatar/upload:
|
||||
put:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: Upload a user avatar. This will also set the user's avatar provider
|
||||
to "upload"
|
||||
parameters:
|
||||
- description: The avatar as single file.
|
||||
in: formData
|
||||
name: avatar
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The avatar was set successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"400":
|
||||
description: File is no image.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"403":
|
||||
description: File too large.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Upload a user avatar
|
||||
tags:
|
||||
- user
|
||||
/user/settings/email:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
@ -366,3 +366,30 @@ func (err ErrInvalidTOTPPasscode) HTTPError() web.HTTPError {
|
|||
Message: "Invalid totp passcode.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInvalidAvatarProvider represents a "InvalidAvatarProvider" kind of error.
|
||||
type ErrInvalidAvatarProvider struct {
|
||||
AvatarProvider string
|
||||
}
|
||||
|
||||
// IsErrInvalidAvatarProvider checks if an error is a ErrInvalidAvatarProvider.
|
||||
func IsErrInvalidAvatarProvider(err error) bool {
|
||||
_, ok := err.(ErrInvalidAvatarProvider)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidAvatarProvider) Error() string {
|
||||
return "Invalid avatar provider"
|
||||
}
|
||||
|
||||
// ErrCodeInvalidAvatarProvider holds the unique world-error code of this error
|
||||
const ErrCodeInvalidAvatarProvider = 1018
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidAvatarProvider) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeInvalidAvatarProvider,
|
||||
Message: "Invalid avatar provider setting. See docs for valid types.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,9 @@ type User struct {
|
|||
PasswordResetToken string `xorm:"varchar(450) null" json:"-"`
|
||||
EmailConfirmToken string `xorm:"varchar(450) null" json:"-"`
|
||||
|
||||
AvatarProvider string `xorm:"varchar(255) null" json:"-"`
|
||||
AvatarFileID int64 `xorn:"null" json:"-"`
|
||||
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this task was last updated. You cannot change this value.
|
||||
|
@ -269,6 +272,8 @@ func CreateUser(user *User) (newUser *User, err error) {
|
|||
newUser.EmailConfirmToken = utils.MakeRandomString(60)
|
||||
}
|
||||
|
||||
newUser.AvatarProvider = "initials"
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(newUser)
|
||||
if err != nil {
|
||||
|
@ -323,6 +328,16 @@ func UpdateUser(user *User) (updatedUser *User, err error) {
|
|||
|
||||
user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it
|
||||
|
||||
// Validate the avatar type
|
||||
if user.AvatarProvider != "" {
|
||||
if user.AvatarProvider != "default" &&
|
||||
user.AvatarProvider != "gravatar" &&
|
||||
user.AvatarProvider != "initials" &&
|
||||
user.AvatarProvider != "upload" {
|
||||
return updatedUser, &ErrInvalidAvatarProvider{AvatarProvider: user.AvatarProvider}
|
||||
}
|
||||
}
|
||||
|
||||
// Update it
|
||||
_, err = x.ID(user.ID).Update(user)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue