1
0

Add events (#777)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/777
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad
2021-02-02 22:48:37 +00:00
parent a71aa0c898
commit 0ab9ce9ec4
70 changed files with 1636 additions and 283 deletions

View File

@ -101,6 +101,8 @@ const (
LogHTTP Key = `log.http`
LogEcho Key = `log.echo`
LogPath Key = `log.path`
LogEvents Key = `log.events`
LogEventsLevel Key = `log.eventslevel`
RateLimitEnabled Key = `ratelimit.enabled`
RateLimitKind Key = `ratelimit.kind`
@ -281,6 +283,8 @@ func InitDefaultConfig() {
LogHTTP.setDefault("stdout")
LogEcho.setDefault("off")
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
LogEvents.setDefault("stdout")
LogEventsLevel.setDefault("INFO")
// Rate Limit
RateLimitEnabled.setDefault(false)
RateLimitKind.setDefault("user")

97
pkg/events/events.go Normal file
View File

@ -0,0 +1,97 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package events
import (
"context"
"encoding/json"
"time"
"code.vikunja.io/api/pkg/log"
vmetrics "code.vikunja.io/api/pkg/metrics"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/components/metrics"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
)
var pubsub *gochannel.GoChannel
// Event represents the event interface used by all events
type Event interface {
Name() string
}
// InitEvents sets up everything needed to work with events
func InitEvents() (err error) {
logger := log.NewWatermillLogger()
router, err := message.NewRouter(
message.RouterConfig{},
logger,
)
if err != nil {
return err
}
router.AddMiddleware(
middleware.Retry{
MaxRetries: 5,
InitialInterval: time.Millisecond * 100,
Logger: logger,
Multiplier: 2,
}.Middleware,
middleware.Recoverer,
)
metricsBuilder := metrics.NewPrometheusMetricsBuilder(vmetrics.GetRegistry(), "", "")
metricsBuilder.AddPrometheusRouterMetrics(router)
pubsub = gochannel.NewGoChannel(
gochannel.Config{
OutputChannelBuffer: 1024,
},
logger,
)
for topic, funcs := range listeners {
for _, handler := range funcs {
router.AddNoPublisherHandler(topic+"."+handler.Name(), topic, pubsub, func(msg *message.Message) error {
return handler.Handle(msg.Payload)
})
}
}
return router.Run(context.Background())
}
// Dispatch dispatches an event
func Dispatch(event Event) error {
if isUnderTest {
dispatchedTestEvents = append(dispatchedTestEvents, event)
return nil
}
content, err := json.Marshal(event)
if err != nil {
return err
}
msg := message.NewMessage(watermill.NewUUID(), content)
return pubsub.Publish(event.Name(), msg)
}

36
pkg/events/listeners.go Normal file
View File

@ -0,0 +1,36 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package events
import "github.com/ThreeDotsLabs/watermill/message"
// Listener represents something that listens to events
type Listener interface {
Handle(payload message.Payload) error
Name() string
}
var listeners map[string][]Listener
func init() {
listeners = make(map[string][]Listener)
}
// RegisterListener is used to register a listener when a specific event happens
func RegisterListener(name string, listener Listener) {
listeners[name] = append(listeners[name], listener)
}

49
pkg/events/testing.go Normal file
View File

@ -0,0 +1,49 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package events
import (
"testing"
"github.com/stretchr/testify/assert"
)
var (
isUnderTest bool
dispatchedTestEvents []Event
)
// Fake sets up the "test mode" of the events package. Typically you'd call this function in the TestMain function
// in the package you're testing. It will prevent any events from being fired, instead they will be recorded and be
// available for assertions.
func Fake() {
isUnderTest = true
dispatchedTestEvents = nil
}
// AssertDispatched asserts an event has been dispatched.
func AssertDispatched(t *testing.T, event Event) {
var found bool
for _, testEvent := range dispatchedTestEvents {
if event.Name() == testEvent.Name() {
found = true
break
}
}
assert.True(t, found, "Failed to assert "+event.Name()+" has been dispatched.")
}

29
pkg/initialize/events.go Normal file
View File

@ -0,0 +1,29 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package initialize
import "time"
// BootedEvent represents a BootedEvent event
type BootedEvent struct {
BootedAt time.Time
}
// TopicName defines the name for BootedEvent
func (t *BootedEvent) Name() string {
return "booted"
}

View File

@ -17,8 +17,11 @@
package initialize
import (
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/cron"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/mail"
@ -85,4 +88,21 @@ func FullInit() {
// Start the cron
cron.Init()
models.RegisterReminderCron()
// Start processing events
go func() {
models.RegisterListeners()
user.RegisterListeners()
err := events.InitEvents()
if err != nil {
log.Fatal(err.Error())
}
err = events.Dispatch(&BootedEvent{
BootedAt: time.Now(),
})
if err != nil {
log.Fatal(err)
}
}()
}

View File

@ -24,6 +24,8 @@ import (
"strings"
"testing"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
@ -85,6 +87,7 @@ func setupTestEnv() (e *echo.Echo, err error) {
files.InitTests()
user.InitTests()
models.SetupTests()
events.Fake()
err = db.LoadFixtures()
if err != nil {

View File

@ -49,6 +49,7 @@ func InitLogger() {
config.LogDatabase.Set("off")
config.LogHTTP.Set("off")
config.LogEcho.Set("off")
config.LogEvents.Set("off")
return
}

View File

@ -0,0 +1,91 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package log
import (
"fmt"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
"github.com/ThreeDotsLabs/watermill"
"github.com/op/go-logging"
)
const watermillFmt = `%{color}%{time:` + time.RFC3339Nano + `}: %{level}` + "\t" + `▶ [EVENTS] %{id:03x}%{color:reset} %{message}`
const watermillLogModule = `vikunja_events`
type WatermillLogger struct {
logger *logging.Logger
}
func NewWatermillLogger() *WatermillLogger {
lvl := strings.ToUpper(config.LogEventsLevel.GetString())
level, err := logging.LogLevel(lvl)
if err != nil {
Criticalf("Error setting events log level %s: %s", lvl, err.Error())
}
watermillLogger := &WatermillLogger{
logger: logging.MustGetLogger(watermillLogModule),
}
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
backendLeveled := logging.AddModuleLevel(backend)
backendLeveled.SetLevel(level, watermillLogModule)
watermillLogger.logger.SetBackend(backendLeveled)
return watermillLogger
}
func concatFields(fields watermill.LogFields) string {
full := ""
for key, val := range fields {
full += fmt.Sprintf("%s=%s, ", key, val)
}
if full != "" {
full = full[:len(full)-2]
}
return full
}
func (w *WatermillLogger) Error(msg string, err error, fields watermill.LogFields) {
w.logger.Errorf("%s: %s, %s", msg, err, concatFields(fields))
}
func (w *WatermillLogger) Info(msg string, fields watermill.LogFields) {
w.logger.Infof("%s, %s", msg, concatFields(fields))
}
func (w *WatermillLogger) Debug(msg string, fields watermill.LogFields) {
w.logger.Debugf("%s, %s", msg, concatFields(fields))
}
func (w *WatermillLogger) Trace(msg string, fields watermill.LogFields) {
w.logger.Debugf("%s, %s", msg, concatFields(fields))
}
func (w *WatermillLogger) With(fields watermill.LogFields) watermill.LoggerAdapter {
return w
}

View File

@ -44,7 +44,7 @@ func NewXormLogger(lvl string) *XormLogger {
}
level, err := logging.LogLevel(lvl)
if err != nil {
Critical("Error setting database log level: %s", err.Error())
Criticalf("Error setting database log level: %s", err.Error())
}
xormLogger := &XormLogger{

View File

@ -17,7 +17,6 @@
package metrics
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
"github.com/prometheus/client_golang/prometheus"
@ -41,6 +40,18 @@ const (
TeamCountKey = `teamcount`
)
var registry *prometheus.Registry
func GetRegistry() *prometheus.Registry {
if registry == nil {
registry = prometheus.NewRegistry()
registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
registry.MustRegister(prometheus.NewGoCollector())
}
return registry
}
// InitMetrics Initializes the metrics
func InitMetrics() {
// init active users, sometimes we'll have garbage from previous runs in redis instead
@ -48,50 +59,67 @@ func InitMetrics() {
log.Fatalf("Could not set initial count for active users, error was %s", err)
}
GetRegistry()
// Register total list count metric
promauto.NewGaugeFunc(prometheus.GaugeOpts{
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_list_count",
Help: "The number of lists on this instance",
}, func() float64 {
count, _ := GetCount(ListCountKey)
return float64(count)
})
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", ListCountKey, err)
}
// Register total user count metric
promauto.NewGaugeFunc(prometheus.GaugeOpts{
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_user_count",
Help: "The total number of users on this instance",
}, func() float64 {
count, _ := GetCount(UserCountKey)
return float64(count)
})
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", UserCountKey, err)
}
// Register total Namespaces count metric
promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_namespcae_count",
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_namespace_count",
Help: "The total number of namespaces on this instance",
}, func() float64 {
count, _ := GetCount(NamespaceCountKey)
return float64(count)
})
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", NamespaceCountKey, err)
}
// Register total Tasks count metric
promauto.NewGaugeFunc(prometheus.GaugeOpts{
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_task_count",
Help: "The total number of tasks on this instance",
}, func() float64 {
count, _ := GetCount(TaskCountKey)
return float64(count)
})
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", TaskCountKey, err)
}
// Register total user count metric
promauto.NewGaugeFunc(prometheus.GaugeOpts{
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_team_count",
Help: "The total number of teams on this instance",
}, func() float64 {
count, _ := GetCount(TeamCountKey)
return float64(count)
})
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", TeamCountKey, err)
}
}
// GetCount returns the current count from redis
@ -113,22 +141,3 @@ func GetCount(key string) (count int64, err error) {
func SetCount(count int64, key string) error {
return keyvalue.Put(key, count)
}
// UpdateCount updates a count with a given amount
func UpdateCount(update int64, key string) {
if !config.ServiceEnableMetrics.GetBool() {
return
}
if update > 0 {
err := keyvalue.IncrBy(key, update)
if err != nil {
log.Error(err.Error())
}
}
if update < 0 {
err := keyvalue.DecrBy(key, update)
if err != nil {
log.Error(err.Error())
}
}
}

View File

@ -78,14 +78,14 @@ func (bt *BulkTask) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/bulk [post]
func (bt *BulkTask) Update(s *xorm.Session) (err error) {
func (bt *BulkTask) Update(s *xorm.Session, a web.Auth) (err error) {
for _, oldtask := range bt.Tasks {
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
updateDone(oldtask, &bt.Task)
// Update the assignees
if err := oldtask.updateTaskAssignees(s, bt.Assignees); err != nil {
if err := oldtask.updateTaskAssignees(s, bt.Assignees, a); err != nil {
return err
}

View File

@ -84,7 +84,7 @@ func TestBulkTask_Update(t *testing.T) {
if !allowed != tt.wantForbidden {
t.Errorf("BulkTask.Update() want forbidden, got %v, want %v", allowed, tt.wantForbidden)
}
if err := bt.Update(s); (err != nil) != tt.wantErr {
if err := bt.Update(s, tt.fields.User); (err != nil) != tt.wantErr {
t.Errorf("BulkTask.Update() error = %v, wantErr %v", err, tt.wantErr)
}

247
pkg/models/events.go Normal file
View File

@ -0,0 +1,247 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
)
/////////////////
// Task Events //
/////////////////
// TaskCreatedEvent represents an event where a task has been created
type TaskCreatedEvent struct {
Task *Task
Doer web.Auth
}
// Name defines the name for TaskCreatedEvent
func (t *TaskCreatedEvent) Name() string {
return "task.created"
}
// TaskUpdatedEvent represents an event where a task has been updated
type TaskUpdatedEvent struct {
Task *Task
Doer web.Auth
}
// Name defines the name for TaskUpdatedEvent
func (t *TaskUpdatedEvent) Name() string {
return "task.updated"
}
// TaskDeletedEvent represents a TaskDeletedEvent event
type TaskDeletedEvent struct {
Task *Task
Doer web.Auth
}
// Name defines the name for TaskDeletedEvent
func (t *TaskDeletedEvent) Name() string {
return "task.deleted"
}
// TaskAssigneeCreatedEvent represents an event where a task has been assigned to a user
type TaskAssigneeCreatedEvent struct {
Task *Task
Assignee *user.User
Doer web.Auth
}
// Name defines the name for TaskAssigneeCreatedEvent
func (t *TaskAssigneeCreatedEvent) Name() string {
return "task.assignee.created"
}
// TaskCommentCreatedEvent represents an event where a task comment has been created
type TaskCommentCreatedEvent struct {
Task *Task
Comment *TaskComment
Doer web.Auth
}
// Name defines the name for TaskCommentCreatedEvent
func (t *TaskCommentCreatedEvent) Name() string {
return "task.comment.created"
}
//////////////////////
// Namespace Events //
//////////////////////
// NamespaceCreatedEvent represents an event where a namespace has been created
type NamespaceCreatedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// Name defines the name for NamespaceCreatedEvent
func (n *NamespaceCreatedEvent) Name() string {
return "namespace.created"
}
// NamespaceUpdatedEvent represents an event where a namespace has been updated
type NamespaceUpdatedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// Name defines the name for NamespaceUpdatedEvent
func (n *NamespaceUpdatedEvent) Name() string {
return "namespace.updated"
}
// NamespaceDeletedEvent represents a NamespaceDeletedEvent event
type NamespaceDeletedEvent struct {
Namespace *Namespace
Doer web.Auth
}
// TopicName defines the name for NamespaceDeletedEvent
func (t *NamespaceDeletedEvent) Name() string {
return "namespace.deleted"
}
/////////////////
// List Events //
/////////////////
// ListCreatedEvent represents an event where a list has been created
type ListCreatedEvent struct {
List *List
Doer web.Auth
}
// Name defines the name for ListCreatedEvent
func (l *ListCreatedEvent) Name() string {
return "list.created"
}
// ListUpdatedEvent represents an event where a list has been updated
type ListUpdatedEvent struct {
List *List
Doer web.Auth
}
// Name defines the name for ListUpdatedEvent
func (l *ListUpdatedEvent) Name() string {
return "list.updated"
}
// ListDeletedEvent represents an event where a list has been deleted
type ListDeletedEvent struct {
List *List
Doer web.Auth
}
// Name defines the name for ListDeletedEvent
func (t *ListDeletedEvent) Name() string {
return "list.deleted"
}
////////////////////
// Sharing Events //
////////////////////
// ListSharedWithUserEvent represents an event where a list has been shared with a user
type ListSharedWithUserEvent struct {
List *List
User *user.User
Doer web.Auth
}
// Name defines the name for ListSharedWithUserEvent
func (l *ListSharedWithUserEvent) Name() string {
return "list.shared.user"
}
// ListSharedWithTeamEvent represents an event where a list has been shared with a team
type ListSharedWithTeamEvent struct {
List *List
Team *Team
Doer web.Auth
}
// Name defines the name for ListSharedWithTeamEvent
func (l *ListSharedWithTeamEvent) Name() string {
return "list.shared.team"
}
// NamespaceSharedWithUserEvent represents an event where a namespace has been shared with a user
type NamespaceSharedWithUserEvent struct {
Namespace *Namespace
User *user.User
Doer web.Auth
}
// Name defines the name for NamespaceSharedWithUserEvent
func (n *NamespaceSharedWithUserEvent) Name() string {
return "namespace.shared.user"
}
// NamespaceSharedWithTeamEvent represents an event where a namespace has been shared with a team
type NamespaceSharedWithTeamEvent struct {
Namespace *Namespace
Team *Team
Doer web.Auth
}
// Name defines the name for NamespaceSharedWithTeamEvent
func (n *NamespaceSharedWithTeamEvent) Name() string {
return "namespace.shared.team"
}
/////////////////
// Team Events //
/////////////////
// TeamMemberAddedEvent defines an event where a user is added to a team
type TeamMemberAddedEvent struct {
Team *Team
Member *user.User
Doer web.Auth
}
// Name defines the name for TeamMemberAddedEvent
func (t *TeamMemberAddedEvent) Name() string {
return "team.member.added"
}
// TeamCreatedEvent represents a TeamCreatedEvent event
type TeamCreatedEvent struct {
Team *Team
Doer web.Auth
}
// Name defines the name for TeamCreatedEvent
func (t *TeamCreatedEvent) Name() string {
return "team.created"
}
// TeamDeletedEvent represents a TeamDeletedEvent event
type TeamDeletedEvent struct {
Team *Team
Doer web.Auth
}
// Name defines the name for TeamDeletedEvent
func (t *TeamDeletedEvent) Name() string {
return "team.deleted"
}

View File

@ -190,7 +190,7 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/buckets/{bucketID} [post]
func (b *Bucket) Update(s *xorm.Session) (err error) {
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
_, err = s.
Where("id = ?", b.ID).
Cols("title", "limit").
@ -211,7 +211,7 @@ func (b *Bucket) Update(s *xorm.Session) (err error) {
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/buckets/{bucketID} [delete]
func (b *Bucket) Delete(s *xorm.Session) (err error) {
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
// Prevent removing the last bucket
total, err := s.Where("list_id = ?", b.ListID).Count(&Bucket{})

View File

@ -92,6 +92,8 @@ func TestBucket_ReadAll(t *testing.T) {
}
func TestBucket_Delete(t *testing.T) {
user := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -101,7 +103,7 @@ func TestBucket_Delete(t *testing.T) {
ID: 2, // The second bucket only has 3 tasks
ListID: 1,
}
err := b.Delete(s)
err := b.Delete(s, user)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -125,7 +127,7 @@ func TestBucket_Delete(t *testing.T) {
ID: 34,
ListID: 18,
}
err := b.Delete(s)
err := b.Delete(s, user)
assert.Error(t, err)
assert.True(t, IsErrCannotRemoveLastBucket(err))
err = s.Commit()
@ -141,7 +143,7 @@ func TestBucket_Delete(t *testing.T) {
func TestBucket_Update(t *testing.T) {
testAndAssertBucketUpdate := func(t *testing.T, b *Bucket, s *xorm.Session) {
err := b.Update(s)
err := b.Update(s, &user.User{ID: 1})
assert.NoError(t, err)
err = s.Commit()

View File

@ -93,7 +93,7 @@ func (l *Label) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [put]
func (l *Label) Update(s *xorm.Session) (err error) {
func (l *Label) Update(s *xorm.Session, a web.Auth) (err error) {
_, err = s.
ID(l.ID).
Cols(
@ -106,7 +106,7 @@ func (l *Label) Update(s *xorm.Session) (err error) {
return
}
err = l.ReadOne(s)
err = l.ReadOne(s, a)
return
}
@ -123,7 +123,7 @@ func (l *Label) Update(s *xorm.Session) (err error) {
// @Failure 404 {object} web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [delete]
func (l *Label) Delete(s *xorm.Session) (err error) {
func (l *Label) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.ID(l.ID).Delete(&Label{})
return err
}
@ -178,7 +178,7 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
// @Failure 404 {object} web.HTTPError "Label not found"
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [get]
func (l *Label) ReadOne(s *xorm.Session) (err error) {
func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
label, err := getLabelByIDSimple(s, l.ID)
if err != nil {
return err

View File

@ -61,7 +61,7 @@ func (LabelTask) TableName() string {
// @Failure 404 {object} web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels/{label} [delete]
func (lt *LabelTask) Delete(s *xorm.Session) (err error) {
func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Delete(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
return err
}
@ -208,6 +208,10 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
return nil, 0, 0, err
}
if len(labels) == 0 {
return nil, 0, 0, nil
}
// Get all created by users
var userids []int64
for _, l := range labels {

View File

@ -318,7 +318,7 @@ func TestLabelTask_Delete(t *testing.T) {
if !allowed && !tt.wantForbidden {
t.Errorf("LabelTask.CanDelete() forbidden, want %v", tt.wantForbidden)
}
err := l.Delete(s)
err := l.Delete(s, tt.auth)
if (err != nil) != tt.wantErr {
t.Errorf("LabelTask.Delete() error = %v, wantErr %v", err, tt.wantErr)
}

View File

@ -257,7 +257,7 @@ func TestLabel_ReadOne(t *testing.T) {
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanRead() forbidden, want %v", tt.wantForbidden)
}
err := l.ReadOne(s)
err := l.ReadOne(s, tt.auth)
if (err != nil) != tt.wantErr {
t.Errorf("Label.ReadOne() error = %v, wantErr %v", err, tt.wantErr)
}
@ -419,7 +419,7 @@ func TestLabel_Update(t *testing.T) {
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanUpdate() forbidden, want %v", tt.wantForbidden)
}
if err := l.Update(s); (err != nil) != tt.wantErr {
if err := l.Update(s, tt.auth); (err != nil) != tt.wantErr {
t.Errorf("Label.Update() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !tt.wantForbidden {
@ -505,7 +505,7 @@ func TestLabel_Delete(t *testing.T) {
if !allowed && !tt.wantForbidden {
t.Errorf("Label.CanDelete() forbidden, want %v", tt.wantForbidden)
}
if err := l.Delete(s); (err != nil) != tt.wantErr {
if err := l.Delete(s, tt.auth); (err != nil) != tt.wantErr {
t.Errorf("Label.Delete() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !tt.wantForbidden {

View File

@ -127,7 +127,7 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares/{share} [get]
func (share *LinkSharing) ReadOne(s *xorm.Session) (err error) {
func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Where("id = ?", share.ID).Get(share)
if err != nil {
return err
@ -216,7 +216,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// @Failure 404 {object} web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares/{share} [delete]
func (share *LinkSharing) Delete(s *xorm.Session) (err error) {
func (share *LinkSharing) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Where("id = ?", share.ID).Delete(share)
return
}

View File

@ -21,10 +21,11 @@ import (
"strings"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
@ -186,7 +187,7 @@ func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [get]
func (l *List) ReadOne(s *xorm.Session) (err error) {
func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if l.ID == FavoritesPseudoList.ID {
// Already "built" the list in CanRead
@ -388,6 +389,10 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu
// addListDetails adds owner user objects and list tasks to all lists in the slice
func addListDetails(s *xorm.Session, lists []*List) (err error) {
if len(lists) == 0 {
return
}
var ownerIDs []int64
for _, l := range lists {
ownerIDs = append(ownerIDs, l.OwnerID)
@ -411,6 +416,10 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) {
fileIDs = append(fileIDs, l.BackgroundFileID)
}
if len(fileIDs) == 0 {
return
}
// Unsplash background file info
us := []*UnsplashPhoto{}
err = s.In("file_id", fileIDs).Find(&us)
@ -466,7 +475,7 @@ func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
}
// CreateOrUpdateList updates a list or creates it if it doesn't exist
func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
// Check if the namespace exists
if list.NamespaceID != 0 && list.NamespaceID != FavoritesPseudoNamespace.ID {
@ -492,7 +501,6 @@ func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
if list.ID == 0 {
_, err = s.Insert(list)
metrics.UpdateCount(1, metrics.ListCountKey)
} else {
// We need to specify the cols we want to update here to be able to un-archive lists
colsToUpdate := []string{
@ -522,7 +530,7 @@ func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
}
*list = *l
err = list.ReadOne(s)
err = list.ReadOne(s, auth)
return
}
@ -541,8 +549,16 @@ func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [post]
func (l *List) Update(s *xorm.Session) (err error) {
return CreateOrUpdateList(s, l)
func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
err = CreateOrUpdateList(s, l, a)
if err != nil {
return err
}
return events.Dispatch(&ListUpdatedEvent{
List: l,
Doer: a,
})
}
func updateListLastUpdated(s *xorm.Session, list *List) error {
@ -589,7 +605,7 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
l.Owner = doer
l.ID = 0 // Otherwise only the first time a new list would be created
err = CreateOrUpdateList(s, l)
err = CreateOrUpdateList(s, l, a)
if err != nil {
return
}
@ -599,7 +615,15 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
ListID: l.ID,
Title: "New Bucket",
}
return b.Create(s, a)
err = b.Create(s, a)
if err != nil {
return
}
return events.Dispatch(&ListCreatedEvent{
List: l,
Doer: a,
})
}
// Delete implements the delete method of CRUDable
@ -614,18 +638,24 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [delete]
func (l *List) Delete(s *xorm.Session) (err error) {
func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
// Delete the list
_, err = s.ID(l.ID).Delete(&List{})
if err != nil {
return
}
metrics.UpdateCount(-1, metrics.ListCountKey)
// Delete all todotasks on that list
// Delete all tasks on that list
_, err = s.Where("list_id = ?", l.ID).Delete(&Task{})
return
if err != nil {
return
}
return events.Dispatch(&ListDeletedEvent{
List: l,
Doer: a,
})
}
// SetListBackground sets a background file as list background in the db

View File

@ -67,15 +67,15 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/duplicate [put]
//nolint:gocyclo
func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
log.Debugf("Duplicating list %d", ld.ListID)
ld.List.ID = 0
ld.List.Identifier = "" // Reset the identifier to trigger regenerating a new one
// Set the owner to the current user
ld.List.OwnerID = a.GetID()
if err := CreateOrUpdateList(s, ld.List); err != nil {
ld.List.OwnerID = doer.GetID()
if err := CreateOrUpdateList(s, ld.List, doer); err != nil {
// If there is no available unique list identifier, just reset it.
if IsErrListIdentifierIsNotUnique(err) {
ld.List.Identifier = ""
@ -99,7 +99,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
oldID := b.ID
b.ID = 0
b.ListID = ld.List.ID
if err := b.Create(s, a); err != nil {
if err := b.Create(s, doer); err != nil {
return err
}
bucketMap[oldID] = b.ID
@ -108,7 +108,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID)
// Get all tasks + all task details
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, a, &taskOptions{})
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, doer, &taskOptions{})
if err != nil {
return err
}
@ -124,7 +124,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
t.ListID = ld.List.ID
t.BucketID = bucketMap[t.BucketID]
t.UID = ""
err := createTask(s, t, a, false)
err := createTask(s, t, doer, false)
if err != nil {
return err
}
@ -163,7 +163,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
return err
}
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, a)
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer)
if err != nil {
return err
}
@ -206,7 +206,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
ID: taskMap[a.TaskID],
ListID: ld.List.ID,
}
if err := t.addNewAssigneeByID(s, a.UserID, ld.List); err != nil {
if err := t.addNewAssigneeByID(s, a.UserID, ld.List, doer); err != nil {
if IsErrUserDoesNotHaveAccessToList(err) {
continue
}
@ -269,7 +269,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
}
defer f.File.Close()
file, err := files.Create(f.File, f.Name, f.Size, a)
file, err := files.Create(f.File, f.Name, f.Size, doer)
if err != nil {
return err
}

View File

@ -19,6 +19,8 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/web"
"xorm.io/xorm"
)
@ -77,9 +79,9 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
}
// Check if the team exists
_, err = GetTeamByID(s, tl.TeamID)
team, err := GetTeamByID(s, tl.TeamID)
if err != nil {
return
return err
}
// Check if the list exists
@ -105,6 +107,15 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
return err
}
err = events.Dispatch(&ListSharedWithTeamEvent{
List: l,
Team: team,
Doer: a,
})
if err != nil {
return err
}
err = updateListLastUpdated(s, l)
return
}
@ -122,7 +133,7 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "Team or list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/teams/{teamID} [delete]
func (tl *TeamList) Delete(s *xorm.Session) (err error) {
func (tl *TeamList) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the team exists
_, err = GetTeamByID(s, tl.TeamID)
@ -234,7 +245,7 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
// @Failure 404 {object} web.HTTPError "Team or list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/teams/{teamID} [post]
func (tl *TeamList) Update(s *xorm.Session) (err error) {
func (tl *TeamList) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := tl.Right.isValid(); err != nil {

View File

@ -158,6 +158,8 @@ func TestTeamList_Create(t *testing.T) {
}
func TestTeamList_Delete(t *testing.T) {
user := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -165,7 +167,7 @@ func TestTeamList_Delete(t *testing.T) {
TeamID: 1,
ListID: 3,
}
err := tl.Delete(s)
err := tl.Delete(s, user)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -181,7 +183,7 @@ func TestTeamList_Delete(t *testing.T) {
TeamID: 9999,
ListID: 1,
}
err := tl.Delete(s)
err := tl.Delete(s, user)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
_ = s.Close()
@ -193,7 +195,7 @@ func TestTeamList_Delete(t *testing.T) {
TeamID: 1,
ListID: 9999,
}
err := tl.Delete(s)
err := tl.Delete(s, user)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotHaveAccessToList(err))
_ = s.Close()
@ -267,7 +269,7 @@ func TestTeamList_Update(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := tl.Update(s)
err := tl.Update(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("TeamList.Update() error = %v, wantErr %v", err, tt.wantErr)
}

View File

@ -125,7 +125,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
NamespaceID: 1,
}
list.Description = "Lorem Ipsum dolor sit amet."
err := list.Update(s)
err := list.Update(s, usr)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -143,7 +143,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
ID: 99999999,
Title: "test",
}
err := list.Update(s)
err := list.Update(s, usr)
assert.Error(t, err)
assert.True(t, IsErrListDoesNotExist(err))
_ = s.Close()
@ -172,7 +172,7 @@ func TestList_Delete(t *testing.T) {
list := List{
ID: 1,
}
err := list.Delete(s)
err := list.Delete(s, &user.User{ID: 1})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)

View File

@ -19,6 +19,8 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
@ -112,6 +114,15 @@ func (lu *ListUser) Create(s *xorm.Session, a web.Auth) (err error) {
return err
}
err = events.Dispatch(&ListSharedWithUserEvent{
List: l,
User: u,
Doer: a,
})
if err != nil {
return err
}
err = updateListLastUpdated(s, l)
return
}
@ -129,7 +140,7 @@ func (lu *ListUser) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "user or list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/users/{userID} [delete]
func (lu *ListUser) Delete(s *xorm.Session) (err error) {
func (lu *ListUser) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the user exists
u, err := user.GetUserByUsername(s, lu.Username)
@ -231,7 +242,7 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
// @Failure 404 {object} web.HTTPError "User or list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{listID}/users/{userID} [post]
func (lu *ListUser) Update(s *xorm.Session) (err error) {
func (lu *ListUser) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := lu.Right.isValid(); err != nil {

View File

@ -311,7 +311,7 @@ func TestListUser_Update(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := lu.Update(s)
err := lu.Update(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("ListUser.Update() error = %v, wantErr %v", err, tt.wantErr)
}
@ -393,7 +393,7 @@ func TestListUser_Delete(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := lu.Delete(s)
err := lu.Delete(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("ListUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
}

154
pkg/models/listeners.go Normal file
View File

@ -0,0 +1,154 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/modules/keyvalue"
"github.com/ThreeDotsLabs/watermill/message"
)
// RegisterListeners registers all event listeners
func RegisterListeners() {
events.RegisterListener((&ListCreatedEvent{}).Name(), &IncreaseListCounter{})
events.RegisterListener((&ListDeletedEvent{}).Name(), &DecreaseListCounter{})
events.RegisterListener((&NamespaceCreatedEvent{}).Name(), &IncreaseNamespaceCounter{})
events.RegisterListener((&NamespaceDeletedEvent{}).Name(), &DecreaseNamespaceCounter{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
events.RegisterListener((&TeamCreatedEvent{}).Name(), &IncreaseTeamCounter{})
}
//////
// Task Events
// IncreaseTaskCounter represents a listener
type IncreaseTaskCounter struct {
}
// Name defines the name for the IncreaseTaskCounter listener
func (s *IncreaseTaskCounter) Name() string {
return "task.counter.increase"
}
// Hanlde is executed when the event IncreaseTaskCounter listens on is fired
func (s *IncreaseTaskCounter) Handle(payload message.Payload) (err error) {
return keyvalue.IncrBy(metrics.TaskCountKey, 1)
}
// DecreaseTaskCounter represents a listener
type DecreaseTaskCounter struct {
}
// Name defines the name for the DecreaseTaskCounter listener
func (s *DecreaseTaskCounter) Name() string {
return "task.counter.decrease"
}
// Hanlde is executed when the event DecreaseTaskCounter listens on is fired
func (s *DecreaseTaskCounter) Handle(payload message.Payload) (err error) {
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
}
///////
// List Event Listeners
type IncreaseListCounter struct {
}
func (s *IncreaseListCounter) Name() string {
return "list.counter.increase"
}
func (s *IncreaseListCounter) Handle(payload message.Payload) (err error) {
return keyvalue.IncrBy(metrics.ListCountKey, 1)
}
type DecreaseListCounter struct {
}
func (s *DecreaseListCounter) Name() string {
return "list.counter.decrease"
}
func (s *DecreaseListCounter) Handle(payload message.Payload) (err error) {
return keyvalue.DecrBy(metrics.ListCountKey, 1)
}
//////
// Namespace events
// IncreaseNamespaceCounter represents a listener
type IncreaseNamespaceCounter struct {
}
// Name defines the name for the IncreaseNamespaceCounter listener
func (s *IncreaseNamespaceCounter) Name() string {
return "namespace.counter.increase"
}
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
func (s *IncreaseNamespaceCounter) Handle(payload message.Payload) (err error) {
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
}
// DecreaseNamespaceCounter represents a listener
type DecreaseNamespaceCounter struct {
}
// Name defines the name for the DecreaseNamespaceCounter listener
func (s *DecreaseNamespaceCounter) Name() string {
return "namespace.counter.decrease"
}
// Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired
func (s *DecreaseNamespaceCounter) Handle(payload message.Payload) (err error) {
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
}
///////
// Team Events
// IncreaseTeamCounter represents a listener
type IncreaseTeamCounter struct {
}
// Name defines the name for the IncreaseTeamCounter listener
func (s *IncreaseTeamCounter) Name() string {
return "team.counter.increase"
}
// Hanlde is executed when the event IncreaseTeamCounter listens on is fired
func (s *IncreaseTeamCounter) Handle(payload message.Payload) (err error) {
return keyvalue.IncrBy(metrics.TeamCountKey, 1)
}
// DecreaseTeamCounter represents a listener
type DecreaseTeamCounter struct {
}
// Name defines the name for the DecreaseTeamCounter listener
func (s *DecreaseTeamCounter) Name() string {
return "team.counter.decrease"
}
// Hanlde is executed when the event DecreaseTeamCounter listens on is fired
func (s *DecreaseTeamCounter) Handle(payload message.Payload) (err error) {
return keyvalue.DecrBy(metrics.TeamCountKey, 1)
}

View File

@ -22,6 +22,8 @@ import (
"testing"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/user"
@ -64,5 +66,7 @@ func TestMain(m *testing.M) {
SetupTests()
events.Fake()
os.Exit(m.Run())
}

View File

@ -22,8 +22,9 @@ import (
"strings"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
@ -159,7 +160,7 @@ func (n *Namespace) CheckIsArchived(s *xorm.Session) error {
// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id} [get]
func (n *Namespace) ReadOne(s *xorm.Session) (err error) {
func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
nn, err := GetNamespaceByID(s, n.ID)
if err != nil {
return err
@ -478,7 +479,14 @@ func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
return err
}
metrics.UpdateCount(1, metrics.NamespaceCountKey)
err = events.Dispatch(&NamespaceCreatedEvent{
Namespace: n,
Doer: a,
})
if err != nil {
return err
}
return
}
@ -504,7 +512,7 @@ func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{id} [delete]
func (n *Namespace) Delete(s *xorm.Session) (err error) {
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the namespace exists
_, err = GetNamespaceByID(s, n.ID)
@ -523,6 +531,14 @@ func (n *Namespace) Delete(s *xorm.Session) (err error) {
if err != nil {
return
}
if len(lists) == 0 {
return events.Dispatch(&NamespaceDeletedEvent{
Namespace: n,
Doer: a,
})
}
var listIDs []int64
// We need to do that for here because we need the list ids to delete two times:
// 1) to delete the lists itself
@ -543,9 +559,10 @@ func (n *Namespace) Delete(s *xorm.Session) (err error) {
return
}
metrics.UpdateCount(-1, metrics.NamespaceCountKey)
return
return events.Dispatch(&NamespaceDeletedEvent{
Namespace: n,
Doer: a,
})
}
// Update implements the update method via the interface
@ -562,7 +579,7 @@ func (n *Namespace) Delete(s *xorm.Session) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespace/{id} [post]
func (n *Namespace) Update(s *xorm.Session) (err error) {
func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if we have at least a name
if n.Title == "" {
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
@ -605,5 +622,12 @@ func (n *Namespace) Update(s *xorm.Session) (err error) {
ID(currentNamespace.ID).
Cols(colsToUpdate...).
Update(n)
return
if err != nil {
return err
}
return events.Dispatch(&NamespaceUpdatedEvent{
Namespace: n,
Doer: a,
})
}

View File

@ -19,6 +19,8 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/web"
"xorm.io/xorm"
)
@ -71,15 +73,15 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
}
// Check if the team exists
_, err = GetTeamByID(s, tn.TeamID)
team, err := GetTeamByID(s, tn.TeamID)
if err != nil {
return
return err
}
// Check if the namespace exists
_, err = GetNamespaceByID(s, tn.NamespaceID)
namespace, err := GetNamespaceByID(s, tn.NamespaceID)
if err != nil {
return
return err
}
// Check if the team already has access to the namespace
@ -96,7 +98,15 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
// Insert the new team
_, err = s.Insert(tn)
return
if err != nil {
return err
}
return events.Dispatch(&NamespaceSharedWithTeamEvent{
Namespace: namespace,
Team: team,
Doer: a,
})
}
// Delete deletes a team <-> namespace relation based on the namespace & team id
@ -112,7 +122,7 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
func (tn *TeamNamespace) Delete(s *xorm.Session) (err error) {
func (tn *TeamNamespace) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the team exists
_, err = GetTeamByID(s, tn.TeamID)
@ -219,7 +229,7 @@ func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, pag
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
func (tn *TeamNamespace) Update(s *xorm.Session) (err error) {
func (tn *TeamNamespace) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := tn.Right.isValid(); err != nil {

View File

@ -157,7 +157,7 @@ func TestTeamNamespace_Delete(t *testing.T) {
s := db.NewSession()
allowed, _ := tn.CanDelete(s, u)
assert.True(t, allowed)
err := tn.Delete(s)
err := tn.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -174,7 +174,7 @@ func TestTeamNamespace_Delete(t *testing.T) {
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Delete(s)
err := tn.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
_ = s.Close()
@ -186,7 +186,7 @@ func TestTeamNamespace_Delete(t *testing.T) {
}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := tn.Delete(s)
err := tn.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
_ = s.Close()
@ -260,7 +260,7 @@ func TestTeamNamespace_Update(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := tl.Update(s)
err := tl.Update(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr)
}

View File

@ -69,11 +69,13 @@ func TestNamespace_Create(t *testing.T) {
}
func TestNamespace_ReadOne(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
n := &Namespace{ID: 1}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := n.ReadOne(s)
err := n.ReadOne(s, u)
assert.NoError(t, err)
assert.Equal(t, n.Title, "testnamespace")
_ = s.Close()
@ -82,7 +84,7 @@ func TestNamespace_ReadOne(t *testing.T) {
n := &Namespace{ID: 99999}
db.LoadAndAssertFixtures(t)
s := db.NewSession()
err := n.ReadOne(s)
err := n.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()
@ -90,6 +92,8 @@ func TestNamespace_ReadOne(t *testing.T) {
}
func TestNamespace_Update(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -97,7 +101,7 @@ func TestNamespace_Update(t *testing.T) {
ID: 1,
Title: "Lorem Ipsum",
}
err := n.Update(s)
err := n.Update(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -114,7 +118,7 @@ func TestNamespace_Update(t *testing.T) {
ID: 99999,
Title: "Lorem Ipsum",
}
err := n.Update(s)
err := n.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()
@ -127,7 +131,7 @@ func TestNamespace_Update(t *testing.T) {
Title: "Lorem Ipsum",
Owner: &user.User{ID: 99999},
}
err := n.Update(s)
err := n.Update(s, u)
assert.Error(t, err)
assert.True(t, user.IsErrUserDoesNotExist(err))
_ = s.Close()
@ -138,7 +142,7 @@ func TestNamespace_Update(t *testing.T) {
n := &Namespace{
ID: 1,
}
err := n.Update(s)
err := n.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
_ = s.Close()
@ -146,13 +150,15 @@ func TestNamespace_Update(t *testing.T) {
}
func TestNamespace_Delete(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
n := &Namespace{
ID: 1,
}
err := n.Delete(s)
err := n.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -167,7 +173,7 @@ func TestNamespace_Delete(t *testing.T) {
n := &Namespace{
ID: 9999,
}
err := n.Delete(s)
err := n.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrNamespaceDoesNotExist(err))
_ = s.Close()

View File

@ -19,6 +19,8 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
@ -75,7 +77,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
}
// Check if the namespace exists
l, err := GetNamespaceByID(s, nu.NamespaceID)
n, err := GetNamespaceByID(s, nu.NamespaceID)
if err != nil {
return
}
@ -89,7 +91,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
// Check if the user already has access or is owner of that namespace
// We explicitly DO NOT check for teams here
if l.OwnerID == nu.UserID {
if n.OwnerID == nu.UserID {
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
}
@ -105,8 +107,15 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
// Insert user <-> namespace relation
_, err = s.Insert(nu)
if err != nil {
return err
}
return
return events.Dispatch(&NamespaceSharedWithUserEvent{
Namespace: n,
User: user,
Doer: a,
})
}
// Delete deletes a namespace <-> user relation
@ -122,7 +131,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
func (nu *NamespaceUser) Delete(s *xorm.Session) (err error) {
func (nu *NamespaceUser) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the user exists
user, err := user2.GetUserByUsername(s, nu.Username)
@ -220,7 +229,7 @@ func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, pag
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/users/{userID} [post]
func (nu *NamespaceUser) Update(s *xorm.Session) (err error) {
func (nu *NamespaceUser) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := nu.Right.isValid(); err != nil {

View File

@ -315,7 +315,7 @@ func TestNamespaceUser_Update(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := nu.Update(s)
err := nu.Update(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr)
}
@ -396,7 +396,7 @@ func TestNamespaceUser_Delete(t *testing.T) {
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
err := nu.Delete(s)
err := nu.Delete(s, &user.User{ID: 1})
if (err != nil) != tt.wantErr {
t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
}

View File

@ -133,7 +133,7 @@ func getSavedFilterSimpleByID(s *xorm.Session, id int64) (sf *SavedFilter, err e
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
// @Failure 500 {object} models.Message "Internal error"
// @Router /filters/{id} [get]
func (sf *SavedFilter) ReadOne(s *xorm.Session) error {
func (sf *SavedFilter) ReadOne(s *xorm.Session, a web.Auth) error {
// s already contains almost the full saved filter from the rights check, we only need to add the user
u, err := user.GetUserByID(s, sf.OwnerID)
sf.Owner = u
@ -153,7 +153,7 @@ func (sf *SavedFilter) ReadOne(s *xorm.Session) error {
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /filters/{id} [post]
func (sf *SavedFilter) Update(s *xorm.Session) error {
func (sf *SavedFilter) Update(s *xorm.Session, a web.Auth) error {
_, err := s.
Where("id = ?", sf.ID).
Cols(
@ -178,7 +178,7 @@ func (sf *SavedFilter) Update(s *xorm.Session) error {
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /filters/{id} [delete]
func (sf *SavedFilter) Delete(s *xorm.Session) error {
func (sf *SavedFilter) Delete(s *xorm.Session, a web.Auth) error {
_, err := s.
Where("id = ?", sf.ID).
Delete(sf)

View File

@ -86,7 +86,7 @@ func TestSavedFilter_ReadOne(t *testing.T) {
// canRead pre-populates the struct
_, _, err := sf.CanRead(s, user1)
assert.NoError(t, err)
err = sf.ReadOne(s)
err = sf.ReadOne(s, user1)
assert.NoError(t, err)
assert.NotNil(t, sf.Owner)
}
@ -102,7 +102,7 @@ func TestSavedFilter_Update(t *testing.T) {
Description: "", // Explicitly reset the description
Filters: &TaskCollection{},
}
err := sf.Update(s)
err := sf.Update(s, &user.User{ID: 1})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -121,7 +121,7 @@ func TestSavedFilter_Delete(t *testing.T) {
sf := &SavedFilter{
ID: 1,
}
err := sf.Delete(s)
err := sf.Delete(s, &user.User{ID: 1})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)

View File

@ -19,6 +19,8 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
@ -57,7 +59,7 @@ func getRawTaskAssigneesForTasks(s *xorm.Session, taskIDs []int64) (taskAssignee
}
// Create or update a bunch of task assignees
func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User) (err error) {
func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer web.Auth) (err error) {
// Load the current assignees
currentAssignees, err := getRawTaskAssigneesForTasks(s, []int64{t.ID})
@ -132,7 +134,7 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User) (err
}
// Add the new assignee
err = t.addNewAssigneeByID(s, u.ID, list)
err = t.addNewAssigneeByID(s, u.ID, list, doer)
if err != nil {
return err
}
@ -166,7 +168,7 @@ func (t *Task) setTaskAssignees(assignees []*user.User) {
// @Failure 403 {object} web.HTTPError "Not allowed to delete the assignee."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/assignees/{userID} [delete]
func (la *TaskAssginee) Delete(s *xorm.Session) (err error) {
func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Delete(&TaskAssginee{TaskID: la.TaskID, UserID: la.UserID})
if err != nil {
return err
@ -198,10 +200,10 @@ func (la *TaskAssginee) Create(s *xorm.Session, a web.Auth) (err error) {
}
task := &Task{ID: la.TaskID}
return task.addNewAssigneeByID(s, la.UserID, list)
return task.addNewAssigneeByID(s, la.UserID, list, a)
}
func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *List) (err error) {
func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *List, auth web.Auth) (err error) {
// Check if the user exists and has access to the list
newAssignee, err := user.GetUserByID(s, newAssigneeID)
if err != nil {
@ -223,6 +225,15 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
return err
}
err = events.Dispatch(&TaskAssigneeCreatedEvent{
Task: t,
Assignee: newAssignee,
Doer: auth,
})
if err != nil {
return err
}
err = updateListLastUpdated(s, &List{ID: t.ListID})
return
}
@ -313,6 +324,6 @@ func (ba *BulkAssignees) Create(s *xorm.Session, a web.Auth) (err error) {
task.Assignees = append(task.Assignees, &a.User)
}
err = task.updateTaskAssignees(s, ba.Assignees)
err = task.updateTaskAssignees(s, ba.Assignees, a)
return
}

View File

@ -80,7 +80,7 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
}
// ReadOne returns a task attachment
func (ta *TaskAttachment) ReadOne(s *xorm.Session) (err error) {
func (ta *TaskAttachment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Where("id = ?", ta.ID).Get(ta)
if err != nil {
return
@ -176,9 +176,9 @@ func (ta *TaskAttachment) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// @Failure 404 {object} models.Message "The task does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id}/attachments/{attachmentID} [delete]
func (ta *TaskAttachment) Delete(s *xorm.Session) error {
func (ta *TaskAttachment) Delete(s *xorm.Session, a web.Auth) error {
// Load the attachment
err := ta.ReadOne(s)
err := ta.ReadOne(s, a)
if err != nil && !files.IsErrFileDoesNotExist(err) {
return err
}
@ -209,6 +209,10 @@ func getTaskAttachmentsByTaskIDs(s *xorm.Session, taskIDs []int64) (attachments
return
}
if len(attachments) == 0 {
return
}
fileIDs := []int64{}
userIDs := []int64{}
for _, a := range attachments {

View File

@ -30,6 +30,8 @@ import (
)
func TestTaskAttachment_ReadOne(t *testing.T) {
u := &user.User{ID: 1}
t.Run("Normal File", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -39,7 +41,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
ta := &TaskAttachment{
ID: 1,
}
err := ta.ReadOne(s)
err := ta.ReadOne(s, u)
assert.NoError(t, err)
assert.NotNil(t, ta.File)
assert.True(t, ta.File.ID == ta.FileID && ta.FileID != 0)
@ -63,7 +65,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
ta := &TaskAttachment{
ID: 9999,
}
err := ta.ReadOne(s)
err := ta.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskAttachmentDoesNotExist(err))
})
@ -76,7 +78,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
ta := &TaskAttachment{
ID: 2,
}
err := ta.ReadOne(s)
err := ta.ReadOne(s, u)
assert.Error(t, err)
assert.EqualError(t, err, "file 9999 does not exist")
})
@ -153,10 +155,12 @@ func TestTaskAttachment_Delete(t *testing.T) {
s := db.NewSession()
defer s.Close()
u := &user.User{ID: 1}
files.InitTestFileFixtures(t)
t.Run("Normal", func(t *testing.T) {
ta := &TaskAttachment{ID: 1}
err := ta.Delete(s)
err := ta.Delete(s, u)
assert.NoError(t, err)
// Check if the file itself was deleted
_, err = files.FileStat("/1") // The new file has the id 2 since it's the second attachment
@ -165,14 +169,14 @@ func TestTaskAttachment_Delete(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
files.InitTestFileFixtures(t)
ta := &TaskAttachment{ID: 9999}
err := ta.Delete(s)
err := ta.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskAttachmentDoesNotExist(err))
})
t.Run("Existing attachment, nonexisting file", func(t *testing.T) {
files.InitTestFileFixtures(t)
ta := &TaskAttachment{ID: 2}
err := ta.Delete(s)
err := ta.Delete(s, u)
assert.NoError(t, err)
})
}

View File

@ -19,6 +19,8 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/user"
@ -60,7 +62,7 @@ func (tc *TaskComment) TableName() string {
// @Router /tasks/{taskID}/comments [put]
func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
// Check if the task exists
_, err = GetTaskSimple(s, &Task{ID: tc.TaskID})
task, err := GetTaskSimple(s, &Task{ID: tc.TaskID})
if err != nil {
return err
}
@ -70,6 +72,16 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
if err != nil {
return
}
err = events.Dispatch(&TaskCommentCreatedEvent{
Task: &task,
Comment: tc,
Doer: a,
})
if err != nil {
return err
}
tc.Author, err = user.GetUserByID(s, a.GetID())
return
}
@ -88,7 +100,7 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [delete]
func (tc *TaskComment) Delete(s *xorm.Session) error {
func (tc *TaskComment) Delete(s *xorm.Session, a web.Auth) error {
deleted, err := s.
ID(tc.ID).
NoAutoCondition().
@ -113,7 +125,7 @@ func (tc *TaskComment) Delete(s *xorm.Session) error {
// @Failure 404 {object} web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [post]
func (tc *TaskComment) Update(s *xorm.Session) error {
func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
updated, err := s.
ID(tc.ID).
Cols("comment").
@ -138,7 +150,7 @@ func (tc *TaskComment) Update(s *xorm.Session) error {
// @Failure 404 {object} web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [get]
func (tc *TaskComment) ReadOne(s *xorm.Session) (err error) {
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Get(tc)
if err != nil {
return

View File

@ -65,13 +65,15 @@ func TestTaskComment_Create(t *testing.T) {
}
func TestTaskComment_Delete(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1}
err := tc.Delete(s)
err := tc.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -86,13 +88,15 @@ func TestTaskComment_Delete(t *testing.T) {
defer s.Close()
tc := &TaskComment{ID: 9999}
err := tc.Delete(s)
err := tc.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
}
func TestTaskComment_Update(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -102,7 +106,7 @@ func TestTaskComment_Update(t *testing.T) {
ID: 1,
Comment: "testing",
}
err := tc.Update(s)
err := tc.Update(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -120,20 +124,22 @@ func TestTaskComment_Update(t *testing.T) {
tc := &TaskComment{
ID: 9999,
}
err := tc.Update(s)
err := tc.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
}
func TestTaskComment_ReadOne(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1}
err := tc.ReadOne(s)
err := tc.ReadOne(s, u)
assert.NoError(t, err)
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment)
assert.NotEmpty(t, tc.Author.ID)
@ -144,7 +150,7 @@ func TestTaskComment_ReadOne(t *testing.T) {
defer s.Close()
tc := &TaskComment{ID: 9999}
err := tc.ReadOne(s)
err := tc.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})

View File

@ -201,7 +201,7 @@ func (rel *TaskRelation) Create(s *xorm.Session, a web.Auth) error {
// @Failure 404 {object} web.HTTPError "The task relation was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/relations [delete]
func (rel *TaskRelation) Delete(s *xorm.Session) error {
func (rel *TaskRelation) Delete(s *xorm.Session, a web.Auth) error {
// Check if the relation exists
exists, err := s.
Cols("task_id", "other_task_id", "relation_kind").

View File

@ -97,6 +97,8 @@ func TestTaskRelation_Create(t *testing.T) {
}
func TestTaskRelation_Delete(t *testing.T) {
u := &user.User{ID: 1}
t.Run("Normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -107,7 +109,7 @@ func TestTaskRelation_Delete(t *testing.T) {
OtherTaskID: 29,
RelationKind: RelationKindSubtask,
}
err := rel.Delete(s)
err := rel.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -127,7 +129,7 @@ func TestTaskRelation_Delete(t *testing.T) {
OtherTaskID: 3,
RelationKind: RelationKindSubtask,
}
err := rel.Delete(s)
err := rel.Delete(s, u)
assert.Error(t, err)
assert.True(t, IsErrRelationDoesNotExist(err))
})

View File

@ -22,10 +22,11 @@ import (
"strconv"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
@ -596,6 +597,11 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
for _, rt := range relatedTasks {
relatedTaskIDs = append(relatedTaskIDs, rt.OtherTaskID)
}
if len(relatedTaskIDs) == 0 {
return
}
fullRelatedTasks := make(map[int64]*Task)
err = s.In("id", relatedTaskIDs).Find(&fullRelatedTasks)
if err != nil {
@ -814,7 +820,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
// Update the assignees
if updateAssignees {
if err := t.updateTaskAssignees(s, t.Assignees); err != nil {
if err := t.updateTaskAssignees(s, t.Assignees, a); err != nil {
return err
}
}
@ -824,10 +830,16 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
return err
}
metrics.UpdateCount(1, metrics.TaskCountKey)
t.setIdentifier(l)
err = events.Dispatch(&TaskCreatedEvent{
Task: t,
Doer: a,
})
if err != nil {
return err
}
err = updateListLastUpdated(s, &List{ID: t.ListID})
return
}
@ -847,7 +859,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id} [post]
//nolint:gocyclo
func (t *Task) Update(s *xorm.Session) (err error) {
func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the task exists and get the old values
ot, err := GetTaskByIDSimple(s, t.ID)
@ -870,7 +882,7 @@ func (t *Task) Update(s *xorm.Session) (err error) {
updateDone(&ot, t)
// Update the assignees
if err := ot.updateTaskAssignees(s, t.Assignees); err != nil {
if err := ot.updateTaskAssignees(s, t.Assignees, a); err != nil {
return err
}
@ -1028,6 +1040,14 @@ func (t *Task) Update(s *xorm.Session) (err error) {
}
t.Updated = nt.Updated
err = events.Dispatch(&TaskUpdatedEvent{
Task: t,
Doer: a,
})
if err != nil {
return err
}
return updateListLastUpdated(s, &List{ID: t.ListID})
}
@ -1166,7 +1186,7 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id} [delete]
func (t *Task) Delete(s *xorm.Session) (err error) {
func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
if _, err = s.ID(t.ID).Delete(Task{}); err != nil {
return err
@ -1177,7 +1197,13 @@ func (t *Task) Delete(s *xorm.Session) (err error) {
return err
}
metrics.UpdateCount(-1, metrics.TaskCountKey)
err = events.Dispatch(&TaskDeletedEvent{
Task: t,
Doer: a,
})
if err != nil {
return
}
err = updateListLastUpdated(s, &List{ID: t.ListID})
return
@ -1195,7 +1221,7 @@ func (t *Task) Delete(s *xorm.Session) (err error) {
// @Failure 404 {object} models.Message "Task not found"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{ID} [get]
func (t *Task) ReadOne(s *xorm.Session) (err error) {
func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
taskMap := make(map[int64]*Task, 1)
taskMap[t.ID] = &Task{}

View File

@ -20,6 +20,8 @@ import (
"testing"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert"
@ -65,6 +67,7 @@ func TestTask_Create(t *testing.T) {
"bucket_id": 1,
}, false)
events.AssertDispatched(t, &TaskCreatedEvent{})
})
t.Run("empty title", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -127,6 +130,8 @@ func TestTask_Create(t *testing.T) {
}
func TestTask_Update(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -138,7 +143,7 @@ func TestTask_Update(t *testing.T) {
Description: "Lorem Ipsum Dolor",
ListID: 1,
}
err := task.Update(s)
err := task.Update(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -161,7 +166,7 @@ func TestTask_Update(t *testing.T) {
Description: "Lorem Ipsum Dolor",
ListID: 1,
}
err := task.Update(s)
err := task.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})
@ -177,7 +182,7 @@ func TestTask_Update(t *testing.T) {
ListID: 1,
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
}
err := task.Update(s)
err := task.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrBucketLimitExceeded(err))
})
@ -194,7 +199,7 @@ func TestTask_Update(t *testing.T) {
ListID: 1,
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
}
err := task.Update(s)
err := task.Update(s, u)
assert.NoError(t, err)
})
}
@ -208,7 +213,7 @@ func TestTask_Delete(t *testing.T) {
task := &Task{
ID: 1,
}
err := task.Delete(s)
err := task.Delete(s, &user.User{ID: 1})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -440,13 +445,15 @@ func TestUpdateDone(t *testing.T) {
}
func TestTask_ReadOne(t *testing.T) {
u := &user.User{ID: 1}
t.Run("default", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
task := &Task{ID: 1}
err := task.ReadOne(s)
err := task.ReadOne(s, u)
assert.NoError(t, err)
assert.Equal(t, "task #1", task.Title)
})
@ -456,7 +463,7 @@ func TestTask_ReadOne(t *testing.T) {
defer s.Close()
task := &Task{ID: 99999}
err := task.ReadOne(s)
err := task.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})

View File

@ -17,6 +17,7 @@
package models
import (
"code.vikunja.io/api/pkg/events"
user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
@ -39,9 +40,9 @@ import (
func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
// Check if the team extst
_, err = GetTeamByID(s, tm.TeamID)
team, err := GetTeamByID(s, tm.TeamID)
if err != nil {
return
return err
}
// Check if the user exists
@ -64,7 +65,15 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
// Insert the user
_, err = s.Insert(tm)
return
if err != nil {
return err
}
return events.Dispatch(&TeamMemberAddedEvent{
Team: team,
Member: user,
Doer: a,
})
}
// Delete deletes a user from a team
@ -78,7 +87,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
// @Success 200 {object} models.Message "The user was successfully removed from the team."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id}/members/{userID} [delete]
func (tm *TeamMember) Delete(s *xorm.Session) (err error) {
func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) {
total, err := s.Where("team_id = ?", tm.TeamID).Count(&TeamMember{})
if err != nil {
@ -110,7 +119,7 @@ func (tm *TeamMember) Delete(s *xorm.Session) (err error) {
// @Success 200 {object} models.Message "The member right was successfully changed."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id}/members/{userID}/admin [post]
func (tm *TeamMember) Update(s *xorm.Session) (err error) {
func (tm *TeamMember) Update(s *xorm.Session, a web.Auth) (err error) {
// Find the numeric user id
user, err := user2.GetUserByUsername(s, tm.Username)
if err != nil {

View File

@ -101,7 +101,7 @@ func TestTeamMember_Delete(t *testing.T) {
TeamID: 1,
Username: "user1",
}
err := tm.Delete(s)
err := tm.Delete(s, &user.User{ID: 1})
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -114,6 +114,8 @@ func TestTeamMember_Delete(t *testing.T) {
}
func TestTeamMember_Update(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -124,7 +126,7 @@ func TestTeamMember_Update(t *testing.T) {
Username: "user1",
Admin: true,
}
err := tm.Update(s)
err := tm.Update(s, u)
assert.NoError(t, err)
assert.False(t, tm.Admin) // Since this endpoint toggles the right, we should get a false for admin back.
err = s.Commit()
@ -148,7 +150,7 @@ func TestTeamMember_Update(t *testing.T) {
Username: "user1",
Admin: true,
}
err := tm.Update(s)
err := tm.Update(s, u)
assert.NoError(t, err)
assert.False(t, tm.Admin)
err = s.Commit()

View File

@ -19,9 +19,10 @@ package models
import (
"time"
"code.vikunja.io/api/pkg/events"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
@ -119,6 +120,11 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) {
}
func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
if len(teams) == 0 {
return nil
}
// Put the teams in a map to make assigning more info to it more efficient
teamMap := make(map[int64]*Team, len(teams))
var teamIDs []int64
@ -177,7 +183,7 @@ func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the team"
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id} [get]
func (t *Team) ReadOne(s *xorm.Session) (err error) {
func (t *Team) ReadOne(s *xorm.Session, a web.Auth) (err error) {
team, err := GetTeamByID(s, t.ID)
if team != nil {
*t = *team
@ -270,8 +276,10 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) {
return err
}
metrics.UpdateCount(1, metrics.TeamCountKey)
return
return events.Dispatch(&TeamCreatedEvent{
Team: t,
Doer: a,
})
}
// Delete deletes a team
@ -285,7 +293,7 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id} [delete]
func (t *Team) Delete(s *xorm.Session) (err error) {
func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
// Delete the team
_, err = s.ID(t.ID).Delete(&Team{})
@ -311,8 +319,10 @@ func (t *Team) Delete(s *xorm.Session) (err error) {
return
}
metrics.UpdateCount(-1, metrics.TeamCountKey)
return
return events.Dispatch(&TeamDeletedEvent{
Team: t,
Doer: a,
})
}
// Update is the handler to create a team
@ -328,7 +338,7 @@ func (t *Team) Delete(s *xorm.Session) (err error) {
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id} [post]
func (t *Team) Update(s *xorm.Session) (err error) {
func (t *Team) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if we have a name
if t.Name == "" {
return ErrTeamNameCannotBeEmpty{}

View File

@ -62,13 +62,15 @@ func TestTeam_Create(t *testing.T) {
}
func TestTeam_ReadOne(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
team := &Team{ID: 1}
err := team.ReadOne(s)
err := team.ReadOne(s, u)
assert.NoError(t, err)
assert.Equal(t, "testteam1", team.Name)
assert.Equal(t, "Lorem Ipsum", team.Description)
@ -81,7 +83,7 @@ func TestTeam_ReadOne(t *testing.T) {
defer s.Close()
team := &Team{ID: -1}
err := team.ReadOne(s)
err := team.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
})
@ -91,7 +93,7 @@ func TestTeam_ReadOne(t *testing.T) {
defer s.Close()
team := &Team{ID: 99999}
err := team.ReadOne(s)
err := team.ReadOne(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
})
@ -113,6 +115,8 @@ func TestTeam_ReadAll(t *testing.T) {
}
func TestTeam_Update(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -122,7 +126,7 @@ func TestTeam_Update(t *testing.T) {
ID: 1,
Name: "SomethingNew",
}
err := team.Update(s)
err := team.Update(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)
@ -140,7 +144,7 @@ func TestTeam_Update(t *testing.T) {
ID: 1,
Name: "",
}
err := team.Update(s)
err := team.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
})
@ -153,13 +157,15 @@ func TestTeam_Update(t *testing.T) {
ID: 9999,
Name: "SomethingNew",
}
err := team.Update(s)
err := team.Update(s, u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
})
}
func TestTeam_Delete(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -168,7 +174,7 @@ func TestTeam_Delete(t *testing.T) {
team := &Team{
ID: 1,
}
err := team.Delete(s)
err := team.Delete(s, u)
assert.NoError(t, err)
err = s.Commit()
assert.NoError(t, err)

View File

@ -20,6 +20,8 @@ import (
"os"
"testing"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
@ -30,5 +32,6 @@ func TestMain(m *testing.M) {
user.InitTests()
files.InitTests()
models.SetupTests()
events.Fake()
os.Exit(m.Run())
}

View File

@ -226,7 +226,7 @@ func InsertFromStructure(str []*models.NamespaceWithLists, user *user.User) (err
return err
}
buckets := bucketsIn.([]*models.Bucket)
err = buckets[0].Delete(s)
err = buckets[0].Delete(s, user)
if err != nil {
_ = s.Rollback()
return err

View File

@ -20,6 +20,8 @@ import (
"os"
"testing"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
@ -37,5 +39,6 @@ func TestMain(m *testing.M) {
files.InitTests()
user.InitTests()
models.SetupTests()
events.Fake()
os.Exit(m.Run())
}

View File

@ -104,7 +104,7 @@ func RenewToken(c echo.Context) (err error) {
if typ == auth.AuthTypeLinkShare {
share := &models.LinkSharing{}
share.ID = int64(claims["id"].(float64))
err := share.ReadOne(s)
err := share.ReadOne(s, share)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)

View File

@ -147,7 +147,7 @@ func GetTaskAttachment(c echo.Context) error {
}
// Get the attachment incl file
err = taskAttachment.ReadOne(s)
err = taskAttachment.ReadOne(s, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)

View File

@ -318,7 +318,7 @@ func (vcls *VikunjaCaldavListStorage) UpdateResource(rpath, content string) (*da
}
// Update the task
err = vTask.Update(s)
err = vTask.Update(s, vcls.user)
if err != nil {
_ = s.Rollback()
return nil, err
@ -354,7 +354,7 @@ func (vcls *VikunjaCaldavListStorage) DeleteResource(rpath string) error {
}
// Delete it
err = vcls.task.Delete(s)
err = vcls.task.Delete(s, vcls.user)
if err != nil {
_ = s.Rollback()
return err
@ -458,7 +458,7 @@ func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr Vi
log.Errorf("User %v tried to access a caldav resource (List %v) which they are not allowed to access", vcls.user.Username, vcls.list.ID)
return rr, models.ErrUserDoesNotHaveAccessToList{ListID: vcls.list.ID}
}
err = vcls.list.ReadOne(s)
err = vcls.list.ReadOne(s, vcls.user)
if err != nil {
_ = s.Rollback()
return

View File

@ -71,7 +71,7 @@ func setupMetrics(a *echo.Group) {
}
}
a.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
a.GET("/metrics", echo.WrapHandler(promhttp.HandlerFor(metrics.GetRegistry(), promhttp.HandlerOpts{})))
}
func setupMetricsMiddleware(a *echo.Group) {

27
pkg/user/events.go Normal file
View File

@ -0,0 +1,27 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package user
// CreatedEvent represents a CreatedEvent event
type CreatedEvent struct {
User *User
}
// TopicName defines the name for CreatedEvent
func (t *CreatedEvent) Name() string {
return "user.created"
}

45
pkg/user/listeners.go Normal file
View File

@ -0,0 +1,45 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package user
import (
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/modules/keyvalue"
"github.com/ThreeDotsLabs/watermill/message"
)
func RegisterListeners() {
events.RegisterListener((&CreatedEvent{}).Name(), &IncreaseUserCounter{})
}
///////
// User Events
// IncreaseUserCounter represents a listener
type IncreaseUserCounter struct {
}
// Name defines the name for the IncreaseUserCounter listener
func (s *IncreaseUserCounter) Name() string {
return "increase.user.counter"
}
// Hanlde is executed when the event IncreaseUserCounter listens on is fired
func (s *IncreaseUserCounter) Handle(payload message.Payload) (err error) {
return keyvalue.IncrBy(metrics.UserCountKey, 1)
}

View File

@ -18,6 +18,7 @@ package user
import (
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
)
@ -37,4 +38,6 @@ func InitTests() {
if err != nil {
log.Fatal(err)
}
events.Fake()
}

View File

@ -18,8 +18,8 @@ package user
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/utils"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
@ -70,15 +70,19 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
return nil, err
}
// Update the metrics
metrics.UpdateCount(1, metrics.ActiveUsersKey)
// Get the full new User
newUserOut, err := GetUserByID(s, user.ID)
if err != nil {
return nil, err
}
err = events.Dispatch(&CreatedEvent{
User: newUserOut,
})
if err != nil {
return nil, err
}
sendConfirmEmail(user)
return newUserOut, err