feat: extend audit event coverage to auth, security, and lifecycle paths

Second wave of emission sites covering the remaining taxonomy items
that map to existing features:

- email_sent (activation / reset / email-change kinds)
- user_activated, user_token_refresh (actor decoded from claims)
- share_link_viewed (CountViews path)
- set_current_version / delete_version / update_metadata / update_view
  with file linkage resolved post-op
- live_photo_uploaded (entity-type switch at upload completion)
- thumb_generated (non-stateless thumb pipeline)
- copy_from on source file; copy_to now links the new copy
- webdav_login_failed + dav account create/update/delete
- nick/avatar/password/2FA/passkey security changes
- oauth_grant create/exchange/revoke
- admin user_changed / group_changed / storage_added
- file_imported per imported physical file
- activity.Actor opt for ctx-less or differing actors
- WithActivityClient dep option; davbearer test fixture wired

Unwired IDs correspond to features that do not exist yet (payment,
account linking, membership, mount, abuse report, quota notify).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3587/head
Tomas Dvorak 2 weeks ago
parent 2505c647dd
commit 1bbaddf832

@ -160,6 +160,13 @@ func WithShareClient(s inventory.ShareClient) Option {
}) })
} }
// WithActivityClient Set the default activity client
func WithActivityClient(s inventory.ActivityClient) Option {
return optionFunc(func(o *dependency) {
o.activityClient = s
})
}
// WithSearchIndexer Set the default search indexer // WithSearchIndexer Set the default search indexer
func WithSearchIndexer(s searcher.SearchIndexer) Option { func WithSearchIndexer(s searcher.SearchIndexer) Option {
return optionFunc(func(o *dependency) { return optionFunc(func(o *dependency) {

@ -9,6 +9,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
@ -162,6 +163,8 @@ func WebDAVAuth() gin.HandlerFunc {
} }
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventWebdavLoginFailed,
activity.Extra(map[string]any{"username": username}))
l.Debug("WebDAVAuth: failed to get user %q with provided credential: %s", username, err) l.Debug("WebDAVAuth: failed to get user %q with provided credential: %s", username, err)
c.Status(http.StatusUnauthorized) c.Status(http.StatusUnauthorized)
c.Abort() c.Abort()

@ -13,11 +13,19 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/auth" "github.com/cloudreve/Cloudreve/v4/pkg/auth"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset" "github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/cache" "github.com/cloudreve/Cloudreve/v4/pkg/cache"
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
"github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type stubSettingProvider struct {
setting.Provider
}
func (stubSettingProvider) AuditLogEnabled(context.Context, int) bool { return true }
type stubTokenAuth struct { type stubTokenAuth struct {
uid int uid int
scopes []string scopes []string
@ -68,6 +76,8 @@ func newDavAuthDep(t *testing.T, groupPerms *boolset.BooleanSet, scopes []string
dependency.WithLogger(logging.NewConsoleLogger(logging.LevelDebug)), dependency.WithLogger(logging.NewConsoleLogger(logging.LevelDebug)),
dependency.WithUserClient(inventory.NewUserClient(client)), dependency.WithUserClient(inventory.NewUserClient(client)),
dependency.WithTokenAuth(stubTokenAuth{uid: u.ID, scopes: scopes}), dependency.WithTokenAuth(stubTokenAuth{uid: u.ID, scopes: scopes}),
dependency.WithSettingProvider(stubSettingProvider{}),
dependency.WithActivityClient(inventory.NewActivityClient(client, conf.SQLiteDB)),
) )
} }

@ -20,6 +20,12 @@ func File(id int) Opt {
return func(p *inventory.RecordActivityParams) { p.FileID = id } return func(p *inventory.RecordActivityParams) { p.FileID = id }
} }
// Actor overrides the actor ID when ctx carries no user (e.g. token
// refresh, activation) or the actor differs from the ctx user.
func Actor(id int) Opt {
return func(p *inventory.RecordActivityParams) { p.ActorID = id }
}
// Share attaches the subject share ID. // Share attaches the subject share ID.
func Share(id int) Opt { func Share(id int) Opt {
return func(p *inventory.RecordActivityParams) { p.ShareID = id } return func(p *inventory.RecordActivityParams) { p.ShareID = id }

@ -708,8 +708,11 @@ func (f *DBFS) MoveOrCopy(ctx context.Context, path []*fs.URI, dst *fs.URI, isCo
for _, target := range targets { for _, target := range targets {
if isCopy { if isCopy {
f.emitFileCreated(ctx, newFile(destination, copiedNewTargetsMap[target.ID()])) copied := copiedNewTargetsMap[target.ID()]
f.record(ctx, types.EventCopyTo, activity.File(target.ID()), activity.Extra(map[string]any{"dst": destination.Uri(false).String()})) f.emitFileCreated(ctx, newFile(destination, copied))
dst := destination.Uri(false).String()
f.record(ctx, types.EventCopyTo, activity.File(copied.ID), activity.Extra(map[string]any{"dst": dst}))
f.record(ctx, types.EventCopyFrom, activity.File(target.ID()), activity.Extra(map[string]any{"dst": dst}))
} else { } else {
f.emitFileMoved(ctx, target, destination) f.emitFileMoved(ctx, target, destination)
f.record(ctx, types.EventMoveTo, activity.File(target.ID()), activity.Extra(map[string]any{"dst": destination.Uri(false).String()})) f.record(ctx, types.EventMoveTo, activity.File(target.ID()), activity.Extra(map[string]any{"dst": destination.Uri(false).String()}))

@ -392,7 +392,11 @@ func (f *DBFS) CompleteUpload(ctx context.Context, session *fs.UploadSession) (f
} }
f.emitFileModified(ctx, filePrivate) f.emitFileModified(ctx, filePrivate)
f.record(ctx, types.EventEntityUploaded, activity.File(filePrivate.Model.ID), activity.Extra(map[string]any{"uri": session.Props.Uri.String(), "size": session.Props.Size})) uploadEvent := types.EventEntityUploaded
if entityType == types.EntityTypeLivePhoto {
uploadEvent = types.EventLivePhotoUploaded
}
f.record(ctx, uploadEvent, activity.File(filePrivate.Model.ID), activity.Extra(map[string]any{"uri": session.Props.Uri.String(), "size": session.Props.Size}))
file, err = f.Get(ctx, session.Props.Uri, WithFileEntities(), WithNotRoot()) file, err = f.Get(ctx, session.Props.Uri, WithFileEntities(), WithNotRoot())
if err != nil { if err != nil {

@ -12,6 +12,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/task" "github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
@ -220,6 +221,8 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es
return nil, fmt.Errorf("failed to find thumb entity") return nil, fmt.Errorf("failed to find thumb entity")
} }
activity.Record(ctx, m.settings, m.dep.ActivityClient(), types.EventThumbGenerated,
activity.File(file.ID()), activity.Extra(map[string]any{"uri": uri.String()}))
} }
if m.settings.ThumbGCAfterGen(ctx) { if m.settings.ThumbGCAfterGen(ctx) {

@ -13,6 +13,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/task" "github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
@ -203,6 +204,9 @@ func (m *ImportTask) processBatch(ctx context.Context, dep dependency.Dep, user
} }
m.l.Error("Failed to import file %s: %s, skipping", physicalFile.RelativePath, err) m.l.Error("Failed to import file %s: %s, skipping", physicalFile.RelativePath, err)
failed++ failed++
} else {
activity.Record(ctx, dep.SettingProvider(), dep.ActivityClient(), types.EventFileImported,
activity.Extra(map[string]any{"uri": dst.Join(physicalFile.RelativePath).String(), "src": physicalFile.Source}))
} }
} }
} }

