244 lines
7.0 KiB
Go
244 lines
7.0 KiB
Go
package activity
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/apperr"
|
|
"RenewCMS/api/pkg/claims"
|
|
validator "RenewCMS/api/pkg/validator"
|
|
"RenewCMS/internal/domain/audit"
|
|
"RenewCMS/internal/domain/user"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
csvContentType = "text/csv; charset=utf-8"
|
|
csvContentDisposition = `attachment; filename="activity.csv"`
|
|
)
|
|
|
|
type Handler struct {
|
|
ListUseCase *useCases.ListActivityUseCase
|
|
SummaryUseCase *useCases.GetActivitySummaryUseCase
|
|
ExportUseCase *useCases.ExportActivityUseCase
|
|
}
|
|
|
|
type filterQuery struct {
|
|
Q string `validate:"max=200"`
|
|
Actor string `validate:"max=40"`
|
|
Category string `validate:"omitempty,oneof=articles media users api_keys auth system"`
|
|
Outcome string `validate:"omitempty,oneof=all success failed"`
|
|
Security string `validate:"omitempty,oneof=true"`
|
|
Range string `validate:"omitempty,oneof=today 7d 30d all custom"`
|
|
From string `validate:"max=10"`
|
|
To string `validate:"max=10"`
|
|
}
|
|
|
|
type listQuery struct {
|
|
filterQuery
|
|
Page int `validate:"gte=1"`
|
|
Size int `validate:"oneof=5 25 50 100"`
|
|
}
|
|
|
|
type actorResponse struct {
|
|
Type audit.ActorType `json:"type"`
|
|
ID *uint32 `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type targetResponse struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Exists bool `json:"exists"`
|
|
}
|
|
|
|
type itemResponse struct {
|
|
ID uint64 `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Action string `json:"action"`
|
|
Category audit.Category `json:"category"`
|
|
Actor actorResponse `json:"actor"`
|
|
Target *targetResponse `json:"target"`
|
|
Outcome audit.Outcome `json:"outcome"`
|
|
FailureReason *string `json:"failure_reason"`
|
|
Security bool `json:"security"`
|
|
Source audit.Source `json:"source"`
|
|
IP *string `json:"ip"`
|
|
UserAgent *string `json:"user_agent"`
|
|
Params map[string]any `json:"params"`
|
|
Changes []audit.Change `json:"changes"`
|
|
}
|
|
|
|
type listResponse struct {
|
|
Items []itemResponse `json:"items"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
Size int `json:"size"`
|
|
}
|
|
|
|
func NewHandler(list *useCases.ListActivityUseCase, summary *useCases.GetActivitySummaryUseCase, export *useCases.ExportActivityUseCase) *Handler {
|
|
return &Handler{ListUseCase: list, SummaryUseCase: summary, ExportUseCase: export}
|
|
}
|
|
|
|
func readFilterQuery(values url.Values) filterQuery {
|
|
return filterQuery{
|
|
Q: values.Get("q"),
|
|
Actor: values.Get("actor"),
|
|
Category: values.Get("category"),
|
|
Outcome: values.Get("outcome"),
|
|
Security: values.Get("security"),
|
|
Range: values.Get("range"),
|
|
From: values.Get("from"),
|
|
To: values.Get("to"),
|
|
}
|
|
}
|
|
|
|
func readListQuery(values url.Values) (listQuery, error) {
|
|
page, err := intParam(values, "page", 1)
|
|
if err != nil {
|
|
return listQuery{}, err
|
|
}
|
|
size, err := intParam(values, "size", 25)
|
|
if err != nil {
|
|
return listQuery{}, err
|
|
}
|
|
return listQuery{filterQuery: readFilterQuery(values), Page: page, Size: size}, nil
|
|
}
|
|
|
|
func intParam(values url.Values, name string, fallback int) (int, error) {
|
|
if !values.Has(name) {
|
|
return fallback, nil
|
|
}
|
|
return strconv.Atoi(values.Get(name))
|
|
}
|
|
|
|
func toActivityQuery(query filterQuery) useCases.ActivityQuery {
|
|
return useCases.ActivityQuery{
|
|
Q: query.Q,
|
|
Actor: query.Actor,
|
|
Category: query.Category,
|
|
Outcome: query.Outcome,
|
|
Security: query.Security,
|
|
Range: query.Range,
|
|
From: query.From,
|
|
To: query.To,
|
|
}
|
|
}
|
|
|
|
func writeActivityError(w http.ResponseWriter, err error) {
|
|
switch {
|
|
case errors.Is(err, audit.ErrInvalidFilter):
|
|
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
|
case errors.Is(err, audit.ErrActorNotAllowed):
|
|
http.Error(w, apperr.OwnerOrAdminRequiredErrorMessage, http.StatusForbidden)
|
|
case errors.Is(err, user.ErrUserNotFound):
|
|
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
|
default:
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
|
query, err := readListQuery(r.URL.Query())
|
|
if err != nil || validator.Validate.Struct(query) != nil {
|
|
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
viewerId, ok := claims.UserId(r)
|
|
if !ok {
|
|
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
activityQuery := toActivityQuery(query.filterQuery)
|
|
activityQuery.Page, activityQuery.Size = query.Page, query.Size
|
|
page, err := h.ListUseCase.ListActivity(useCases.ListActivityCommand{ViewerID: viewerId, Query: activityQuery})
|
|
if err != nil {
|
|
writeActivityError(w, err)
|
|
return
|
|
}
|
|
|
|
items := make([]itemResponse, 0, len(page.Items))
|
|
for _, entry := range page.Items {
|
|
items = append(items, toItemResponse(entry))
|
|
}
|
|
writeJSON(w, listResponse{Items: items, Total: page.Total, Page: page.Page, Size: page.Size})
|
|
}
|
|
|
|
func toItemResponse(entry useCases.ActivityEntry) itemResponse {
|
|
event := entry.Event
|
|
item := itemResponse{
|
|
ID: event.ID,
|
|
CreatedAt: event.CreatedAt,
|
|
Action: event.Action,
|
|
Category: event.Category,
|
|
Actor: actorResponse{Type: event.Actor.Type, ID: event.Actor.ID, Name: event.Actor.Name},
|
|
Outcome: event.Outcome,
|
|
FailureReason: event.FailureReason,
|
|
Security: event.Security,
|
|
Source: event.Source,
|
|
IP: event.IP,
|
|
UserAgent: event.UserAgent,
|
|
Params: event.Params,
|
|
Changes: event.Changes,
|
|
}
|
|
if item.Params == nil {
|
|
item.Params = map[string]any{}
|
|
}
|
|
if item.Changes == nil {
|
|
item.Changes = []audit.Change{}
|
|
}
|
|
if event.Target.Type != "" {
|
|
item.Target = &targetResponse{Type: event.Target.Type, ID: event.Target.ID, Name: event.Target.Name, Exists: entry.TargetExists}
|
|
}
|
|
return item
|
|
}
|
|
|
|
func (h *Handler) Summary(w http.ResponseWriter, r *http.Request) {
|
|
query := readFilterQuery(r.URL.Query())
|
|
if validator.Validate.Struct(query) != nil {
|
|
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
summary, err := h.SummaryUseCase.GetActivitySummary(toActivityQuery(query))
|
|
if err != nil {
|
|
writeActivityError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, summary)
|
|
}
|
|
|
|
func (h *Handler) Export(w http.ResponseWriter, r *http.Request) {
|
|
query := readFilterQuery(r.URL.Query())
|
|
if validator.Validate.Struct(query) != nil {
|
|
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
prepared, err := h.ExportUseCase.Prepare(toActivityQuery(query))
|
|
if err != nil {
|
|
writeActivityError(w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", csvContentType)
|
|
w.Header().Set("Content-Disposition", csvContentDisposition)
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
_ = prepared.WriteCSV(w)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, payload any) {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
_, _ = w.Write(body)
|
|
}
|