package api import ( "net/http" "time" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore" "github.com/grafana/grafana/pkg/services/anonymous/sortopts" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) const anonymousDeviceExpiration = 30 * 24 * time.Hour type deviceDTO struct { anonstore.Device LastSeenAt string `json:"lastSeenAt"` AvatarUrl string `json:"avatarUrl"` } type AnonDeviceServiceAPI struct { cfg *setting.Cfg store anonstore.AnonStore accesscontrol accesscontrol.AccessControl RouterRegister routing.RouteRegister log log.Logger } func NewAnonDeviceServiceAPI( cfg *setting.Cfg, anonstore anonstore.AnonStore, accesscontrol accesscontrol.AccessControl, routerRegister routing.RouteRegister, ) *AnonDeviceServiceAPI { return &AnonDeviceServiceAPI{ cfg: cfg, store: anonstore, accesscontrol: accesscontrol, RouterRegister: routerRegister, log: log.New("anon.api"), } } func (api *AnonDeviceServiceAPI) RegisterAPIEndpoints() { auth := accesscontrol.Middleware(api.accesscontrol) api.RouterRegister.Group("/api/anonymous", func(anonRoutes routing.RouteRegister) { anonRoutes.Get("/devices", auth(accesscontrol.EvalPermission(accesscontrol.ActionUsersRead)), routing.Wrap(api.ListDevices)) anonRoutes.Get("/search", auth(accesscontrol.EvalPermission(accesscontrol.ActionUsersRead)), routing.Wrap(api.SearchDevices)) }) } // swagger:route GET /stats devices listDevices // // # Lists all devices within the last 30 days // // Produces: // - application/json // // Responses: // // 200: devicesResponse // 401: unauthorisedError // 403: forbiddenError // 404: notFoundError // 500: internalServerError func (api *AnonDeviceServiceAPI) ListDevices(c *contextmodel.ReqContext) response.Response { fromTime := time.Now().Add(-anonymousDeviceExpiration) toTime := time.Now() devices, err := api.store.ListDevices(c.Req.Context(), &fromTime, &toTime) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to list devices", err) } // convert to response format resDevices := make([]*deviceDTO, 0, len(devices)) for _, device := range devices { resDevices = append(resDevices, &deviceDTO{ Device: *device, LastSeenAt: util.GetAgeString(device.UpdatedAt), AvatarUrl: dtos.GetGravatarUrl(api.cfg, device.DeviceID), }) } return response.JSON(http.StatusOK, resDevices) } // swagger:route POST /search devices SearchDevices // // # Lists all devices within the last 30 days // // Produces: // - application/json // // Responses: // // 200: devicesSearchResponse // 401: unauthorisedError // 403: forbiddenError // 404: notFoundError // 500: internalServerError func (api *AnonDeviceServiceAPI) SearchDevices(c *contextmodel.ReqContext) response.Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 100 } page := c.QueryInt("page") if page < 1 { page = 1 } searchQuery := c.Query("query") sortOpts, err := sortopts.ParseSortQueryParam(c.Query("sort")) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to list devices", err) } // TODO: potential add from and to time to query query := &anonstore.SearchDeviceQuery{ Query: searchQuery, Page: page, Limit: perPage, SortOpts: sortOpts, } results, err := api.store.SearchDevices(c.Req.Context(), query) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to list devices", err) } return response.JSON(http.StatusOK, results) } // swagger:response devicesResponse type DevicesResponse struct { // in:body Body []deviceDTO `json:"body"` } // swagger:response devicesSearchResponse type DevicesSearchResponse struct { // in:body Body anonstore.SearchDeviceQueryResult `json:"body"` }