diff --git a/api/system/spec.json b/api/system/spec.json index ec5f5d24d..8bdfc4b8b 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -1759,7 +1759,6 @@ } ] }, - { "title": "Statistics", "entrypoint": "stats", @@ -1863,5 +1862,63 @@ } } ] + }, + { + "title": "Action log", + "entrypoint": "actionlog", + "path": "/actionlog", + "struct": [ + { + "imports": [ + "time" + ] + } + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "Action log events", + "path": "/", + "parameters": { + "get": [ + { + "name": "from", + "type": "*time.Time", + "required": false, + "title": "From" + }, + { + "name": "to", + "type": "*time.Time", + "required": false, + "title": "To" + }, + { + "name": "resource", + "required": false, + "title": "Resource", + "type": "string" + }, + { + "name": "action", + "required": false, + "title": "Action", + "type": "string" + }, + { + "name": "actorID", + "required": false, + "title": "Filter by one or more actors", + "type": "[]string" + }, + {"type": "uint", "name": "limit", "title": "Limit"}, + {"type": "uint", "name": "offset", "title": "Offset"}, + {"type": "uint", "name": "page", "title": "Page number (1-based)"}, + {"type": "uint", "name": "perPage", "title": "Returned items per page (default 50)"} + ] + } + } + ] } ] diff --git a/api/system/spec/actionlog.json b/api/system/spec/actionlog.json new file mode 100644 index 000000000..93be252e5 --- /dev/null +++ b/api/system/spec/actionlog.json @@ -0,0 +1,77 @@ +{ + "Title": "Action log", + "Interface": "Actionlog", + "Struct": [ + { + "imports": [ + "time" + ] + } + ], + "Parameters": null, + "Protocol": "", + "Authentication": null, + "Path": "/actionlog", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "Action log events", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "from", + "required": false, + "title": "From", + "type": "*time.Time" + }, + { + "name": "to", + "required": false, + "title": "To", + "type": "*time.Time" + }, + { + "name": "resource", + "required": false, + "title": "Resource", + "type": "string" + }, + { + "name": "action", + "required": false, + "title": "Action", + "type": "string" + }, + { + "name": "actorID", + "required": false, + "title": "Filter by one or more actors", + "type": "[]string" + }, + { + "name": "limit", + "title": "Limit", + "type": "uint" + }, + { + "name": "offset", + "title": "Offset", + "type": "uint" + }, + { + "name": "page", + "title": "Page number (1-based)", + "type": "uint" + }, + { + "name": "perPage", + "title": "Returned items per page (default 50)", + "type": "uint" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/docs/system/README.md b/docs/system/README.md index 0e1f4eee6..a349162be 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -1,3 +1,38 @@ +# Action log + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/actionlog/` | Action log events | + +## Action log events + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/actionlog/` | HTTP/S | GET | +Warning: implode(): Invalid arguments passed in /private/tmp/Users/darh/Work.crust/corteza-server/codegen/templates/README.tpl on line 32 + | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| from | *time.Time | GET | From | N/A | NO | +| to | *time.Time | GET | To | N/A | NO | +| resource | string | GET | Resource | N/A | NO | +| action | string | GET | Action | N/A | NO | +| actorID | []string | GET | Filter by one or more actors | N/A | NO | +| limit | uint | GET | Limit | N/A | NO | +| offset | uint | GET | Offset | N/A | NO | +| page | uint | GET | Page number (1-based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | + +--- + + + + # Applications | Method | Endpoint | Purpose | diff --git a/system/rest/actionlog.go b/system/rest/actionlog.go new file mode 100644 index 000000000..48c4887da --- /dev/null +++ b/system/rest/actionlog.go @@ -0,0 +1,96 @@ +package rest + +import ( + "context" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/cortezaproject/corteza-server/pkg/rh" + "github.com/cortezaproject/corteza-server/system/rest/request" + "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + Actionlog struct { + actionSvc actionlog.Recorder + userSvc service.UserService + } + + // Extend actionlog.Action so we can + // provide user's email + actionlogActionPayload struct { + *actionlog.Action + Actor string `json:"actor,omitempty"` + } + + actionlogPayload struct { + Filter actionlog.Filter `json:"filter"` + Set []*actionlogActionPayload `json:"set"` + } +) + +func (Actionlog) New() *Actionlog { + return &Actionlog{ + actionSvc: service.DefaultActionlog, + userSvc: service.DefaultUser, + } +} + +func (ctrl *Actionlog) List(ctx context.Context, r *request.ActionlogList) (interface{}, error) { + ee, f, err := ctrl.actionSvc.Find(ctx, actionlog.Filter{ + From: r.From, + To: r.To, + ActorID: payload.ParseUInt64s(r.ActorID), + Resource: r.Resource, + PageFilter: rh.Paging(r), + }) + + return ctrl.makeFilterPayload(ctx, ee, f, err) +} + +func (ctrl Actionlog) makeFilterPayload(ctx context.Context, ee []*actionlog.Action, f actionlog.Filter, err error) (*actionlogPayload, error) { + if err != nil { + return nil, err + } + + var ( + pp = make([]*actionlogActionPayload, len(ee)) + ) + + // Remap events to payload structs + for e := range ee { + pp[e] = &actionlogActionPayload{Action: ee[e]} + } + + err = ctrl.userSvc.With(ctx).Preloader( + func(c chan uint64) { + for e := range ee { + c <- ee[e].ActorID + } + + close(c) + }, + types.UserFilter{ + Deleted: rh.FilterStateInclusive, + Suspended: rh.FilterStateInclusive, + }, + func(u *types.User) error { + for p := range pp { + if pp[p].ActorID == u.ID { + pp[p].Actor = u.Name + if pp[p].Actor == "" { + pp[p].Actor = u.Email + } + } + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return &actionlogPayload{Filter: f, Set: pp}, nil +} diff --git a/system/rest/handlers/actionlog.go b/system/rest/handlers/actionlog.go new file mode 100644 index 000000000..99144cbaa --- /dev/null +++ b/system/rest/handlers/actionlog.go @@ -0,0 +1,70 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `actionlog.go`, `actionlog.util.go` or `actionlog_test.go` to + implement your API calls, helper functions and tests. The file `actionlog.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/system/rest/request" +) + +// Internal API interface +type ActionlogAPI interface { + List(context.Context, *request.ActionlogList) (interface{}, error) +} + +// HTTP API interface +type Actionlog struct { + List func(http.ResponseWriter, *http.Request) +} + +func NewActionlog(h ActionlogAPI) *Actionlog { + return &Actionlog{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewActionlogList() + if err := params.Fill(r); err != nil { + logger.LogParamError("Actionlog.List", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.List(r.Context(), params) + if err != nil { + logger.LogControllerError("Actionlog.List", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Actionlog.List", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h Actionlog) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/actionlog/", h.List) + }) +} diff --git a/system/rest/request/actionlog.go b/system/rest/request/actionlog.go new file mode 100644 index 000000000..66df68b95 --- /dev/null +++ b/system/rest/request/actionlog.go @@ -0,0 +1,320 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `actionlog.go`, `actionlog.util.go` or `actionlog_test.go` to + implement your API calls, helper functions and tests. The file `actionlog.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" + + "time" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// ActionlogList request parameters +type ActionlogList struct { + hasFrom bool + rawFrom string + From *time.Time + + hasTo bool + rawTo string + To *time.Time + + hasResource bool + rawResource string + Resource string + + hasAction bool + rawAction string + Action string + + hasActorID bool + rawActorID []string + ActorID []string + + hasLimit bool + rawLimit string + Limit uint + + hasOffset bool + rawOffset string + Offset uint + + hasPage bool + rawPage string + Page uint + + hasPerPage bool + rawPerPage string + PerPage uint +} + +// NewActionlogList request +func NewActionlogList() *ActionlogList { + return &ActionlogList{} +} + +// Auditable returns all auditable/loggable parameters +func (r ActionlogList) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["from"] = r.From + out["to"] = r.To + out["resource"] = r.Resource + out["action"] = r.Action + out["actorID"] = r.ActorID + out["limit"] = r.Limit + out["offset"] = r.Offset + out["page"] = r.Page + out["perPage"] = r.PerPage + + return out +} + +// Fill processes request and fills internal variables +func (r *ActionlogList) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["from"]; ok { + r.hasFrom = true + r.rawFrom = val + + if r.From, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } + if val, ok := get["to"]; ok { + r.hasTo = true + r.rawTo = val + + if r.To, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } + if val, ok := get["resource"]; ok { + r.hasResource = true + r.rawResource = val + r.Resource = val + } + if val, ok := get["action"]; ok { + r.hasAction = true + r.rawAction = val + r.Action = val + } + + if val, ok := urlQuery["actorID[]"]; ok { + r.hasActorID = true + r.rawActorID = val + r.ActorID = parseStrings(val) + } else if val, ok = urlQuery["actorID"]; ok { + r.hasActorID = true + r.rawActorID = val + r.ActorID = parseStrings(val) + } + + if val, ok := get["limit"]; ok { + r.hasLimit = true + r.rawLimit = val + r.Limit = parseUint(val) + } + if val, ok := get["offset"]; ok { + r.hasOffset = true + r.rawOffset = val + r.Offset = parseUint(val) + } + if val, ok := get["page"]; ok { + r.hasPage = true + r.rawPage = val + r.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + r.hasPerPage = true + r.rawPerPage = val + r.PerPage = parseUint(val) + } + + return err +} + +var _ RequestFiller = NewActionlogList() + +// HasFrom returns true if from was set +func (r *ActionlogList) HasFrom() bool { + return r.hasFrom +} + +// RawFrom returns raw value of from parameter +func (r *ActionlogList) RawFrom() string { + return r.rawFrom +} + +// GetFrom returns casted value of from parameter +func (r *ActionlogList) GetFrom() *time.Time { + return r.From +} + +// HasTo returns true if to was set +func (r *ActionlogList) HasTo() bool { + return r.hasTo +} + +// RawTo returns raw value of to parameter +func (r *ActionlogList) RawTo() string { + return r.rawTo +} + +// GetTo returns casted value of to parameter +func (r *ActionlogList) GetTo() *time.Time { + return r.To +} + +// HasResource returns true if resource was set +func (r *ActionlogList) HasResource() bool { + return r.hasResource +} + +// RawResource returns raw value of resource parameter +func (r *ActionlogList) RawResource() string { + return r.rawResource +} + +// GetResource returns casted value of resource parameter +func (r *ActionlogList) GetResource() string { + return r.Resource +} + +// HasAction returns true if action was set +func (r *ActionlogList) HasAction() bool { + return r.hasAction +} + +// RawAction returns raw value of action parameter +func (r *ActionlogList) RawAction() string { + return r.rawAction +} + +// GetAction returns casted value of action parameter +func (r *ActionlogList) GetAction() string { + return r.Action +} + +// HasActorID returns true if actorID was set +func (r *ActionlogList) HasActorID() bool { + return r.hasActorID +} + +// RawActorID returns raw value of actorID parameter +func (r *ActionlogList) RawActorID() []string { + return r.rawActorID +} + +// GetActorID returns casted value of actorID parameter +func (r *ActionlogList) GetActorID() []string { + return r.ActorID +} + +// HasLimit returns true if limit was set +func (r *ActionlogList) HasLimit() bool { + return r.hasLimit +} + +// RawLimit returns raw value of limit parameter +func (r *ActionlogList) RawLimit() string { + return r.rawLimit +} + +// GetLimit returns casted value of limit parameter +func (r *ActionlogList) GetLimit() uint { + return r.Limit +} + +// HasOffset returns true if offset was set +func (r *ActionlogList) HasOffset() bool { + return r.hasOffset +} + +// RawOffset returns raw value of offset parameter +func (r *ActionlogList) RawOffset() string { + return r.rawOffset +} + +// GetOffset returns casted value of offset parameter +func (r *ActionlogList) GetOffset() uint { + return r.Offset +} + +// HasPage returns true if page was set +func (r *ActionlogList) HasPage() bool { + return r.hasPage +} + +// RawPage returns raw value of page parameter +func (r *ActionlogList) RawPage() string { + return r.rawPage +} + +// GetPage returns casted value of page parameter +func (r *ActionlogList) GetPage() uint { + return r.Page +} + +// HasPerPage returns true if perPage was set +func (r *ActionlogList) HasPerPage() bool { + return r.hasPerPage +} + +// RawPerPage returns raw value of perPage parameter +func (r *ActionlogList) RawPerPage() string { + return r.rawPerPage +} + +// GetPerPage returns casted value of perPage parameter +func (r *ActionlogList) GetPerPage() uint { + return r.PerPage +} diff --git a/system/rest/router.go b/system/rest/router.go index 51f00c298..235526cd8 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -38,5 +38,6 @@ func MountRoutes(r chi.Router) { handlers.NewSettings(Settings{}.New()).MountRoutes(r) handlers.NewStats(Stats{}.New()).MountRoutes(r) handlers.NewReminder(Reminder{}.New()).MountRoutes(r) + handlers.NewActionlog(Actionlog{}.New()).MountRoutes(r) }) }