snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
469
server/system/service/notification.go
Normal file
469
server/system/service/notification.go
Normal file
@@ -0,0 +1,469 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
notification struct {
|
||||
ac notificationAccessController
|
||||
log *zap.Logger
|
||||
actionlog actionlog.Recorder
|
||||
store store.Storer
|
||||
notificationSender notificationSender
|
||||
}
|
||||
|
||||
notificationSender interface {
|
||||
Send(kind string, payload interface{}, userIDs ...uint64) error
|
||||
}
|
||||
|
||||
notificationAccessController interface {
|
||||
CanAssignNotification(ctx context.Context) bool
|
||||
}
|
||||
|
||||
NotificationService interface {
|
||||
Find(context.Context, types.NotificationFilter) (types.NotificationSet, types.NotificationFilter, error)
|
||||
FindByID(context.Context, uint64) (*types.Notification, error)
|
||||
|
||||
Create(context.Context, *types.Notification) (*types.Notification, error)
|
||||
Update(context.Context, *types.Notification) (*types.Notification, error)
|
||||
Delete(context.Context, uint64) error
|
||||
|
||||
MarkAsRead(context.Context, uint64) error
|
||||
MarkAsUnread(context.Context, uint64) error
|
||||
MarkAllAsRead(context.Context) error
|
||||
MarkAllAsUnread(context.Context) error
|
||||
}
|
||||
)
|
||||
|
||||
func Notification(ctx context.Context, log *zap.Logger, ns notificationSender) NotificationService {
|
||||
return ¬ification{
|
||||
ac: DefaultAccessControl,
|
||||
log: log,
|
||||
store: DefaultStore,
|
||||
notificationSender: ns,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc notification) Find(ctx context.Context, filter types.NotificationFilter) (nn types.NotificationSet, f types.NotificationFilter, err error) {
|
||||
var (
|
||||
raProps = ¬ificationActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
// Get current user ID
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// Override recipient filter to ensure users can only see their own notifications
|
||||
filter.Recipient = currentUserID
|
||||
|
||||
nn, f, err = store.SearchNotifications(ctx, svc.store, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure we never return nil, but an empty slice instead
|
||||
if nn == nil {
|
||||
nn = make(types.NotificationSet, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return nn, f, svc.recordAction(ctx, raProps, NotificationActionSearch, err)
|
||||
}
|
||||
|
||||
func (svc notification) FindByID(ctx context.Context, ID uint64) (n *types.Notification, err error) {
|
||||
var (
|
||||
raProps = ¬ificationActionProps{notification: &types.Notification{ID: ID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if ID == 0 {
|
||||
return NotificationErrInvalidID()
|
||||
}
|
||||
|
||||
n, err = store.LookupNotificationByID(ctx, svc.store, ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the notification belongs to the current user
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
if n.Recipient != currentUserID {
|
||||
return NotificationErrNotFound()
|
||||
}
|
||||
|
||||
raProps.setNotification(n)
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return n, svc.recordAction(ctx, raProps, NotificationActionLookup, err)
|
||||
}
|
||||
|
||||
func (svc notification) Create(ctx context.Context, new *types.Notification) (n *types.Notification, err error) {
|
||||
var (
|
||||
raProps = ¬ificationActionProps{new: new}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
// Check if current user has permission to assign notifications to others
|
||||
if err := svc.checkAssignee(ctx, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n = new
|
||||
n.ID = nextID()
|
||||
n.CreatedAt = *now()
|
||||
|
||||
// Set creator
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
n.CreatedBy = currentUserID
|
||||
|
||||
if err = store.CreateNotification(ctx, svc.store, n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the notification via websocket
|
||||
if svc.notificationSender != nil {
|
||||
// Send only to the recipient
|
||||
if err = svc.notificationSender.Send("notification", n, n.Recipient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return n, svc.recordAction(ctx, raProps, NotificationActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc notification) Update(ctx context.Context, upd *types.Notification) (n *types.Notification, err error) {
|
||||
var (
|
||||
raProps = ¬ificationActionProps{updated: upd}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if upd.ID == 0 {
|
||||
return NotificationErrInvalidID()
|
||||
}
|
||||
|
||||
if n, err = store.LookupNotificationByID(ctx, svc.store, upd.ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the notification belongs to the current user
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
if n.Recipient != currentUserID {
|
||||
return NotificationErrNotFound()
|
||||
}
|
||||
|
||||
// Check if the recipient is being changed and if so, check permissions
|
||||
if upd.Recipient != 0 && upd.Recipient != n.Recipient {
|
||||
// Create a temporary notification for the permission check
|
||||
tempNotification := &types.Notification{
|
||||
Recipient: upd.Recipient,
|
||||
}
|
||||
|
||||
if err := svc.checkAssignee(ctx, tempNotification); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.Recipient = upd.Recipient
|
||||
}
|
||||
|
||||
// Assign changed values
|
||||
n.Kind = upd.Kind
|
||||
n.Config = upd.Config
|
||||
n.UpdatedAt = now()
|
||||
|
||||
if err = store.UpdateNotification(ctx, svc.store, n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return n, svc.recordAction(ctx, raProps, NotificationActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc notification) Delete(ctx context.Context, ID uint64) (err error) {
|
||||
var (
|
||||
n *types.Notification
|
||||
|
||||
raProps = ¬ificationActionProps{notification: &types.Notification{ID: ID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if ID == 0 {
|
||||
return NotificationErrInvalidID()
|
||||
}
|
||||
|
||||
if n, err = svc.FindByID(ctx, ID); err != nil {
|
||||
return NotificationErrNotFound()
|
||||
}
|
||||
|
||||
// Check if the notification belongs to the current user
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
if n.Recipient != currentUserID {
|
||||
return NotificationErrNotFound()
|
||||
}
|
||||
|
||||
raProps.setNotification(n)
|
||||
|
||||
n.DeletedAt = now()
|
||||
|
||||
if err = store.UpdateNotification(ctx, svc.store, n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the deleted notification via websocket so client can update UI
|
||||
if svc.notificationSender != nil {
|
||||
if err = svc.notificationSender.Send("notification.delete", n, n.Recipient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, raProps, NotificationActionDelete, err)
|
||||
}
|
||||
|
||||
func (svc notification) MarkAsRead(ctx context.Context, ID uint64) (err error) {
|
||||
var (
|
||||
n *types.Notification
|
||||
|
||||
raProps = ¬ificationActionProps{notification: &types.Notification{ID: ID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if ID == 0 {
|
||||
return NotificationErrInvalidID()
|
||||
}
|
||||
|
||||
if n, err = store.LookupNotificationByID(ctx, svc.store, ID); err != nil {
|
||||
return NotificationErrNotFound()
|
||||
}
|
||||
|
||||
// Check if the notification belongs to the current user
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
if n.Recipient != currentUserID {
|
||||
return NotificationErrNotAllowedToRead()
|
||||
}
|
||||
|
||||
raProps.setNotification(n)
|
||||
|
||||
// Mark as read
|
||||
now := time.Now()
|
||||
n.ReadAt = &now
|
||||
n.UpdatedAt = &now
|
||||
|
||||
if err = store.UpdateNotification(ctx, svc.store, n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the updated notification via websocket so client can update UI
|
||||
if svc.notificationSender != nil {
|
||||
if err = svc.notificationSender.Send("notification.read", n, n.Recipient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, raProps, NotificationActionMarkAsRead, err)
|
||||
}
|
||||
|
||||
func (svc notification) MarkAsUnread(ctx context.Context, ID uint64) (err error) {
|
||||
var (
|
||||
n *types.Notification
|
||||
|
||||
raProps = ¬ificationActionProps{notification: &types.Notification{ID: ID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if ID == 0 {
|
||||
return NotificationErrInvalidID()
|
||||
}
|
||||
|
||||
if n, err = store.LookupNotificationByID(ctx, svc.store, ID); err != nil {
|
||||
return NotificationErrNotFound()
|
||||
}
|
||||
|
||||
// Check if the notification belongs to the current user
|
||||
currentUserID := intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
if n.Recipient != currentUserID {
|
||||
return NotificationErrNotAllowedToRead()
|
||||
}
|
||||
|
||||
raProps.setNotification(n)
|
||||
|
||||
// Mark as unread
|
||||
n.ReadAt = nil
|
||||
now := time.Now()
|
||||
n.UpdatedAt = &now
|
||||
|
||||
if err = store.UpdateNotification(ctx, svc.store, n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the updated notification via websocket so client can update UI
|
||||
if svc.notificationSender != nil {
|
||||
if err = svc.notificationSender.Send("notification.unread", n, n.Recipient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, raProps, NotificationActionMarkAsUnread, err)
|
||||
}
|
||||
|
||||
func (svc notification) MarkAllAsRead(ctx context.Context) (err error) {
|
||||
var (
|
||||
currentUserID = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
raProps = ¬ificationActionProps{notification: &types.Notification{Recipient: currentUserID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
var (
|
||||
cursor *filter.PagingCursor
|
||||
nn types.NotificationSet
|
||||
f types.NotificationFilter
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
// Process unread notifications in batches
|
||||
for {
|
||||
// Query unread notifications with pagination
|
||||
f = types.NotificationFilter{
|
||||
Recipient: currentUserID,
|
||||
Read: filter.StateExcluded,
|
||||
Paging: filter.Paging{
|
||||
Limit: 100,
|
||||
PageCursor: cursor,
|
||||
},
|
||||
}
|
||||
|
||||
nn, f, err = store.SearchNotifications(ctx, svc.store, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Mark all notifications in this batch as read
|
||||
for _, n := range nn {
|
||||
n.ReadAt = &now
|
||||
n.UpdatedAt = &now
|
||||
}
|
||||
|
||||
err = store.UpdateNotification(ctx, svc.store, nn...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the updated notifications via websocket so client can update UI
|
||||
if svc.notificationSender != nil && len(nn) > 0 {
|
||||
if err = svc.notificationSender.Send("notification.read.all", nn, currentUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor for next page or break if no more pages
|
||||
cursor = f.PageCursor
|
||||
if cursor == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, raProps, NotificationActionMarkAllAsRead, err)
|
||||
}
|
||||
|
||||
func (svc notification) MarkAllAsUnread(ctx context.Context) (err error) {
|
||||
var (
|
||||
currentUserID = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
raProps = ¬ificationActionProps{notification: &types.Notification{Recipient: currentUserID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
var (
|
||||
cursor *filter.PagingCursor
|
||||
nn types.NotificationSet
|
||||
f types.NotificationFilter
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
for {
|
||||
f = types.NotificationFilter{
|
||||
Recipient: currentUserID,
|
||||
Read: filter.StateExclusive, // only read notifications
|
||||
Paging: filter.Paging{
|
||||
Limit: 100,
|
||||
PageCursor: cursor,
|
||||
},
|
||||
}
|
||||
|
||||
nn, f, err = store.SearchNotifications(ctx, svc.store, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range nn {
|
||||
n.ReadAt = nil
|
||||
n.UpdatedAt = &now
|
||||
}
|
||||
|
||||
if err = store.UpdateNotification(ctx, svc.store, nn...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if svc.notificationSender != nil && len(nn) > 0 {
|
||||
if err = svc.notificationSender.Send("notification.unread.all", nn, currentUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cursor = f.PageCursor
|
||||
if cursor == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, raProps, NotificationActionMarkAllAsUnread, err)
|
||||
}
|
||||
|
||||
func (svc notification) checkAssignee(ctx context.Context, n *types.Notification) (err error) {
|
||||
// Check if user is assigning to someone else
|
||||
if n.Recipient != svc.currentUser(ctx) {
|
||||
if !svc.ac.CanAssignNotification(ctx) {
|
||||
return NotificationErrNotAllowedToAssign()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc notification) currentUser(ctx context.Context) uint64 {
|
||||
return intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
}
|
||||
Reference in New Issue
Block a user