Merge branch 'main' into webhook-events
continuous-integration/drone/pr Build is failing
Details
continuous-integration/drone/pr Build is failing
Details
This commit is contained in:
commit
bc53f9dcc5
|
@ -56,6 +56,9 @@ service:
|
||||||
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||||
# for user deletion.
|
# for user deletion.
|
||||||
enableuserdeletion: true
|
enableuserdeletion: true
|
||||||
|
# The maximum size clients will be able to request for user avatars.
|
||||||
|
# If clients request a size bigger than this, it will be changed on the fly.
|
||||||
|
maxavatarsize: 1024
|
||||||
|
|
||||||
database:
|
database:
|
||||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||||
|
|
|
@ -76,7 +76,7 @@ Default: `<jwt-secret>`
|
||||||
|
|
||||||
Full path: `service.JWTSecret`
|
Full path: `service.JWTSecret`
|
||||||
|
|
||||||
Environment path: `VIKUNJA_SERVICE_JWT_SECRET`
|
Environment path: `VIKUNJA_SERVICE_JWTSECRET`
|
||||||
|
|
||||||
|
|
||||||
### jwtttl
|
### jwtttl
|
||||||
|
@ -321,6 +321,18 @@ Full path: `service.enableuserdeletion`
|
||||||
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
|
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
|
||||||
|
|
||||||
|
|
||||||
|
### maxavatarsize
|
||||||
|
|
||||||
|
The maximum size clients will be able to request for user avatars.
|
||||||
|
If clients request a size bigger than this, it will be changed on the fly.
|
||||||
|
|
||||||
|
Default: `1024`
|
||||||
|
|
||||||
|
Full path: `service.maxavatarsize`
|
||||||
|
|
||||||
|
Environment path: `VIKUNJA_SERVICE_MAXAVATARSIZE`
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## database
|
## database
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -56,7 +56,7 @@ require (
|
||||||
github.com/spf13/afero v1.8.2
|
github.com/spf13/afero v1.8.2
|
||||||
github.com/spf13/cobra v1.4.0
|
github.com/spf13/cobra v1.4.0
|
||||||
github.com/spf13/viper v1.11.0
|
github.com/spf13/viper v1.11.0
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.7.2
|
||||||
github.com/swaggo/swag v1.8.2
|
github.com/swaggo/swag v1.8.2
|
||||||
github.com/tkuchiki/go-timezone v0.2.2
|
github.com/tkuchiki/go-timezone v0.2.2
|
||||||
github.com/ulule/limiter/v3 v3.10.0
|
github.com/ulule/limiter/v3 v3.10.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -748,6 +748,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||||
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||||
|
|
|
@ -1051,7 +1051,7 @@ func printConfig(config []*configOption, level int, parent string) (rendered str
|
||||||
fullPath := parent + "." + option.key
|
fullPath := parent + "." + option.key
|
||||||
|
|
||||||
rendered += "Full path: `" + fullPath + "`\n\n"
|
rendered += "Full path: `" + fullPath + "`\n\n"
|
||||||
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(fullPath) + "`\n\n"
|
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(strings.ToUpper(fullPath)) + "`\n\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ const (
|
||||||
ServiceTestingtoken Key = `service.testingtoken`
|
ServiceTestingtoken Key = `service.testingtoken`
|
||||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||||
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
||||||
|
ServiceMaxAvatarSize Key = `service.maxavatarsize`
|
||||||
|
|
||||||
AuthLocalEnabled Key = `auth.local.enabled`
|
AuthLocalEnabled Key = `auth.local.enabled`
|
||||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||||
|
@ -295,6 +296,7 @@ func InitDefaultConfig() {
|
||||||
ServiceEnableTotp.setDefault(true)
|
ServiceEnableTotp.setDefault(true)
|
||||||
ServiceEnableEmailReminders.setDefault(true)
|
ServiceEnableEmailReminders.setDefault(true)
|
||||||
ServiceEnableUserDeletion.setDefault(true)
|
ServiceEnableUserDeletion.setDefault(true)
|
||||||
|
ServiceMaxAvatarSize.setDefault(1024)
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
AuthLocalEnabled.setDefault(true)
|
AuthLocalEnabled.setDefault(true)
|
||||||
|
|
|
@ -19,35 +19,83 @@ package models
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/user"
|
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/cron"
|
"code.vikunja.io/api/pkg/cron"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/log"
|
"code.vikunja.io/api/pkg/log"
|
||||||
"code.vikunja.io/api/pkg/notifications"
|
"code.vikunja.io/api/pkg/notifications"
|
||||||
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
|
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) {
|
||||||
now = utils.GetTimeWithoutNanoSeconds(now)
|
now = utils.GetTimeWithoutNanoSeconds(now)
|
||||||
|
nextMinute := now.Add(1 * time.Minute)
|
||||||
|
|
||||||
var tasks []*Task
|
var tasks []*Task
|
||||||
err = s.
|
err = s.
|
||||||
Where("due_date is not null and due_date < ?", now.Format(dbTimeFormat)).
|
Where("due_date is not null and due_date < ?", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||||
And("done = false").
|
And("done = false").
|
||||||
Find(&tasks)
|
Find(&tasks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskIDs []int64
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
taskIDs = append(taskIDs, task.ID)
|
taskIDs = append(taskIDs, task.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uts := make(map[int64]*userWithTasks)
|
||||||
|
tzs := make(map[string]*time.Location)
|
||||||
|
for _, t := range users {
|
||||||
|
if t.User.Timezone == "" {
|
||||||
|
t.User.Timezone = config.GetTimeZone().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
tz, exists := tzs[t.User.Timezone]
|
||||||
|
if !exists {
|
||||||
|
tz, err = time.LoadLocation(t.User.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tzs[t.User.Timezone] = tz
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is 9:00 for that current user, add the task to their list of overdue tasks
|
||||||
|
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, tz)
|
||||||
|
isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz))
|
||||||
|
wasTimeForReminder := overdueMailTime.Before(now.Add(time.Minute))
|
||||||
|
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
|
||||||
|
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
|
||||||
|
_, exists := uts[t.User.ID]
|
||||||
|
if !exists {
|
||||||
|
uts[t.User.ID] = &userWithTasks{
|
||||||
|
user: t.User,
|
||||||
|
tasks: []*Task{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type userWithTasks struct {
|
type userWithTasks struct {
|
||||||
|
@ -66,36 +114,18 @@ func RegisterOverdueReminderCron() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cron.Schedule("0 8 * * *", func() {
|
err := cron.Schedule("* * * * *", func() {
|
||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
uts, err := getUndoneOverdueTasks(s, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get tasks with reminders in the next minute: %s", err)
|
log.Errorf("[Undone Overdue Tasks Reminder] Could not get undone overdue tasks in the next minute: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
|
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(uts))
|
||||||
if err != nil {
|
|
||||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get task users to send them reminders: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uts := make(map[int64]*userWithTasks)
|
|
||||||
for _, t := range users {
|
|
||||||
_, exists := uts[t.User.ID]
|
|
||||||
if !exists {
|
|
||||||
uts[t.User.ID] = &userWithTasks{
|
|
||||||
user: t.User,
|
|
||||||
tasks: []*Task{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users))
|
|
||||||
|
|
||||||
for _, ut := range uts {
|
for _, ut := range uts {
|
||||||
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
||||||
|
@ -117,7 +147,6 @@ func RegisterOverdueReminderCron() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
|
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -32,21 +32,34 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
|
||||||
|
|
||||||
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
|
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
tasks, err := getUndoneOverdueTasks(s, now)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, taskIDs, 0)
|
assert.Len(t, tasks, 0)
|
||||||
})
|
})
|
||||||
t.Run("undone overdue", func(t *testing.T) {
|
t.Run("undone overdue", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
|
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
uts, err := getUndoneOverdueTasks(s, now)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, taskIDs, 1)
|
assert.Len(t, uts, 1)
|
||||||
assert.Equal(t, int64(6), taskIDs[0])
|
assert.Len(t, uts[1].tasks, 2)
|
||||||
|
// The tasks don't always have the same order, so we only check their presence, not their position.
|
||||||
|
var task5Present bool
|
||||||
|
var task6Present bool
|
||||||
|
for _, t := range uts[1].tasks {
|
||||||
|
if t.ID == 5 {
|
||||||
|
task5Present = true
|
||||||
|
}
|
||||||
|
if t.ID == 6 {
|
||||||
|
task6Present = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Truef(t, task5Present, "expected task 5 to be present but was not")
|
||||||
|
assert.Truef(t, task6Present, "expected task 6 to be present but was not")
|
||||||
})
|
})
|
||||||
t.Run("done overdue", func(t *testing.T) {
|
t.Run("done overdue", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
|
@ -55,8 +68,8 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
|
||||||
|
|
||||||
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
|
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
tasks, err := getUndoneOverdueTasks(s, now)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, taskIDs, 0)
|
assert.Len(t, tasks, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/files"
|
"code.vikunja.io/api/pkg/files"
|
||||||
"code.vikunja.io/api/pkg/log"
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
@ -49,7 +50,7 @@ import (
|
||||||
// @tags user
|
// @tags user
|
||||||
// @Produce octet-stream
|
// @Produce octet-stream
|
||||||
// @Param username path string true "The username of the user who's avatar you want to get"
|
// @Param username path string true "The username of the user who's avatar you want to get"
|
||||||
// @Param size query int false "The size of the avatar you want to get"
|
// @Param size query int false "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size."
|
||||||
// @Success 200 {} blob "The avatar"
|
// @Success 200 {} blob "The avatar"
|
||||||
// @Failure 404 {object} models.Message "The user does not exist."
|
// @Failure 404 {object} models.Message "The user does not exist."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
@ -97,6 +98,9 @@ func GetAvatar(c echo.Context) error {
|
||||||
return handler.HandleHTTPError(err, c)
|
return handler.HandleHTTPError(err, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if sizeInt > config.ServiceMaxAvatarSize.GetInt64() {
|
||||||
|
sizeInt = config.ServiceMaxAvatarSize.GetInt64()
|
||||||
|
}
|
||||||
|
|
||||||
// Get the avatar
|
// Get the avatar
|
||||||
a, mimeType, err := avatarProvider.GetAvatar(u, sizeInt)
|
a, mimeType, err := avatarProvider.GetAvatar(u, sizeInt)
|
||||||
|
|
|
@ -7537,7 +7537,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The size of the avatar you want to get",
|
"description": "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size.",
|
||||||
"name": "size",
|
"name": "size",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
|
|
|
@ -7528,7 +7528,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The size of the avatar you want to get",
|
"description": "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size.",
|
||||||
"name": "size",
|
"name": "size",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1433,7 +1433,8 @@ paths:
|
||||||
name: username
|
name: username
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: The size of the avatar you want to get
|
- description: The size of the avatar you want to get. If bigger than the max
|
||||||
|
configured size this will be adjusted to the maximum size.
|
||||||
in: query
|
in: query
|
||||||
name: size
|
name: size
|
||||||
type: integer
|
type: integer
|
||||||
|
|
Loading…
Reference in New Issue