diff --git a/api/Makefile b/api/Makefile index 6660b91935..83280e6c11 100644 --- a/api/Makefile +++ b/api/Makefile @@ -50,6 +50,7 @@ build-v4: node_modules playbooks @cat $(V4_SRC)/imports.yaml >> $(V4_YAML) @cat $(V4_SRC)/exports.yaml >> $(V4_YAML) @cat $(V4_SRC)/ip_filters.yaml >> $(V4_YAML) + @cat $(V4_SRC)/reports.yaml >> $(V4_YAML) @if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi @if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi @echo Extracting code samples diff --git a/api/v4/source/reports.yaml b/api/v4/source/reports.yaml new file mode 100644 index 0000000000..83e335155d --- /dev/null +++ b/api/v4/source/reports.yaml @@ -0,0 +1,94 @@ + /api/v4/reports/users: + get: + tags: + - users + summary: Get a list of paged and sorted users for admin reporting purposes + description: > + Get a list of paged users for admin reporting purposes, based on provided parameters. + + Must be a system admin to invoke this API. + + ##### Permissions + + Requires `sysconsole_read_user_management_users`. + + operationId: GetUsersForReporting + parameters: + - name: sort_column + in: query + description: The column to sort the users by. Must be one of ("CreateAt", "Username", "FirstName", "LastName", "Nickname", "Email") or the API will return an error. + schema: + type: string + default: 'Username' + - name: sort_direction + in: query + description: The sorting direction. Must be one of ("asc", "desc"). Will default to 'asc' if not specified or the input is invalid. + schema: + type: string + default: 'asc' + - name: page_size + in: query + description: The maximum number of users to return. + schema: + type: integer + default: 50 + minimum: 1 + maximum: 100 + - name: last_column_value + in: query + description: The value of the sorted column belonging to the last user returned in the page. Should be blank for the first page asked for. + schema: + type: string + - name: last_id + in: query + description: The value of the user id belonging to the last user returned in the page. Should be blank for the first page asked for. + schema: + type: string + - name: date_range + in: query + description: The date range of the post statistics to display. Must be one of ("last30days", "previousmonth", "last6months", "alltime"). Will default to 'alltime' if the input is not valid. + schema: + type: string + default: 'alltime' + - name: role_filter + in: query + description: Filter users by their role. + schema: + type: string + - name: team_filter + in: query + description: Filter users by a specified team ID. + schema: + type: string + - name: has_no_team + in: query + description: If true, show only users that have no team. Will ignore provided "team_filter" if true. + schema: + type: boolean + - name: hide_active + in: query + description: If true, show only users that are inactive. Cannot be used at the same time as "hide_inactive" + schema: + type: boolean + - name: hide_inactive + in: query + description: If true, show only users that are active. Cannot be used at the same time as "hide_active" + schema: + type: boolean + responses: + "200": + description: User page retrieval successful + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UserReport" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" diff --git a/api/v4/source/users.yaml b/api/v4/source/users.yaml index e2c5b803ad..44aaf4b72b 100644 --- a/api/v4/source/users.yaml +++ b/api/v4/source/users.yaml @@ -3251,97 +3251,3 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" - /api/v4/users/report: - get: - tags: - - users - summary: Get a list of paged and sorted users for admin reporting purposes - description: > - Get a list of paged users for admin reporting purposes, based on provided parameters. - - Must be a system admin to invoke this API. - - ##### Permissions - - Requires `sysconsole_read_user_management_users`. - - operationId: GetUsersForReporting - parameters: - - name: sort_column - in: query - description: The column to sort the users by. Must be one of ("CreateAt", "Username", "FirstName", "LastName", "Nickname", "Email") or the API will return an error. - schema: - type: string - default: 'Username' - - name: sort_direction - in: query - description: The sorting direction. Must be one of ("asc", "desc"). Will default to 'asc' if not specified or the input is invalid. - schema: - type: string - default: 'asc' - - name: page_size - in: query - description: The maximum number of users to return. - schema: - type: integer - default: 50 - minimum: 1 - maximum: 100 - - name: last_column_value - in: query - description: The value of the sorted column belonging to the last user returned in the page. Should be blank for the first page asked for. - schema: - type: string - - name: last_id - in: query - description: The value of the user id belonging to the last user returned in the page. Should be blank for the first page asked for. - schema: - type: string - - name: date_range - in: query - description: The date range of the post statistics to display. Must be one of ("last30days", "previousmonth", "last6months", "alltime"). Will default to 'alltime' if the input is not valid. - schema: - type: string - default: 'alltime' - - name: role_filter - in: query - description: Filter users by their role. - schema: - type: string - - name: team_filter - in: query - description: Filter users by a specified team ID. - schema: - type: string - - name: has_no_team - in: query - description: If true, show only users that have no team. Will ignore provided "team_filter" if true. - schema: - type: boolean - - name: hide_active - in: query - description: If true, show only users that are inactive. Cannot be used at the same time as "hide_inactive" - schema: - type: boolean - - name: hide_inactive - in: query - description: If true, show only users that are active. Cannot be used at the same time as "hide_active" - schema: - type: boolean - responses: - "200": - description: User page retrieval successful - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/UserReport" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "500": - $ref: "#/components/responses/InternalServerError" diff --git a/server/channels/api4/api.go b/server/channels/api4/api.go index c6a6957981..0d5d762d8b 100644 --- a/server/channels/api4/api.go +++ b/server/channels/api4/api.go @@ -139,6 +139,8 @@ type Routes struct { Drafts *mux.Router // 'api/v4/drafts' IPFiltering *mux.Router // 'api/v4/ip_filtering' + + Reports *mux.Router // 'api/v4/reports' } type API struct { @@ -265,6 +267,8 @@ func Init(srv *app.Server) (*API, error) { api.BaseRoutes.IPFiltering = api.BaseRoutes.APIRoot.PathPrefix("/ip_filtering").Subrouter() + api.BaseRoutes.Reports = api.BaseRoutes.APIRoot.PathPrefix("/reports").Subrouter() + api.InitUser() api.InitBot() api.InitTeam() @@ -309,6 +313,7 @@ func Init(srv *app.Server) (*API, error) { api.InitHostedCustomer() api.InitDrafts() api.InitIPFiltering() + api.InitReports() srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404)) diff --git a/server/channels/api4/report.go b/server/channels/api4/report.go new file mode 100644 index 0000000000..b824457560 --- /dev/null +++ b/server/channels/api4/report.go @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" +) + +func (api *API) InitReports() { + api.BaseRoutes.Reports.Handle("/users", api.APISessionRequired(getUsersForReporting)).Methods("GET") +} + +func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) { + if !(c.IsSystemAdmin() && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers)) { + c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers) + return + } + + sortColumn := "Username" + if r.URL.Query().Get("sort_column") != "" { + sortColumn = r.URL.Query().Get("sort_column") + } + + pageSize := 50 + if pageSizeStr, err := strconv.ParseInt(r.URL.Query().Get("page_size"), 10, 64); err == nil { + pageSize = int(pageSizeStr) + } + + teamFilter := r.URL.Query().Get("team_filter") + if !(teamFilter == "" || model.IsValidId(teamFilter)) { + c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_team_filter", nil, "", http.StatusBadRequest) + return + } + + hideActive := r.URL.Query().Get("hide_active") == "true" + hideInactive := r.URL.Query().Get("hide_inactive") == "true" + if hideActive && hideInactive { + c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_active_filter", nil, "", http.StatusBadRequest) + return + } + + options := &model.UserReportOptions{ + ReportingBaseOptions: model.ReportingBaseOptions{ + SortColumn: sortColumn, + SortDesc: r.URL.Query().Get("sort_direction") == "desc", + PageSize: pageSize, + LastSortColumnValue: r.URL.Query().Get("last_column_value"), + DateRange: r.URL.Query().Get("date_range"), + }, + Team: teamFilter, + LastUserId: r.URL.Query().Get("last_id"), + Role: r.URL.Query().Get("role_filter"), + HasNoTeam: r.URL.Query().Get("has_no_team") == "true", + HideActive: hideActive, + HideInactive: hideInactive, + } + options.PopulateDateRange(time.Now()) + + // Don't allow fetching more than 100 users at a time from the normal query endpoint + if options.PageSize <= 0 || options.PageSize > model.ReportingMaxPageSize { + c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_page_size", nil, "", http.StatusBadRequest) + return + } + + userReports, err := c.App.GetUsersForReporting(options) + if err != nil { + c.Err = err + return + } + + if jsonErr := json.NewEncoder(w).Encode(userReports); jsonErr != nil { + c.Logger.Warn("Error writing response", mlog.Err(jsonErr)) + } +} diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index 94333fa28b..3c1dfff738 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -108,8 +108,6 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST") api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods("POST") - - api.BaseRoutes.Users.Handle("/report", api.APISessionRequired(getUsersForReporting)).Methods("GET") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -3445,59 +3443,3 @@ func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Reques c.Logger.Warn("Error writing response", mlog.Err(err)) } } - -func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) { - if !(c.IsSystemAdmin() && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers)) { - c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers) - return - } - - sortColumn := "Username" - if r.URL.Query().Get("sort_column") != "" { - sortColumn = r.URL.Query().Get("sort_column") - } - - pageSize := 50 - if pageSizeStr, err := strconv.ParseInt(r.URL.Query().Get("page_size"), 10, 64); err == nil { - pageSize = int(pageSizeStr) - } - - teamFilter := r.URL.Query().Get("team_filter") - if !(teamFilter == "" || model.IsValidId(teamFilter)) { - c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_team_filter", nil, "", http.StatusBadRequest) - return - } - - hideActive := r.URL.Query().Get("hide_active") == "true" - hideInactive := r.URL.Query().Get("hide_inactive") == "true" - if hideActive && hideInactive { - c.Err = model.NewAppError("getUsersForReporting", "api.getUsersForReporting.invalid_active_filter", nil, "", http.StatusBadRequest) - return - } - - options := &model.UserReportOptionsAPI{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ - SortColumn: sortColumn, - SortDesc: r.URL.Query().Get("sort_direction") == "desc", - PageSize: pageSize, - Team: teamFilter, - LastSortColumnValue: r.URL.Query().Get("last_column_value"), - LastUserId: r.URL.Query().Get("last_id"), - Role: r.URL.Query().Get("role_filter"), - HasNoTeam: r.URL.Query().Get("has_no_team") == "true", - HideActive: hideActive, - HideInactive: hideInactive, - }, - DateRange: r.URL.Query().Get("date_range"), - } - - userReports, err := c.App.GetUsersForReporting(options.ToBaseOptions(time.Now())) - if err != nil { - c.Err = err - return - } - - if jsonErr := json.NewEncoder(w).Encode(userReports); jsonErr != nil { - c.Logger.Warn("Error writing response", mlog.Err(jsonErr)) - } -} diff --git a/server/channels/app/report.go b/server/channels/app/report.go new file mode 100644 index 0000000000..e86478e0fe --- /dev/null +++ b/server/channels/app/report.go @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "net/http" + + "github.com/mattermost/mattermost/server/public/model" +) + +func (a *App) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) { + if appErr := filter.IsValid(); appErr != nil { + return nil, appErr + } + + return a.getUserReport(filter) +} + +func (a *App) getUserReport(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) { + userReportQuery, err := a.Srv().Store().User().GetUserReport(filter) + if err != nil { + return nil, model.NewAppError("GetUsersForReporting", "app.report.get_user_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + userReports := make([]*model.UserReport, len(userReportQuery)) + for i, user := range userReportQuery { + userReports[i] = user.ToReport() + } + + return userReports, nil +} diff --git a/server/channels/app/user.go b/server/channels/app/user.go index 731c7e7864..336cda5987 100644 --- a/server/channels/app/user.go +++ b/server/channels/app/user.go @@ -24,7 +24,6 @@ import ( "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" - pUtils "github.com/mattermost/mattermost/server/public/utils" "github.com/mattermost/mattermost/server/v8/channels/app/email" "github.com/mattermost/mattermost/server/v8/channels/app/imaging" "github.com/mattermost/mattermost/server/v8/channels/app/users" @@ -2818,36 +2817,3 @@ func (a *App) UserIsFirstAdmin(user *model.User) bool { return true } - -func (a *App) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) { - // Don't allow fetching more than 100 users at a time from the normal query endpoint - if filter.PageSize <= 0 || filter.PageSize > 100 { - return nil, model.NewAppError("GetUsersForReporting", "app.user.get_users_for_reporting.invalid_page_size", nil, "", http.StatusBadRequest) - } - - // Validate date range - if filter.EndAt > 0 && filter.StartAt > filter.EndAt { - return nil, model.NewAppError("GetUsersForReporting", "app.user.get_users_for_reporting.bad_date_range", nil, "", http.StatusBadRequest) - } - - return a.getUserReport(filter) -} - -func (a *App) getUserReport(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) { - // Validate against the columns we allow sorting for - if !pUtils.Contains(model.UserReportSortColumns, filter.SortColumn) { - return nil, model.NewAppError("GetUsersForReporting", "app.user.get_user_report.invalid_sort_column", nil, "", http.StatusBadRequest) - } - - userReportQuery, err := a.Srv().Store().User().GetUserReport(filter) - if err != nil { - return nil, model.NewAppError("GetUsersForReporting", "app.user.get_user_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - userReports := make([]*model.UserReport, len(userReportQuery)) - for i, user := range userReportQuery { - userReports[i] = user.ToReport() - } - - return userReports, nil -} diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go index 7006d51c72..b20f0689b8 100644 --- a/server/channels/app/user_test.go +++ b/server/channels/app/user_test.go @@ -1934,31 +1934,17 @@ func TestSendSubscriptionHistoryEvent(t *testing.T) { } func TestGetUsersForReporting(t *testing.T) { - t.Run("should throw error on invalid page size", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ - SortColumn: "Username", - PageSize: 999, - }, - }) - require.Error(t, err) - require.Nil(t, userReports) - }) - t.Run("should throw error on invalid date range", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, + StartAt: 1000, + EndAt: 500, }, - StartAt: 1000, - EndAt: 500, }) require.Error(t, err) require.Nil(t, userReports) @@ -1969,7 +1955,7 @@ func TestGetUsersForReporting(t *testing.T) { defer th.TearDown() userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "FakeColumn", PageSize: 50, }, @@ -2015,7 +2001,7 @@ func TestGetUsersForReporting(t *testing.T) { mockStore.On("User").Return(&mockUserStore) userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, }, diff --git a/server/channels/store/storetest/user_store.go b/server/channels/store/storetest/user_store.go index cd82b272fd..7b64b1e450 100644 --- a/server/channels/store/storetest/user_store.go +++ b/server/channels/store/storetest/user_store.go @@ -6247,7 +6247,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should return info for all the users", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, }, @@ -6268,7 +6268,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should return in the correct order", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", SortDesc: true, PageSize: 50, @@ -6290,7 +6290,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should fail on invalid sort column", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "FakeColumn", SortDesc: true, PageSize: 50, @@ -6302,7 +6302,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should only return amount of users in page", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 2, }, @@ -6320,12 +6320,12 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should return correct paging", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, LastSortColumnValue: u2.Username, - LastUserId: u2.Id, }, + LastUserId: u2.Id, }) require.NoError(t, err) require.NotNil(t, userReport) @@ -6337,7 +6337,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should return accurate post stats for various date ranges", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, }, @@ -6356,11 +6356,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate) userReport, err = ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, + StartAt: now.AddDate(0, 0, -2).UnixMilli(), }, - StartAt: now.AddDate(0, 0, -2).UnixMilli(), }) require.NoError(t, err) require.Len(t, userReport, 3) @@ -6376,11 +6376,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate) userReport, err = ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, + EndAt: now.AddDate(0, 0, -2).UnixMilli(), }, - EndAt: now.AddDate(0, 0, -2).UnixMilli(), }) require.NoError(t, err) require.Len(t, userReport, 3) @@ -6396,12 +6396,12 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[2].LastPostDate) userReport, err = ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, + StartAt: now.AddDate(0, 0, -3).UnixMilli(), + EndAt: now.AddDate(0, 0, -2).UnixMilli(), }, - StartAt: now.AddDate(0, 0, -3).UnixMilli(), - EndAt: now.AddDate(0, 0, -2).UnixMilli(), }) require.NoError(t, err) require.Len(t, userReport, 3) @@ -6419,11 +6419,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should filter on roles", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, - Role: "system", }, + Role: "system", }) require.NoError(t, err) require.Len(t, userReport, 1) @@ -6433,11 +6433,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should filter on teams", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, - HasNoTeam: true, }, + HasNoTeam: true, }) require.NoError(t, err) require.Len(t, userReport, 2) @@ -6445,11 +6445,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { require.Equal(t, u2.Id, userReport[1].Id) userReport, err = ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, - Team: team.Id, }, + Team: team.Id, }) require.NoError(t, err) require.Len(t, userReport, 1) @@ -6458,11 +6458,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { t.Run("should filter on activation", func(t *testing.T) { userReport, err := ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ - SortColumn: "Username", - PageSize: 50, - HideInactive: true, + ReportingBaseOptions: model.ReportingBaseOptions{ + SortColumn: "Username", + PageSize: 50, }, + HideInactive: true, }) require.NoError(t, err) require.Len(t, userReport, 2) @@ -6470,11 +6470,11 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) { require.Equal(t, u2.Id, userReport[1].Id) userReport, err = ss.User().GetUserReport(&model.UserReportOptions{ - UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{ + ReportingBaseOptions: model.ReportingBaseOptions{ SortColumn: "Username", PageSize: 50, - HideActive: true, }, + HideActive: true, }) require.NoError(t, err) require.Len(t, userReport, 1) diff --git a/server/i18n/en.json b/server/i18n/en.json index 515fcbfd85..07885b806d 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2024,6 +2024,10 @@ "id": "api.getUsersForReporting.invalid_active_filter", "translation": "Cannot hide both active and inactive users." }, + { + "id": "api.getUsersForReporting.invalid_page_size", + "translation": "Page size is invalid or too large." + }, { "id": "api.getUsersForReporting.invalid_team_filter", "translation": "Invalid team id provided." @@ -6582,6 +6586,10 @@ "id": "app.recover.save.app_error", "translation": "Unable to save the token." }, + { + "id": "app.report.get_user_report.store_error", + "translation": "Failed to fetch user report." + }, { "id": "app.role.check_roles_exist.role_not_found", "translation": "The provided role does not exist" @@ -7098,26 +7106,10 @@ "id": "app.user.get_unread_count.app_error", "translation": "We could not get the unread message count for the user." }, - { - "id": "app.user.get_user_report.invalid_sort_column", - "translation": "Provided sort column is not valid." - }, - { - "id": "app.user.get_user_report.store_error", - "translation": "There was an error querying the user report." - }, { "id": "app.user.get_users_batch_for_indexing.get_users.app_error", "translation": "Unable to get the users batch for indexing." }, - { - "id": "app.user.get_users_for_reporting.bad_date_range", - "translation": "Date range provided is invalid." - }, - { - "id": "app.user.get_users_for_reporting.invalid_page_size", - "translation": "Page size is invalid or too large." - }, { "id": "app.user.missing_account.const", "translation": "Unable to find the user." @@ -9622,6 +9614,10 @@ "id": "model.reaction.is_valid.user_id.app_error", "translation": "Invalid user id." }, + { + "id": "model.reporting_base_options.is_valid.bad_date_range", + "translation": "Date range provided is invalid." + }, { "id": "model.search_params_list.is_valid.include_deleted_channels.app_error", "translation": "All IncludeDeletedChannels params should have the same value." @@ -9894,6 +9890,10 @@ "id": "model.user_access_token.is_valid.user_id.app_error", "translation": "Invalid user id." }, + { + "id": "model.user_report_options.is_valid.invalid_sort_column", + "translation": "Provided sort column is not valid." + }, { "id": "model.utils.decode_json.app_error", "translation": "could not decode." diff --git a/server/public/model/client4.go b/server/public/model/client4.go index 7cb4872095..17eea08cf0 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -166,6 +166,10 @@ func (c *Client4) usersRoute() string { return "/users" } +func (c *Client4) reportsRoute() string { + return "/reports" +} + func (c *Client4) userRoute(userId string) string { return fmt.Sprintf(c.usersRoute()+"/%v", userId) } @@ -1918,7 +1922,7 @@ func (c *Client4) EnableUserAccessToken(ctx context.Context, tokenId string) (*R return BuildResponse(r), nil } -func (c *Client4) GetUsersForReporting(ctx context.Context, options *UserReportOptionsAPI) ([]*UserReport, *Response, error) { +func (c *Client4) GetUsersForReporting(ctx context.Context, options *UserReportOptions) ([]*UserReport, *Response, error) { values := url.Values{} if options.SortColumn != "" { values.Set("sort_column", options.SortColumn) @@ -1954,7 +1958,7 @@ func (c *Client4) GetUsersForReporting(ctx context.Context, options *UserReportO values.Set("date_range", options.DateRange) } - r, err := c.DoAPIGet(ctx, c.usersRoute()+"/report?"+values.Encode(), "") + r, err := c.DoAPIGet(ctx, c.reportsRoute()+"/users?"+values.Encode(), "") if err != nil { return nil, BuildResponse(r), err } diff --git a/server/public/model/report.go b/server/public/model/report.go new file mode 100644 index 0000000000..687912da05 --- /dev/null +++ b/server/public/model/report.go @@ -0,0 +1,109 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "time" + + pUtils "github.com/mattermost/mattermost/server/public/utils" +) + +const ( + ReportDurationLast30Days = "last_30_days" + ReportDurationPreviousMonth = "previous_month" + ReportDurationLast6Months = "last_6_months" + + ReportingMaxPageSize = 100 +) + +var ( + UserReportSortColumns = []string{"CreateAt", "Username", "FirstName", "LastName", "Nickname", "Email", "Roles"} +) + +type ReportingBaseOptions struct { + SortDesc bool + PageSize int + SortColumn string + LastSortColumnValue string + DateRange string + StartAt int64 + EndAt int64 +} + +func (options *ReportingBaseOptions) PopulateDateRange(now time.Time) { + startAt := int64(0) + endAt := int64(0) + + if options.DateRange == ReportDurationLast30Days { + startAt = now.AddDate(0, 0, -30).UnixMilli() + } else if options.DateRange == ReportDurationPreviousMonth { + startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local) + startAt = startOfMonth.AddDate(0, -1, 0).UnixMilli() + endAt = startOfMonth.UnixMilli() + } else if options.DateRange == ReportDurationLast6Months { + startAt = now.AddDate(0, -6, -0).UnixMilli() + } + + options.StartAt = startAt + options.EndAt = endAt +} + +func (options *ReportingBaseOptions) IsValid() *AppError { + if options.EndAt > 0 && options.StartAt > options.EndAt { + return NewAppError("ReportingBaseOptions.IsValid", "model.reporting_base_options.is_valid.bad_date_range", nil, "", http.StatusBadRequest) + } + + return nil +} + +type UserReportQuery struct { + User + UserPostStats +} + +type UserReport struct { + Id string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + CreateAt int64 `json:"create_at,omitempty"` + DisplayName string `json:"display_name"` + Roles string `json:"roles"` + UserPostStats +} + +type UserReportOptions struct { + ReportingBaseOptions + LastUserId string + Role string + Team string + HasNoTeam bool + HideActive bool + HideInactive bool +} + +func (u *UserReportOptions) IsValid() *AppError { + if appErr := u.ReportingBaseOptions.IsValid(); appErr != nil { + return appErr + } + + // Validate against the columns we allow sorting for + if !pUtils.Contains(UserReportSortColumns, u.SortColumn) { + return NewAppError("UserReportOptions.IsValid", "model.user_report_options.is_valid.invalid_sort_column", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (u *UserReportQuery) ToReport() *UserReport { + return &UserReport{ + Id: u.Id, + Username: u.Username, + Email: u.Email, + CreateAt: u.CreateAt, + DisplayName: u.GetDisplayName(ShowNicknameFullName), + Roles: u.Roles, + UserPostStats: u.UserPostStats, + } +} diff --git a/server/public/model/user.go b/server/public/model/user.go index da99d04508..d29d282439 100644 --- a/server/public/model/user.go +++ b/server/public/model/user.go @@ -48,10 +48,6 @@ const ( PushThreadsNotifyProp = "push_threads" EmailThreadsNotifyProp = "email_threads" - ReportDurationLast30Days = "last_30_days" - ReportDurationPreviousMonth = "previous_month" - ReportDurationLast6Months = "last_6_months" - DefaultLocale = "en" UserAuthServiceEmail = "email" @@ -71,10 +67,6 @@ const ( DesktopTokenTTL = time.Minute * 3 ) -var ( - UserReportSortColumns = []string{"CreateAt", "Username", "FirstName", "LastName", "Nickname", "Email", "Roles"} -) - //msgp:tuple User // User contains the details about the user. @@ -1029,74 +1021,3 @@ type UserPostStats struct { DaysActive *int `json:"days_active,omitempty"` TotalPosts *int `json:"total_posts,omitempty"` } - -type UserReportQuery struct { - User - UserPostStats -} - -type UserReport struct { - Id string `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - CreateAt int64 `json:"create_at,omitempty"` - DisplayName string `json:"display_name"` - Roles string `json:"roles"` - UserPostStats -} - -type UserReportOptionsWithoutDateRange struct { - SortColumn string - SortDesc bool - PageSize int - LastSortColumnValue string - LastUserId string - Role string - Team string - HasNoTeam bool - HideActive bool - HideInactive bool -} - -type UserReportOptions struct { - UserReportOptionsWithoutDateRange - StartAt int64 - EndAt int64 -} - -type UserReportOptionsAPI struct { - UserReportOptionsWithoutDateRange - DateRange string -} - -func (u *UserReportOptionsAPI) ToBaseOptions(now time.Time) *UserReportOptions { - startAt := int64(0) - endAt := int64(0) - if u.DateRange == ReportDurationLast30Days { - startAt = now.AddDate(0, 0, -30).UnixMilli() - } else if u.DateRange == ReportDurationPreviousMonth { - startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local) - startAt = startOfMonth.AddDate(0, -1, 0).UnixMilli() - endAt = startOfMonth.UnixMilli() - } else if u.DateRange == ReportDurationLast6Months { - startAt = now.AddDate(0, -6, -0).UnixMilli() - } - - return &UserReportOptions{ - UserReportOptionsWithoutDateRange: u.UserReportOptionsWithoutDateRange, - StartAt: startAt, - EndAt: endAt, - } -} - -func (u *UserReportQuery) ToReport() *UserReport { - return &UserReport{ - Id: u.Id, - Username: u.Username, - Email: u.Email, - CreateAt: u.CreateAt, - DisplayName: u.GetDisplayName(ShowNicknameFullName), - Roles: u.Roles, - UserPostStats: u.UserPostStats, - } -} diff --git a/server/public/model/user_serial_gen.go b/server/public/model/user_serial_gen.go index 4551ba60dc..e828f29b67 100644 --- a/server/public/model/user_serial_gen.go +++ b/server/public/model/user_serial_gen.go @@ -1218,1013 +1218,3 @@ func (z *UserPostStats) Msgsize() (s int) { } return } - -// DecodeMsg implements msgp.Decodable -func (z *UserReport) DecodeMsg(dc *msgp.Reader) (err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, err = dc.ReadMapKeyPtr() - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "Id": - z.Id, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Id") - return - } - case "Username": - z.Username, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Username") - return - } - case "Email": - z.Email, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Email") - return - } - case "CreateAt": - z.CreateAt, err = dc.ReadInt64() - if err != nil { - err = msgp.WrapError(err, "CreateAt") - return - } - case "DisplayName": - z.DisplayName, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "DisplayName") - return - } - case "Roles": - z.Roles, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Roles") - return - } - case "UserPostStats": - err = z.UserPostStats.DecodeMsg(dc) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - default: - err = dc.Skip() - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - return -} - -// EncodeMsg implements msgp.Encodable -func (z *UserReport) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 7 - // write "Id" - err = en.Append(0x87, 0xa2, 0x49, 0x64) - if err != nil { - return - } - err = en.WriteString(z.Id) - if err != nil { - err = msgp.WrapError(err, "Id") - return - } - // write "Username" - err = en.Append(0xa8, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65) - if err != nil { - return - } - err = en.WriteString(z.Username) - if err != nil { - err = msgp.WrapError(err, "Username") - return - } - // write "Email" - err = en.Append(0xa5, 0x45, 0x6d, 0x61, 0x69, 0x6c) - if err != nil { - return - } - err = en.WriteString(z.Email) - if err != nil { - err = msgp.WrapError(err, "Email") - return - } - // write "CreateAt" - err = en.Append(0xa8, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74) - if err != nil { - return - } - err = en.WriteInt64(z.CreateAt) - if err != nil { - err = msgp.WrapError(err, "CreateAt") - return - } - // write "DisplayName" - err = en.Append(0xab, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65) - if err != nil { - return - } - err = en.WriteString(z.DisplayName) - if err != nil { - err = msgp.WrapError(err, "DisplayName") - return - } - // write "Roles" - err = en.Append(0xa5, 0x52, 0x6f, 0x6c, 0x65, 0x73) - if err != nil { - return - } - err = en.WriteString(z.Roles) - if err != nil { - err = msgp.WrapError(err, "Roles") - return - } - // write "UserPostStats" - err = en.Append(0xad, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73) - if err != nil { - return - } - err = z.UserPostStats.EncodeMsg(en) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - return -} - -// MarshalMsg implements msgp.Marshaler -func (z *UserReport) MarshalMsg(b []byte) (o []byte, err error) { - o = msgp.Require(b, z.Msgsize()) - // map header, size 7 - // string "Id" - o = append(o, 0x87, 0xa2, 0x49, 0x64) - o = msgp.AppendString(o, z.Id) - // string "Username" - o = append(o, 0xa8, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65) - o = msgp.AppendString(o, z.Username) - // string "Email" - o = append(o, 0xa5, 0x45, 0x6d, 0x61, 0x69, 0x6c) - o = msgp.AppendString(o, z.Email) - // string "CreateAt" - o = append(o, 0xa8, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x74) - o = msgp.AppendInt64(o, z.CreateAt) - // string "DisplayName" - o = append(o, 0xab, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65) - o = msgp.AppendString(o, z.DisplayName) - // string "Roles" - o = append(o, 0xa5, 0x52, 0x6f, 0x6c, 0x65, 0x73) - o = msgp.AppendString(o, z.Roles) - // string "UserPostStats" - o = append(o, 0xad, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73) - o, err = z.UserPostStats.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - return -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *UserReport) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "Id": - z.Id, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Id") - return - } - case "Username": - z.Username, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Username") - return - } - case "Email": - z.Email, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Email") - return - } - case "CreateAt": - z.CreateAt, bts, err = msgp.ReadInt64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "CreateAt") - return - } - case "DisplayName": - z.DisplayName, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "DisplayName") - return - } - case "Roles": - z.Roles, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Roles") - return - } - case "UserPostStats": - bts, err = z.UserPostStats.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - default: - bts, err = msgp.Skip(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - o = bts - return -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *UserReport) Msgsize() (s int) { - s = 1 + 3 + msgp.StringPrefixSize + len(z.Id) + 9 + msgp.StringPrefixSize + len(z.Username) + 6 + msgp.StringPrefixSize + len(z.Email) + 9 + msgp.Int64Size + 12 + msgp.StringPrefixSize + len(z.DisplayName) + 6 + msgp.StringPrefixSize + len(z.Roles) + 14 + z.UserPostStats.Msgsize() - return -} - -// DecodeMsg implements msgp.Decodable -func (z *UserReportOptions) DecodeMsg(dc *msgp.Reader) (err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, err = dc.ReadMapKeyPtr() - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "UserReportOptionsWithoutDateRange": - err = z.UserReportOptionsWithoutDateRange.DecodeMsg(dc) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - case "StartAt": - z.StartAt, err = dc.ReadInt64() - if err != nil { - err = msgp.WrapError(err, "StartAt") - return - } - case "EndAt": - z.EndAt, err = dc.ReadInt64() - if err != nil { - err = msgp.WrapError(err, "EndAt") - return - } - default: - err = dc.Skip() - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - return -} - -// EncodeMsg implements msgp.Encodable -func (z *UserReportOptions) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 3 - // write "UserReportOptionsWithoutDateRange" - err = en.Append(0x83, 0xd9, 0x21, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x44, 0x61, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65) - if err != nil { - return - } - err = z.UserReportOptionsWithoutDateRange.EncodeMsg(en) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - // write "StartAt" - err = en.Append(0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74) - if err != nil { - return - } - err = en.WriteInt64(z.StartAt) - if err != nil { - err = msgp.WrapError(err, "StartAt") - return - } - // write "EndAt" - err = en.Append(0xa5, 0x45, 0x6e, 0x64, 0x41, 0x74) - if err != nil { - return - } - err = en.WriteInt64(z.EndAt) - if err != nil { - err = msgp.WrapError(err, "EndAt") - return - } - return -} - -// MarshalMsg implements msgp.Marshaler -func (z *UserReportOptions) MarshalMsg(b []byte) (o []byte, err error) { - o = msgp.Require(b, z.Msgsize()) - // map header, size 3 - // string "UserReportOptionsWithoutDateRange" - o = append(o, 0x83, 0xd9, 0x21, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x44, 0x61, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65) - o, err = z.UserReportOptionsWithoutDateRange.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - // string "StartAt" - o = append(o, 0xa7, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74) - o = msgp.AppendInt64(o, z.StartAt) - // string "EndAt" - o = append(o, 0xa5, 0x45, 0x6e, 0x64, 0x41, 0x74) - o = msgp.AppendInt64(o, z.EndAt) - return -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *UserReportOptions) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "UserReportOptionsWithoutDateRange": - bts, err = z.UserReportOptionsWithoutDateRange.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - case "StartAt": - z.StartAt, bts, err = msgp.ReadInt64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "StartAt") - return - } - case "EndAt": - z.EndAt, bts, err = msgp.ReadInt64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "EndAt") - return - } - default: - bts, err = msgp.Skip(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - o = bts - return -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *UserReportOptions) Msgsize() (s int) { - s = 1 + 35 + z.UserReportOptionsWithoutDateRange.Msgsize() + 8 + msgp.Int64Size + 6 + msgp.Int64Size - return -} - -// DecodeMsg implements msgp.Decodable -func (z *UserReportOptionsAPI) DecodeMsg(dc *msgp.Reader) (err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, err = dc.ReadMapKeyPtr() - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "UserReportOptionsWithoutDateRange": - err = z.UserReportOptionsWithoutDateRange.DecodeMsg(dc) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - case "DateRange": - z.DateRange, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "DateRange") - return - } - default: - err = dc.Skip() - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - return -} - -// EncodeMsg implements msgp.Encodable -func (z *UserReportOptionsAPI) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 2 - // write "UserReportOptionsWithoutDateRange" - err = en.Append(0x82, 0xd9, 0x21, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x44, 0x61, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65) - if err != nil { - return - } - err = z.UserReportOptionsWithoutDateRange.EncodeMsg(en) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - // write "DateRange" - err = en.Append(0xa9, 0x44, 0x61, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65) - if err != nil { - return - } - err = en.WriteString(z.DateRange) - if err != nil { - err = msgp.WrapError(err, "DateRange") - return - } - return -} - -// MarshalMsg implements msgp.Marshaler -func (z *UserReportOptionsAPI) MarshalMsg(b []byte) (o []byte, err error) { - o = msgp.Require(b, z.Msgsize()) - // map header, size 2 - // string "UserReportOptionsWithoutDateRange" - o = append(o, 0x82, 0xd9, 0x21, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x44, 0x61, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65) - o, err = z.UserReportOptionsWithoutDateRange.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - // string "DateRange" - o = append(o, 0xa9, 0x44, 0x61, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65) - o = msgp.AppendString(o, z.DateRange) - return -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *UserReportOptionsAPI) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "UserReportOptionsWithoutDateRange": - bts, err = z.UserReportOptionsWithoutDateRange.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "UserReportOptionsWithoutDateRange") - return - } - case "DateRange": - z.DateRange, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "DateRange") - return - } - default: - bts, err = msgp.Skip(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - o = bts - return -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *UserReportOptionsAPI) Msgsize() (s int) { - s = 1 + 35 + z.UserReportOptionsWithoutDateRange.Msgsize() + 10 + msgp.StringPrefixSize + len(z.DateRange) - return -} - -// DecodeMsg implements msgp.Decodable -func (z *UserReportOptionsWithoutDateRange) DecodeMsg(dc *msgp.Reader) (err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, err = dc.ReadMapKeyPtr() - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "SortColumn": - z.SortColumn, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "SortColumn") - return - } - case "SortDesc": - z.SortDesc, err = dc.ReadBool() - if err != nil { - err = msgp.WrapError(err, "SortDesc") - return - } - case "PageSize": - z.PageSize, err = dc.ReadInt() - if err != nil { - err = msgp.WrapError(err, "PageSize") - return - } - case "LastSortColumnValue": - z.LastSortColumnValue, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "LastSortColumnValue") - return - } - case "LastUserId": - z.LastUserId, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "LastUserId") - return - } - case "Role": - z.Role, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Role") - return - } - case "Team": - z.Team, err = dc.ReadString() - if err != nil { - err = msgp.WrapError(err, "Team") - return - } - case "HasNoTeam": - z.HasNoTeam, err = dc.ReadBool() - if err != nil { - err = msgp.WrapError(err, "HasNoTeam") - return - } - case "HideActive": - z.HideActive, err = dc.ReadBool() - if err != nil { - err = msgp.WrapError(err, "HideActive") - return - } - case "HideInactive": - z.HideInactive, err = dc.ReadBool() - if err != nil { - err = msgp.WrapError(err, "HideInactive") - return - } - default: - err = dc.Skip() - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - return -} - -// EncodeMsg implements msgp.Encodable -func (z *UserReportOptionsWithoutDateRange) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 10 - // write "SortColumn" - err = en.Append(0x8a, 0xaa, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e) - if err != nil { - return - } - err = en.WriteString(z.SortColumn) - if err != nil { - err = msgp.WrapError(err, "SortColumn") - return - } - // write "SortDesc" - err = en.Append(0xa8, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x65, 0x73, 0x63) - if err != nil { - return - } - err = en.WriteBool(z.SortDesc) - if err != nil { - err = msgp.WrapError(err, "SortDesc") - return - } - // write "PageSize" - err = en.Append(0xa8, 0x50, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65) - if err != nil { - return - } - err = en.WriteInt(z.PageSize) - if err != nil { - err = msgp.WrapError(err, "PageSize") - return - } - // write "LastSortColumnValue" - err = en.Append(0xb3, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65) - if err != nil { - return - } - err = en.WriteString(z.LastSortColumnValue) - if err != nil { - err = msgp.WrapError(err, "LastSortColumnValue") - return - } - // write "LastUserId" - err = en.Append(0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64) - if err != nil { - return - } - err = en.WriteString(z.LastUserId) - if err != nil { - err = msgp.WrapError(err, "LastUserId") - return - } - // write "Role" - err = en.Append(0xa4, 0x52, 0x6f, 0x6c, 0x65) - if err != nil { - return - } - err = en.WriteString(z.Role) - if err != nil { - err = msgp.WrapError(err, "Role") - return - } - // write "Team" - err = en.Append(0xa4, 0x54, 0x65, 0x61, 0x6d) - if err != nil { - return - } - err = en.WriteString(z.Team) - if err != nil { - err = msgp.WrapError(err, "Team") - return - } - // write "HasNoTeam" - err = en.Append(0xa9, 0x48, 0x61, 0x73, 0x4e, 0x6f, 0x54, 0x65, 0x61, 0x6d) - if err != nil { - return - } - err = en.WriteBool(z.HasNoTeam) - if err != nil { - err = msgp.WrapError(err, "HasNoTeam") - return - } - // write "HideActive" - err = en.Append(0xaa, 0x48, 0x69, 0x64, 0x65, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65) - if err != nil { - return - } - err = en.WriteBool(z.HideActive) - if err != nil { - err = msgp.WrapError(err, "HideActive") - return - } - // write "HideInactive" - err = en.Append(0xac, 0x48, 0x69, 0x64, 0x65, 0x49, 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65) - if err != nil { - return - } - err = en.WriteBool(z.HideInactive) - if err != nil { - err = msgp.WrapError(err, "HideInactive") - return - } - return -} - -// MarshalMsg implements msgp.Marshaler -func (z *UserReportOptionsWithoutDateRange) MarshalMsg(b []byte) (o []byte, err error) { - o = msgp.Require(b, z.Msgsize()) - // map header, size 10 - // string "SortColumn" - o = append(o, 0x8a, 0xaa, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e) - o = msgp.AppendString(o, z.SortColumn) - // string "SortDesc" - o = append(o, 0xa8, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x65, 0x73, 0x63) - o = msgp.AppendBool(o, z.SortDesc) - // string "PageSize" - o = append(o, 0xa8, 0x50, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65) - o = msgp.AppendInt(o, z.PageSize) - // string "LastSortColumnValue" - o = append(o, 0xb3, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65) - o = msgp.AppendString(o, z.LastSortColumnValue) - // string "LastUserId" - o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64) - o = msgp.AppendString(o, z.LastUserId) - // string "Role" - o = append(o, 0xa4, 0x52, 0x6f, 0x6c, 0x65) - o = msgp.AppendString(o, z.Role) - // string "Team" - o = append(o, 0xa4, 0x54, 0x65, 0x61, 0x6d) - o = msgp.AppendString(o, z.Team) - // string "HasNoTeam" - o = append(o, 0xa9, 0x48, 0x61, 0x73, 0x4e, 0x6f, 0x54, 0x65, 0x61, 0x6d) - o = msgp.AppendBool(o, z.HasNoTeam) - // string "HideActive" - o = append(o, 0xaa, 0x48, 0x69, 0x64, 0x65, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65) - o = msgp.AppendBool(o, z.HideActive) - // string "HideInactive" - o = append(o, 0xac, 0x48, 0x69, 0x64, 0x65, 0x49, 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65) - o = msgp.AppendBool(o, z.HideInactive) - return -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *UserReportOptionsWithoutDateRange) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "SortColumn": - z.SortColumn, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "SortColumn") - return - } - case "SortDesc": - z.SortDesc, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "SortDesc") - return - } - case "PageSize": - z.PageSize, bts, err = msgp.ReadIntBytes(bts) - if err != nil { - err = msgp.WrapError(err, "PageSize") - return - } - case "LastSortColumnValue": - z.LastSortColumnValue, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "LastSortColumnValue") - return - } - case "LastUserId": - z.LastUserId, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "LastUserId") - return - } - case "Role": - z.Role, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Role") - return - } - case "Team": - z.Team, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Team") - return - } - case "HasNoTeam": - z.HasNoTeam, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "HasNoTeam") - return - } - case "HideActive": - z.HideActive, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "HideActive") - return - } - case "HideInactive": - z.HideInactive, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "HideInactive") - return - } - default: - bts, err = msgp.Skip(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - o = bts - return -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *UserReportOptionsWithoutDateRange) Msgsize() (s int) { - s = 1 + 11 + msgp.StringPrefixSize + len(z.SortColumn) + 9 + msgp.BoolSize + 9 + msgp.IntSize + 20 + msgp.StringPrefixSize + len(z.LastSortColumnValue) + 11 + msgp.StringPrefixSize + len(z.LastUserId) + 5 + msgp.StringPrefixSize + len(z.Role) + 5 + msgp.StringPrefixSize + len(z.Team) + 10 + msgp.BoolSize + 11 + msgp.BoolSize + 13 + msgp.BoolSize - return -} - -// DecodeMsg implements msgp.Decodable -func (z *UserReportQuery) DecodeMsg(dc *msgp.Reader) (err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, err = dc.ReadMapHeader() - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, err = dc.ReadMapKeyPtr() - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "User": - err = z.User.DecodeMsg(dc) - if err != nil { - err = msgp.WrapError(err, "User") - return - } - case "UserPostStats": - err = z.UserPostStats.DecodeMsg(dc) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - default: - err = dc.Skip() - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - return -} - -// EncodeMsg implements msgp.Encodable -func (z *UserReportQuery) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 2 - // write "User" - err = en.Append(0x82, 0xa4, 0x55, 0x73, 0x65, 0x72) - if err != nil { - return - } - err = z.User.EncodeMsg(en) - if err != nil { - err = msgp.WrapError(err, "User") - return - } - // write "UserPostStats" - err = en.Append(0xad, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73) - if err != nil { - return - } - err = z.UserPostStats.EncodeMsg(en) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - return -} - -// MarshalMsg implements msgp.Marshaler -func (z *UserReportQuery) MarshalMsg(b []byte) (o []byte, err error) { - o = msgp.Require(b, z.Msgsize()) - // map header, size 2 - // string "User" - o = append(o, 0x82, 0xa4, 0x55, 0x73, 0x65, 0x72) - o, err = z.User.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "User") - return - } - // string "UserPostStats" - o = append(o, 0xad, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73) - o, err = z.UserPostStats.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - return -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *UserReportQuery) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0001 uint32 - zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - for zb0001 > 0 { - zb0001-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - switch msgp.UnsafeString(field) { - case "User": - bts, err = z.User.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "User") - return - } - case "UserPostStats": - bts, err = z.UserPostStats.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "UserPostStats") - return - } - default: - bts, err = msgp.Skip(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - } - } - o = bts - return -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *UserReportQuery) Msgsize() (s int) { - s = 1 + 5 + z.User.Msgsize() + 14 + z.UserPostStats.Msgsize() - return -} diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 7f9490b47a..d98b22e2c3 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -477,6 +477,10 @@ export default class Client4 { return `${this.getBaseRoute()}/drafts`; } + getReportsRoute(): string { + return `${this.getBaseRoute()}/reports`; + } + getCSRFFromCookie() { if (typeof document !== 'undefined' && typeof document.cookie !== 'undefined') { const cookies = document.cookie.split(';'); @@ -986,7 +990,7 @@ export default class Client4 { getUsersForReporting = (filter: UserReportOptions) => { const queryString = buildQueryString(filter); return this.doFetch( - `${this.getUsersRoute()}/report${queryString}`, + `${this.getReportsRoute()}/users${queryString}`, {method: 'get'}, ); }