@ -10,6 +10,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent/user" "github.com/cloudreve/Cloudreve/v4/ent/user"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset" "github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
@ -265,6 +266,17 @@ func (s *UpsertUserService) Update(c *gin.Context) (*GetUserResponse, error) {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update user", err) return nil, serializer.NewError(serializer.CodeDBError, "Failed to update user", err)
} }
subject := activity.Extra(map[string]any{"user_id": newUser.ID})
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventUserChanged, subject)
if existing.GroupUsers != newUser.GroupUsers {
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventGroupChanged,
subject, activity.Extra(map[string]any{"from": existing.GroupUsers, "to": newUser.GroupUsers}))
}
if existing.Storage != newUser.Storage {
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventStorageAdded,
subject, activity.Extra(map[string]any{"from": existing.Storage, "to": newUser.Storage}))
}
service := &SingleUserService{ID: newUser.ID} service := &SingleUserService{ID: newUser.ID}
return service.Get(c) return service.Get(c)
} }

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
@ -82,6 +84,12 @@ func (s *SetCurrentVersionService) Set(c *gin.Context) error {
return fmt.Errorf("failed to set current version: %w", err) return fmt.Errorf("failed to set current version: %w", err)
} }
opts := []activity.Opt{activity.Extra(map[string]any{"version": versionId})}
if target, err := m.Get(c, uri); err == nil {
opts = append(opts, activity.File(target.ID()))
}
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventSetCurrentVersion, opts...)
return nil return nil
} }
@ -114,5 +122,11 @@ func (s *DeleteVersionService) Delete(c *gin.Context) error {
return fmt.Errorf("failed to delete version: %w", err) return fmt.Errorf("failed to delete version: %w", err)
} }
opts := []activity.Opt{activity.Extra(map[string]any{"version": versionId})}
if target, err := m.Get(c, uri); err == nil {
opts = append(opts, activity.File(target.ID()))
}
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventDeleteVersion, opts...)
return nil return nil
} }

