Files
bssapp/svc/repository/activitylog_repository.go
2026-02-11 17:46:22 +03:00

287 lines
6.1 KiB
Go

package repository
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
)
type ActivityLogRow struct {
CreatedAt time.Time `json:"created_at"`
RequestStartedAt *time.Time `json:"request_started_at,omitempty"`
RequestFinishedAt *time.Time `json:"request_finished_at,omitempty"`
DurationMs *int `json:"duration_ms,omitempty"`
HttpStatus *int `json:"http_status,omitempty"`
Username string `json:"username"`
RoleCode string `json:"role_code"`
ActionCategory string `json:"action_category"`
ActionType string `json:"action_type"`
ActionTarget string `json:"action_target"`
Description string `json:"description"`
// ✅ TARGET USER
TargetDfUsrID int64 `json:"target_dfusr_id"`
TargetUsername string `json:"target_username"`
// ✅ DIFF
ChangeBefore string `json:"change_before"`
ChangeAfter string `json:"change_after"`
IpAddress string `json:"ip_address"`
IsSuccess bool `json:"is_success"`
ErrorMsg string `json:"error_message"`
UserAgent string `json:"user_agent"`
SessionID string `json:"session_id"`
// audit ids
UserUUID string `json:"user_id"`
DfUsrID int64 `json:"dfusr_id"`
Email string `json:"email"`
IsActive bool `json:"is_active"`
}
type ActivityLogQuery struct {
Username string
RoleCode string
ActionCategory string
ActionType string
ActionTarget string
Success *bool
StatusMin *int
StatusMax *int
DateFrom *time.Time
DateTo *time.Time
Page int
Limit int
}
type ActivityLogResult struct {
Total int64 `json:"total"`
Items []ActivityLogRow `json:"items"`
}
func ListActivityLogs(
ctx context.Context,
db *sql.DB,
q ActivityLogQuery,
) (ActivityLogResult, error) {
// ---------- defaults ----------
if q.Page <= 0 {
q.Page = 1
}
// limit <=0 → unlimited
if q.Limit < 0 {
q.Limit = 50
}
if q.Limit > 200 {
q.Limit = 200
}
offset := 0
if q.Limit > 0 {
offset = (q.Page - 1) * q.Limit
}
where := []string{}
args := []interface{}{}
add := func(cond string, val interface{}) {
where = append(where, cond)
args = append(args, val)
}
// ---------- filters ----------
if strings.TrimSpace(q.Username) != "" {
add(fmt.Sprintf("l.username ILIKE $%d", len(args)+1),
"%"+strings.TrimSpace(q.Username)+"%")
}
if strings.TrimSpace(q.RoleCode) != "" {
add(fmt.Sprintf("r.code = $%d", len(args)+1),
strings.TrimSpace(q.RoleCode))
}
if strings.TrimSpace(q.ActionCategory) != "" {
add(fmt.Sprintf("l.action_category = $%d", len(args)+1),
strings.TrimSpace(q.ActionCategory))
}
if strings.TrimSpace(q.ActionType) != "" {
add(fmt.Sprintf("l.action_type = $%d", len(args)+1),
strings.TrimSpace(q.ActionType))
}
if strings.TrimSpace(q.ActionTarget) != "" {
add(fmt.Sprintf("l.action_target ILIKE $%d", len(args)+1),
"%"+strings.TrimSpace(q.ActionTarget)+"%")
}
if q.Success != nil {
add(fmt.Sprintf("l.is_success = $%d", len(args)+1), *q.Success)
}
if q.StatusMin != nil {
add(fmt.Sprintf("l.http_status >= $%d", len(args)+1), *q.StatusMin)
}
if q.StatusMax != nil {
add(fmt.Sprintf("l.http_status <= $%d", len(args)+1), *q.StatusMax)
}
if q.DateFrom != nil {
add(fmt.Sprintf("l.created_at >= $%d", len(args)+1), *q.DateFrom)
}
if q.DateTo != nil {
add(fmt.Sprintf("l.created_at < $%d", len(args)+1),
q.DateTo.Add(24*time.Hour))
}
whereSQL := ""
if len(where) > 0 {
whereSQL = "WHERE " + strings.Join(where, " AND ")
}
// ---------- COUNT ----------
countSQL := fmt.Sprintf(`
SELECT count(*)
FROM mk_user_activity_log l
LEFT JOIN mk_dfusr u ON u.id = l.dfusr_id
LEFT JOIN dfrole_usr ru ON ru.dfusr_id = u.id
LEFT JOIN dfrole r ON r.id = ru.dfrole_id
%s
`, whereSQL)
var total int64
if err := db.QueryRowContext(ctx, countSQL, args...).Scan(&total); err != nil {
return ActivityLogResult{}, err
}
// ---------- LIST ----------
listArgs := append([]interface{}{}, args...)
listSQL := fmt.Sprintf(`
SELECT
l.created_at,
l.request_started_at,
l.request_finished_at,
l.duration_ms,
l.http_status,
COALESCE(u.username, l.username, '') AS username,
COALESCE(r.code,''),
COALESCE(l.action_category,''),
COALESCE(l.action_type,''),
COALESCE(l.action_target,''),
COALESCE(l.description,''),
COALESCE(l.target_dfusr_id, 0),
COALESCE(l.target_username, ''),
COALESCE(l.change_before::text, ''),
COALESCE(l.change_after::text, ''),
COALESCE(l.ip_address,''),
COALESCE(l.is_success,false),
COALESCE(l.error_message,''),
COALESCE(l.user_agent,''),
COALESCE(l.session_id,''),
COALESCE(l.user_id::text,''),
COALESCE(l.dfusr_id,0),
COALESCE(u.email,''),
COALESCE(u.is_active,false)
FROM mk_user_activity_log l
LEFT JOIN mk_dfusr u ON u.id = l.dfusr_id
LEFT JOIN dfrole_usr ru ON ru.dfusr_id = u.id
LEFT JOIN dfrole r ON r.id = ru.dfrole_id
%s
ORDER BY l.created_at DESC
`, whereSQL)
// LIMIT sadece istenirse
if q.Limit > 0 {
listSQL += fmt.Sprintf(
" LIMIT $%d OFFSET $%d",
len(listArgs)+1,
len(listArgs)+2,
)
listArgs = append(listArgs, q.Limit, offset)
}
rows, err := db.QueryContext(ctx, listSQL, listArgs...)
if err != nil {
return ActivityLogResult{}, err
}
defer rows.Close()
items := []ActivityLogRow{}
for rows.Next() {
var it ActivityLogRow
var started, finished sql.NullTime
var dur, status sql.NullInt64
if err := rows.Scan(
&it.CreatedAt,
&started,
&finished,
&dur,
&status,
&it.Username,
&it.RoleCode,
&it.ActionCategory,
&it.ActionType,
&it.ActionTarget,
&it.Description,
// ✅ NEW
&it.TargetDfUsrID,
&it.TargetUsername,
&it.ChangeBefore,
&it.ChangeAfter,
&it.IpAddress,
&it.IsSuccess,
&it.ErrorMsg,
&it.UserAgent,
&it.SessionID,
&it.UserUUID,
&it.DfUsrID,
&it.Email,
&it.IsActive,
); err != nil {
return ActivityLogResult{}, err
}
if started.Valid {
it.RequestStartedAt = &started.Time
}
if finished.Valid {
it.RequestFinishedAt = &finished.Time
}
if dur.Valid {
v := int(dur.Int64)
it.DurationMs = &v
}
if status.Valid {
v := int(status.Int64)
it.HttpStatus = &v
}
items = append(items, it)
}
return ActivityLogResult{
Total: total,
Items: items,
}, nil
}