From 7f1c2c2f1178d01519b278a67c8d246fba9cd309 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 16 May 2025 02:15:52 +0530 Subject: [PATCH] feat(wip): activity log / audit log - single table stores acitivites against entities, actors, timestamps, ip addresses and activity description. - admin page to view, sort and filter activity logs. - new `activity_logs:manage` permission --- cmd/actvity_log.go | 36 +++ cmd/handlers.go | 5 +- cmd/init.go | 15 ++ cmd/login.go | 18 +- cmd/main.go | 3 + cmd/users.go | 28 +- frontend/src/api/index.js | 4 +- frontend/src/components/table/SimpleTable.vue | 10 +- .../src/composables/useActivityLogFilters.js | 39 +++ frontend/src/constants/navigation.js | 5 + frontend/src/constants/permissions.js | 3 +- .../admin/activity-log/ActivityLog.vue | 246 ++++++++++++++++++ .../src/features/admin/roles/RoleForm.vue | 3 +- .../src/features/contact/ContactsList.vue | 17 +- .../{view => filter}/FilterBuilder.vue | 0 frontend/src/features/view/ViewForm.vue | 2 +- frontend/src/layouts/admin/ActivityLog.vue | 5 + frontend/src/layouts/contact/ContactList.vue | 2 +- frontend/src/router/index.js | 6 + frontend/src/utils/pagination.js | 15 ++ .../views/admin/activity-log/ActivityLog.vue | 9 + i18n/en.json | 4 + internal/activity_log/activity_log.go | 210 +++++++++++++++ internal/activity_log/models/models.go | 28 ++ internal/activity_log/queries.sql | 26 ++ internal/authz/models/models.go | 4 + internal/migrations/v0.6.0.go | 46 ++++ internal/user/agent.go | 2 +- internal/user/models/models.go | 2 + schema.sql | 16 ++ 30 files changed, 781 insertions(+), 28 deletions(-) create mode 100644 cmd/actvity_log.go create mode 100644 frontend/src/composables/useActivityLogFilters.js create mode 100644 frontend/src/features/admin/activity-log/ActivityLog.vue rename frontend/src/features/{view => filter}/FilterBuilder.vue (100%) create mode 100644 frontend/src/layouts/admin/ActivityLog.vue create mode 100644 frontend/src/utils/pagination.js create mode 100644 frontend/src/views/admin/activity-log/ActivityLog.vue create mode 100644 internal/activity_log/activity_log.go create mode 100644 internal/activity_log/models/models.go create mode 100644 internal/activity_log/queries.sql diff --git a/cmd/actvity_log.go b/cmd/actvity_log.go new file mode 100644 index 0000000..17057a1 --- /dev/null +++ b/cmd/actvity_log.go @@ -0,0 +1,36 @@ +package main + +import ( + "strconv" + + "github.com/abhinavxd/libredesk/internal/envelope" + "github.com/zerodha/fastglue" +) + +// handleGetActivityLogs returns activity logs from the database. +func handleGetActivityLogs(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + order = string(r.RequestCtx.QueryArgs().Peek("order")) + orderBy = string(r.RequestCtx.QueryArgs().Peek("order_by")) + filters = string(r.RequestCtx.QueryArgs().Peek("filters")) + page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) + pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) + total = 0 + ) + logs, err := app.activityLog.GetAll(order, orderBy, filters, page, pageSize) + if err != nil { + return sendErrorEnvelope(r, err) + } + if len(logs) > 0 { + total = logs[0].Total + } + return r.SendEnvelope(envelope.PageResults{ + Results: logs, + Total: total, + PerPage: pageSize, + TotalPages: (total + pageSize - 1) / pageSize, + Page: page, + }) + +} diff --git a/cmd/handlers.go b/cmd/handlers.go index de6bfcb..9aa947d 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -16,7 +16,7 @@ import ( func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { // Authentication. g.POST("/api/v1/login", handleLogin) - g.GET("/logout", handleLogout) + g.GET("/logout", auth(handleLogout)) g.GET("/api/v1/oidc/{id}/login", handleOIDCLogin) g.GET("/api/v1/oidc/{id}/finish", handleOIDCCallback) @@ -195,6 +195,9 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.PUT("/api/v1/custom-attributes/{id}", perm(handleUpdateCustomAttribute, "custom_attributes:manage")) g.DELETE("/api/v1/custom-attributes/{id}", perm(handleDeleteCustomAttribute, "custom_attributes:manage")) + // Actvity logs. + g.GET("/api/v1/activity-logs", perm(handleGetActivityLogs, "activity_logs:manage")) + // WebSocket. g.GET("/ws", auth(func(r *fastglue.Request) error { return handleWS(r, hub) diff --git a/cmd/init.go b/cmd/init.go index 925b483..e3fdc69 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -12,6 +12,7 @@ import ( "html/template" + activitylog "github.com/abhinavxd/libredesk/internal/activity_log" "github.com/abhinavxd/libredesk/internal/ai" auth_ "github.com/abhinavxd/libredesk/internal/auth" "github.com/abhinavxd/libredesk/internal/authz" @@ -808,6 +809,20 @@ func initCustomAttribute(db *sqlx.DB, i18n *i18n.I18n) *customAttribute.Manager return m } +// initActivityLog inits activity log manager. +func initActivityLog(db *sqlx.DB, i18n *i18n.I18n) *activitylog.Manager { + lo := initLogger("activity-log") + m, err := activitylog.New(activitylog.Opts{ + DB: db, + Lo: lo, + I18n: i18n, + }) + if err != nil { + log.Fatalf("error initializing activity log manager: %v", err) + } + return m +} + // initLogger initializes a logf logger. func initLogger(src string) *logf.Logger { lvl, env := ko.MustString("app.log_level"), ko.MustString("app.env") diff --git a/cmd/login.go b/cmd/login.go index c6175b4..6e52a94 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -14,6 +14,7 @@ func handleLogin(r *fastglue.Request) error { app = r.Context.(*App) email = string(r.RequestCtx.PostArgs().Peek("email")) password = r.RequestCtx.PostArgs().Peek("password") + ip = r.RequestCtx.RemoteIP().String() ) // Verify email and password. @@ -53,12 +54,27 @@ func handleLogin(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } + // Insert activity log. + if err := app.activityLog.Login(user.ID, user.Email.String, ip); err != nil { + app.lo.Error("error creating login activity log", "error", err) + } + return r.SendEnvelope(user) } // handleLogout logs out the user and redirects to the dashboard. func handleLogout(r *fastglue.Request) error { - var app = r.Context.(*App) + var ( + app = r.Context.(*App) + auser = r.RequestCtx.UserValue("user").(amodels.User) + ip = r.RequestCtx.RemoteIP().String() + ) + + // Insert activity log. + if err := app.activityLog.Logout(auser.ID, auser.Email, ip); err != nil { + app.lo.Error("error creating logout activity log", "error", err) + } + if err := app.auth.DestroySession(r); err != nil { return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorDestroying", "name", "{globals.terms.session}"), nil)) } diff --git a/cmd/main.go b/cmd/main.go index 5ff00b2..7779b01 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,6 +13,7 @@ import ( _ "time/tzdata" + activitylog "github.com/abhinavxd/libredesk/internal/activity_log" "github.com/abhinavxd/libredesk/internal/ai" auth_ "github.com/abhinavxd/libredesk/internal/auth" "github.com/abhinavxd/libredesk/internal/authz" @@ -86,6 +87,7 @@ type App struct { view *view.Manager ai *ai.Manager search *search.Manager + activityLog *activitylog.Manager notifier *notifier.Service customAttribute *customAttribute.Manager @@ -218,6 +220,7 @@ func main() { conversation: conversation, automation: automation, businessHours: businessHours, + activityLog: initActivityLog(db, i18n), customAttribute: initCustomAttribute(db, i18n), authz: initAuthz(i18n), view: initView(db), diff --git a/cmd/users.go b/cmd/users.go index 3e040d4..dc840b3 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -69,10 +69,19 @@ func handleUpdateAgentAvailability(r *fastglue.Request) error { app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) status = string(r.RequestCtx.PostArgs().Peek("status")) + ip = r.RequestCtx.RemoteIP().String() ) + + // Update availability status. if err := app.user.UpdateAvailability(auser.ID, status); err != nil { return sendErrorEnvelope(r, err) } + + // Create activity log. + if err := app.activityLog.UserAvailability(auser.ID, auser.Email, status, ip, "" /*peformedByEmail*/, 0 /*peformedByID*/); err != nil { + app.lo.Error("error creating activity log", "error", err) + } + return r.SendEnvelope(true) } @@ -189,8 +198,10 @@ func handleCreateAgent(r *fastglue.Request) error { // handleUpdateAgent updates an agent. func handleUpdateAgent(r *fastglue.Request) error { var ( - app = r.Context.(*App) - user = models.User{} + app = r.Context.(*App) + user = models.User{} + auser = r.RequestCtx.UserValue("user").(amodels.User) + ip = r.RequestCtx.RemoteIP().String() ) id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string)) if err != nil || id == 0 { @@ -213,11 +224,24 @@ func handleUpdateAgent(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`first_name`"), nil, envelope.InputError) } + agent, err := app.user.GetAgent(id, "") + if err != nil { + return sendErrorEnvelope(r, err) + } + oldAvailabilityStatus := agent.AvailabilityStatus + // Update agent. if err = app.user.UpdateAgent(id, user); err != nil { return sendErrorEnvelope(r, err) } + // Create activity log if user availability status changed. + if oldAvailabilityStatus != user.AvailabilityStatus { + if err := app.activityLog.UserAvailability(id, user.Email.String, user.AvailabilityStatus, ip, auser.Email, auser.ID); err != nil { + app.lo.Error("error creating activity log", "error", err) + } + } + // Upsert agent teams. if err := app.team.UpsertUserTeams(id, user.Teams.Names()); err != nil { return sendErrorEnvelope(r, err) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index cfb1fc4..2280760 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -315,6 +315,7 @@ const updateAIProvider = (data) => http.put('/api/v1/ai/provider', data) const getContactNotes = (id) => http.get(`/api/v1/contacts/${id}/notes`) const createContactNote = (id, data) => http.post(`/api/v1/contacts/${id}/notes`, data) const deleteContactNote = (id, noteId) => http.delete(`/api/v1/contacts/${id}/notes/${noteId}`) +const getActivityLogs = (params) => http.get('/api/v1/activity-logs', { params }) export default { login, @@ -442,5 +443,6 @@ export default { getCustomAttribute, getContactNotes, createContactNote, - deleteContactNote + deleteContactNote, + getActivityLogs } diff --git a/frontend/src/components/table/SimpleTable.vue b/frontend/src/components/table/SimpleTable.vue index 17343b8..69cbe66 100644 --- a/frontend/src/components/table/SimpleTable.vue +++ b/frontend/src/components/table/SimpleTable.vue @@ -1,5 +1,5 @@