@ -784,6 +784,12 @@ func (s *PatchViewService) Patch(c *gin.Context) error {
return err return err
} }
opts := []activity.Opt{activity.Extra(map[string]any{"view": s.View})}
if target, err := m.Get(c, uri); err == nil {
opts = append(opts, activity.File(target.ID()))
}
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventUpdateView, opts...)
return nil return nil
} }

@ -3,6 +3,8 @@ package explorer
import ( import (
"github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/serializer"
@ -33,5 +35,21 @@ func (s *PatchMetadataService) Patch(c *gin.Context) error {
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err) return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
} }
return m.PatchMedata(c, uris, s.Patches...) if err := m.PatchMedata(c, uris, s.Patches...); err != nil {
return err
}
keys := make([]string, 0, len(s.Patches))
for _, p := range s.Patches {
keys = append(keys, p.Key)
}
for _, uri := range uris {
opts := []activity.Opt{activity.Extra(map[string]any{"keys": keys})}
if target, err := m.Get(c, uri); err == nil {
opts = append(opts, activity.File(target.ID()))
}
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventUpdateMetadata, opts...)
}
return nil
} }

@ -10,6 +10,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/auth" "github.com/cloudreve/Cloudreve/v4/pkg/auth"
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
@ -102,6 +103,8 @@ func (s *GrantService) Issue(c *gin.Context) (*GrantResponse, error) {
if err := oAuthClient.UpsertGrant(c, user.ID, app.ID, requestedScopes); err != nil { if err := oAuthClient.UpsertGrant(c, user.ID, app.ID, requestedScopes); err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create grant", err) return nil, serializer.NewError(serializer.CodeDBError, "Failed to create grant", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventOAuthGrantCreate,
activity.Extra(map[string]any{"client_id": s.ClientID, "scopes": requestedScopes}))
// 4. Generate code and save required state into KV for future token exchange request. // 4. Generate code and save required state into KV for future token exchange request.
code := util.RandStringRunesCrypto(128) code := util.RandStringRunesCrypto(128)
@ -221,6 +224,8 @@ func (s *ExchangeTokenService) Exchange(c *gin.Context) (*TokenResponse, error)
if err := oAuthClient.UpdateGrantLastUsedAt(c, user.ID, app.ID); err != nil { if err := oAuthClient.UpdateGrantLastUsedAt(c, user.ID, app.ID); err != nil {
dep.Logger().Warning("Failed to update grant last used at: %s", err) dep.Logger().Warning("Failed to update grant last used at: %s", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventOAuthTokenExchange,
activity.Actor(user.ID), activity.Extra(map[string]any{"client_id": s.ClientID}))
// 10. // 10.
@ -318,6 +323,8 @@ func (s *DeleteOAuthGrantService) Delete(c *gin.Context) error {
if !deleted { if !deleted {
return serializer.NewError(serializer.CodeNotFound, "OAuth grant not found", nil) return serializer.NewError(serializer.CodeNotFound, "OAuth grant not found", nil)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventOAuthGrantRevoke,
activity.Extra(map[string]any{"app_id": s.AppID}))
return nil return nil
} }

@ -6,6 +6,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset" "github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
@ -114,6 +115,8 @@ func (service *CreateDavAccountService) Create(c *gin.Context) (*DavAccount, err
if err != nil { if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create dav account", err) return nil, serializer.NewError(serializer.CodeDBError, "Failed to create dav account", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventWebdavAccountCreate,
activity.Extra(map[string]any{"name": service.Name, "uri": service.Uri}))
accountRes := BuildDavAccount(account, dep.HashIDEncoder()) accountRes := BuildDavAccount(account, dep.HashIDEncoder())
return &accountRes, nil return &accountRes, nil
@ -146,6 +149,8 @@ func (service *CreateDavAccountService) Update(c *gin.Context) (*DavAccount, err
if err != nil { if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update dav account", err) return nil, serializer.NewError(serializer.CodeDBError, "Failed to update dav account", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventWebdavAccountUpdate,
activity.Extra(map[string]any{"name": service.Name, "uri": service.Uri}))
accountRes := BuildDavAccount(account, dep.HashIDEncoder()) accountRes := BuildDavAccount(account, dep.HashIDEncoder())
return &accountRes, nil return &accountRes, nil
@ -197,6 +202,8 @@ func DeleteDavAccount(c *gin.Context) error {
if err := davAccountClient.Delete(c, accountId); err != nil { if err := davAccountClient.Delete(c, accountId); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to delete dav account", err) return serializer.NewError(serializer.CodeDBError, "Failed to delete dav account", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventWebdavAccountDelete,
activity.Extra(map[string]any{"account_id": accountId}))
return nil return nil
} }

@ -8,6 +8,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
@ -78,6 +79,8 @@ func (s *ShareInfoService) Get(c *gin.Context) (*explorer.Share, error) {
if s.CountViews { if s.CountViews {
_ = shareClient.Viewed(c, share) _ = shareClient.Viewed(c, share)
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventShareLinkViewed,
activity.Share(share.ID), activity.File(share.Edges.File.ID))
} }
unlocked := true unlocked := true

@ -7,6 +7,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/auth" "github.com/cloudreve/Cloudreve/v4/pkg/auth"
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
"github.com/cloudreve/Cloudreve/v4/pkg/email" "github.com/cloudreve/Cloudreve/v4/pkg/email"
@ -82,6 +83,8 @@ func (s *RequestEmailChangeService) Request(c *gin.Context) error {
if err := dep.EmailClient(c).Send(c, s.NewEmail, title, body); err != nil { if err := dep.EmailClient(c).Send(c, s.NewEmail, title, body); err != nil {
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send confirmation email", err) return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send confirmation email", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventEmailSent,
activity.Extra(map[string]any{"kind": "email_change"}))
return nil return nil
} }

@ -124,6 +124,8 @@ func (service *UserResetEmailService) Reset(c *gin.Context) error {
if err := dep.EmailClient(c).Send(c, u.Email, title, body); err != nil { if err := dep.EmailClient(c).Send(c, u.Email, title, body); err != nil {
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err) return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventEmailSent,
activity.Actor(u.ID), activity.Extra(map[string]any{"kind": "password_reset"}))
return nil return nil
} }
@ -206,11 +208,20 @@ type RefreshTokenService struct {
func (s *RefreshTokenService) Refresh(c *gin.Context) (*auth.Token, error) { func (s *RefreshTokenService) Refresh(c *gin.Context) (*auth.Token, error) {
dep := dependency.FromContext(c) dep := dependency.FromContext(c)
claims, _ := dep.TokenAuth().Claims(c, s.RefreshToken)
token, err := dep.TokenAuth().Refresh(c, s.RefreshToken) token, err := dep.TokenAuth().Refresh(c, s.RefreshToken)
if err != nil { if err != nil {
return nil, serializer.NewError(serializer.CodeCredentialInvalid, "Failed to issue token pair", err) return nil, serializer.NewError(serializer.CodeCredentialInvalid, "Failed to issue token pair", err)
} }
opts := []activity.Opt{}
if claims != nil {
if uid, err := dep.HashIDEncoder().Decode(claims.Subject, hashid.UserID); err == nil {
opts = append(opts, activity.Actor(uid))
}
}
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventUserTokenRefresh, opts...)
return token, nil return token, nil
} }

@ -9,6 +9,8 @@ import (
"github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/cloudreve/Cloudreve/v4/pkg/util"
@ -246,6 +248,8 @@ func (s *FinishPasskeyRegisterService) FinishPasskeyRegister(c *gin.Context) (*P
if err != nil { if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to add passkey", err) return nil, serializer.NewError(serializer.CodeDBError, "Failed to add passkey", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventAddPasskey,
activity.Extra(map[string]any{"name": name}))
res := BuildPasskey(passkey) res := BuildPasskey(passkey)
return &res, nil return &res, nil
@ -283,6 +287,8 @@ func (s *DeletePasskeyService) DeletePasskey(c *gin.Context) error {
if err := userClient.RemovePasskey(c, u.ID, s.ID); err != nil { if err := userClient.RemovePasskey(c, u.ID, s.ID); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to delete passkey", err) return serializer.NewError(serializer.CodeDBError, "Failed to delete passkey", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventRemovePasskey,
activity.Extra(map[string]any{"credential_id": s.ID}))
return nil return nil
} }

@ -145,6 +145,8 @@ func sendActivationEmail(ctx context.Context, dep dependency.Dep, newUser *ent.U
if err := dep.EmailClient(ctx).Send(ctx, newUser.Email, title, body); err != nil { if err := dep.EmailClient(ctx).Send(ctx, newUser.Email, title, body); err != nil {
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err) return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
} }
activity.Record(ctx, dep.SettingProvider(), dep.ActivityClient(), types.EventEmailSent,
activity.Actor(newUser.ID), activity.Extra(map[string]any{"kind": "activation"}))
return nil return nil
} }
@ -173,5 +175,7 @@ func ActivateUser(c *gin.Context) serializer.Response {
} }
util.WithValue(c, inventory.UserCtx{}, activeUser) util.WithValue(c, inventory.UserCtx{}, activeUser)
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventUserActivated,
activity.Actor(activeUser.ID))
return serializer.Response{Data: BuildUser(activeUser, dep.HashIDEncoder())} return serializer.Response{Data: BuildUser(activeUser, dep.HashIDEncoder())}
} }

@ -16,6 +16,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/activity"
"github.com/cloudreve/Cloudreve/v4/pkg/auth" "github.com/cloudreve/Cloudreve/v4/pkg/auth"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/request" "github.com/cloudreve/Cloudreve/v4/pkg/request"
@ -177,6 +178,7 @@ func UpdateUserAvatar(c *gin.Context) error {
if _, err := dep.UserClient().UpdateAvatar(c, u, GravatarAvatar); err != nil { if _, err := dep.UserClient().UpdateAvatar(c, u, GravatarAvatar); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err) return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventChangeAvatar)
return nil return nil
} }
@ -218,6 +220,7 @@ func updateAvatarFile(ctx context.Context, u *ent.User, contentType string, file
if _, err := dep.UserClient().UpdateAvatar(ctx, u, FileAvatar); err != nil { if _, err := dep.UserClient().UpdateAvatar(ctx, u, FileAvatar); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err) return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
} }
activity.Record(ctx, dep.SettingProvider(), dep.ActivityClient(), types.EventChangeAvatar)
return nil return nil
} }
@ -282,6 +285,7 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
if _, err := userClient.UpdateNickname(c, u, *s.Nick); err != nil { if _, err := userClient.UpdateNickname(c, u, *s.Nick); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user nick", err) return serializer.NewError(serializer.CodeDBError, "Failed to update user nick", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventChangeNick)
} }
if s.Language != nil { if s.Language != nil {
@ -382,6 +386,7 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
if _, err := userClient.UpdatePassword(c, u, *s.NewPassword); err != nil { if _, err := userClient.UpdatePassword(c, u, *s.NewPassword); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user password", err) return serializer.NewError(serializer.CodeDBError, "Failed to update user password", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventChangePassword)
} }
if s.TwoFAEnabled != nil { if s.TwoFAEnabled != nil {
@ -403,6 +408,7 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
if _, err := userClient.UpdateTwoFASecret(c, u, secret.(string)); err != nil { if _, err := userClient.UpdateTwoFASecret(c, u, secret.(string)); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err) return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventEnable2FA)
} else { } else {
if !totp.Validate(*s.TwoFACode, u.TwoFactorSecret) { if !totp.Validate(*s.TwoFACode, u.TwoFactorSecret) {
@ -412,6 +418,7 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
if _, err := userClient.UpdateTwoFASecret(c, u, ""); err != nil { if _, err := userClient.UpdateTwoFASecret(c, u, ""); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err) return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
} }
activity.Record(c, dep.SettingProvider(), dep.ActivityClient(), types.EventDisable2FA)
} }
} }

Loading…
Cancel
Save