[MM-55017] Add API method to get users for Admin Reporting (#25499)
* Add store method to get reporting data * Some store changes * Added app layer * Added API call, some miscellaneous fixes * Fix lint * Fix serialized check * Add API docs * Fix user store tests leaking users * Fix test * PR feedback * Add filtering for role/team/activated user, filter out bot users * Fix mock * Fix test * Oops * Switch to using struct filter * More PR feedback * Fix gen * Fix test * Fix API docs * Fix test * Fix possible SQL injection, some query optimization * Fix migrations * Oops * Add role to API * Fix check * Add Client4 API call for load testing * Fix test * Update server/channels/store/storetest/user_store.go Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> * PR feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7afc14de36
Коммит
109f4643c6
@@ -3524,6 +3524,40 @@ components:
|
|||||||
Description:
|
Description:
|
||||||
description: A description for the CIDRBlock
|
description: A description for the CIDRBlock
|
||||||
type: string
|
type: string
|
||||||
|
UserReport:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
create_at:
|
||||||
|
description: The time in milliseconds a user was created
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
display_name:
|
||||||
|
type: string
|
||||||
|
description: Calculated display name based on user
|
||||||
|
last_login_at:
|
||||||
|
description: Last time the user was logged in
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
last_status_at:
|
||||||
|
description: Last time the user's status was updated
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
last_post_date:
|
||||||
|
description: Last time the user made a post within the given date range
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
days_active:
|
||||||
|
description: Total number of days a user posted within the given date range
|
||||||
|
type: integer
|
||||||
|
total_posts:
|
||||||
|
description: Total number of posts made by a user within the given date range
|
||||||
|
type: integer
|
||||||
Installation:
|
Installation:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -3251,3 +3251,97 @@
|
|||||||
$ref: "#/components/responses/Unauthorized"
|
$ref: "#/components/responses/Unauthorized"
|
||||||
"403":
|
"403":
|
||||||
$ref: "#/components/responses/Forbidden"
|
$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"
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ func (api *API) InitUser() {
|
|||||||
|
|
||||||
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST")
|
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("/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) {
|
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -3443,3 +3445,59 @@ func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Reques
|
|||||||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -828,6 +828,7 @@ type AppIface interface {
|
|||||||
GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError)
|
GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError)
|
||||||
GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError)
|
GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError)
|
||||||
GetUsersEtag(restrictionsHash string) string
|
GetUsersEtag(restrictionsHash string) string
|
||||||
|
GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError)
|
||||||
GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
||||||
GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
||||||
GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
GetUsersInChannelByAdmin(options *model.UserGetOptions) ([]*model.User, *model.AppError)
|
||||||
|
|||||||
@@ -10704,6 +10704,28 @@ func (a *OpenTracingAppLayer) GetUsersEtag(restrictionsHash string) string {
|
|||||||
return resultVar0
|
return resultVar0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersForReporting")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0, resultVar1 := a.app.GetUsersForReporting(filter)
|
||||||
|
|
||||||
|
if resultVar1 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0, resultVar1
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
func (a *OpenTracingAppLayer) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersFromProfiles")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUsersFromProfiles")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
"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/email"
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
|
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/app/users"
|
"github.com/mattermost/mattermost/server/v8/channels/app/users"
|
||||||
@@ -2817,3 +2818,36 @@ func (a *App) UserIsFirstAdmin(user *model.User) bool {
|
|||||||
|
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1932,3 +1932,96 @@ func TestSendSubscriptionHistoryEvent(t *testing.T) {
|
|||||||
require.Equal(t, 10, subscriptionHistoryEvent.Seats, "Number of seats doesn't match")
|
require.Equal(t, 10, subscriptionHistoryEvent.Seats, "Number of seats doesn't match")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
StartAt: 1000,
|
||||||
|
EndAt: 500,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, userReports)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should throw error on bad sort column", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "FakeColumn",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, userReports)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return some formatted reporting data", func(t *testing.T) {
|
||||||
|
th := SetupWithStoreMock(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
// Mock to get the user count
|
||||||
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||||
|
mockUserStore := storemocks.UserStore{}
|
||||||
|
mockUserStore.On("GetUserReport",
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
).Return([]*model.UserReportQuery{
|
||||||
|
{
|
||||||
|
User: model.User{
|
||||||
|
Id: "some-id",
|
||||||
|
CreateAt: 1000,
|
||||||
|
FirstName: "Bob",
|
||||||
|
LastName: "Bobson",
|
||||||
|
},
|
||||||
|
UserPostStats: model.UserPostStats{
|
||||||
|
LastLogin: 1500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
mockStore.On("User").Return(&mockUserStore)
|
||||||
|
|
||||||
|
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.NotNil(t, userReports)
|
||||||
|
require.Equal(t, "Bob Bobson", userReports[0].DisplayName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.down.sql
|
|||||||
channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.up.sql
|
channels/db/migrations/mysql/000116_create_outgoing_oauth_connections.up.sql
|
||||||
channels/db/migrations/mysql/000117_msteams_shared_channels.down.sql
|
channels/db/migrations/mysql/000117_msteams_shared_channels.down.sql
|
||||||
channels/db/migrations/mysql/000117_msteams_shared_channels.up.sql
|
channels/db/migrations/mysql/000117_msteams_shared_channels.up.sql
|
||||||
|
channels/db/migrations/mysql/000118_create_index_poststats.down.sql
|
||||||
|
channels/db/migrations/mysql/000118_create_index_poststats.up.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||||
@@ -464,3 +466,5 @@ channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.down.sq
|
|||||||
channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.up.sql
|
channels/db/migrations/postgres/000116_create_outgoing_oauth_connections.up.sql
|
||||||
channels/db/migrations/postgres/000117_msteams_shared_channels.down.sql
|
channels/db/migrations/postgres/000117_msteams_shared_channels.down.sql
|
||||||
channels/db/migrations/postgres/000117_msteams_shared_channels.up.sql
|
channels/db/migrations/postgres/000117_msteams_shared_channels.up.sql
|
||||||
|
channels/db/migrations/postgres/000118_create_index_poststats.down.sql
|
||||||
|
channels/db/migrations/postgres/000118_create_index_poststats.up.sql
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Nothing to do for MySQL
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Nothing to do for MySQL
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_poststats_userid;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- morph:nontransactional
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_poststats_userid ON poststats(userid)
|
||||||
@@ -11892,6 +11892,24 @@ func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, chan
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUserReport")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
result, err := s.UserStore.GetUserReport(filter)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
func (s *OpenTracingLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersBatchForIndexing")
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUsersBatchForIndexing")
|
||||||
|
|||||||
@@ -13577,6 +13577,27 @@ func (s *RetryLayerUserStore) GetUnreadCountForChannel(userID string, channelID
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.UserStore.GetUserReport(filter)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
func (s *RetryLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
sq "github.com/mattermost/squirrel"
|
sq "github.com/mattermost/squirrel"
|
||||||
@@ -2266,3 +2267,102 @@ func (us SqlUserStore) RefreshPostStatsForUsers() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
|
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
|
||||||
|
selectColumns := []string{"u.Id", "u.LastLogin", "MAX(s.LastActivityAt) AS LastStatusAt"}
|
||||||
|
for _, column := range model.UserReportSortColumns {
|
||||||
|
selectColumns = append(selectColumns, "u."+column)
|
||||||
|
}
|
||||||
|
if isPostgres {
|
||||||
|
selectColumns = append(selectColumns,
|
||||||
|
"MAX(ps.LastPostDate) AS LastPostDate",
|
||||||
|
"COUNT(ps.Day) AS DaysActive",
|
||||||
|
"SUM(ps.NumPosts) AS TotalPosts",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
selectColumns = append(selectColumns,
|
||||||
|
"MAX(p.CreateAt) AS LastPostDate",
|
||||||
|
"COUNT(DATE(FROM_UNIXTIME(p.CreateAt / 1000))) AS DaysActive",
|
||||||
|
"COUNT(p.Id) AS TotalPosts",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortColumnValue := filter.SortColumn
|
||||||
|
if filter.SortDesc {
|
||||||
|
sortColumnValue += " DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := us.getQueryBuilder().
|
||||||
|
Select(selectColumns...).
|
||||||
|
From("Users u").
|
||||||
|
LeftJoin("Status s ON s.UserId = u.Id").
|
||||||
|
Where(sq.Or{
|
||||||
|
sq.Gt{filter.SortColumn: filter.LastSortColumnValue},
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{filter.SortColumn: filter.LastSortColumnValue},
|
||||||
|
sq.Gt{"u.Id": filter.LastUserId},
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM Bots)")).
|
||||||
|
GroupBy("u.Id").
|
||||||
|
OrderBy(sortColumnValue, "u.Id")
|
||||||
|
|
||||||
|
if filter.PageSize > 0 {
|
||||||
|
query = query.Limit(uint64(filter.PageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPostgres {
|
||||||
|
query = query.LeftJoin("PostStats ps ON ps.UserId = u.Id")
|
||||||
|
if filter.StartAt > 0 {
|
||||||
|
startDate := time.UnixMilli(filter.StartAt)
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Expr("ps.UserId IS NULL"),
|
||||||
|
sq.GtOrEq{"ps.Day": startDate.Format("2006-01-02")},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if filter.EndAt > 0 {
|
||||||
|
endDate := time.UnixMilli(filter.EndAt)
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Expr("ps.UserId IS NULL"),
|
||||||
|
sq.Lt{"ps.Day": endDate.Format("2006-01-02")},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = query.LeftJoin("Posts p on p.UserId = u.Id")
|
||||||
|
if filter.StartAt > 0 {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Expr("p.UserId IS NULL"),
|
||||||
|
sq.GtOrEq{"p.CreateAt": filter.StartAt},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if filter.EndAt > 0 {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Expr("p.UserId IS NULL"),
|
||||||
|
sq.Lt{"p.CreateAt": filter.EndAt},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query = applyRoleFilter(query, filter.Role, isPostgres)
|
||||||
|
if filter.HasNoTeam {
|
||||||
|
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM TeamMembers WHERE DeleteAt = 0)"))
|
||||||
|
} else if filter.Team != "" {
|
||||||
|
query = query.Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.DeleteAt = 0)").
|
||||||
|
Where(sq.Eq{"tm.TeamId": filter.Team})
|
||||||
|
}
|
||||||
|
if filter.HideActive {
|
||||||
|
query = query.Where(sq.Gt{"u.DeleteAt": 0})
|
||||||
|
}
|
||||||
|
if filter.HideInactive {
|
||||||
|
query = query.Where(sq.Eq{"u.DeleteAt": 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
userResults := []*model.UserReportQuery{}
|
||||||
|
err := us.GetReplicaX().SelectBuilder(&userResults, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get users for reporting")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userResults, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ type UserStore interface {
|
|||||||
GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error)
|
GetUsersWithInvalidEmails(page int, perPage int, restrictedDomains string) ([]*model.User, error)
|
||||||
InsertUsers(users []*model.User) error
|
InsertUsers(users []*model.User) error
|
||||||
RefreshPostStatsForUsers() error
|
RefreshPostStatsForUsers() error
|
||||||
|
GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotStore interface {
|
type BotStore interface {
|
||||||
|
|||||||
@@ -1153,6 +1153,32 @@ func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserReport provides a mock function with given fields: filter
|
||||||
|
func (_m *UserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
|
ret := _m.Called(filter)
|
||||||
|
|
||||||
|
var r0 []*model.UserReportQuery
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) ([]*model.UserReportQuery, error)); ok {
|
||||||
|
return rf(filter)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) []*model.UserReportQuery); ok {
|
||||||
|
r0 = rf(filter)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.UserReportQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(*model.UserReportOptions) error); ok {
|
||||||
|
r1 = rf(filter)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsersBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
|
// GetUsersBatchForIndexing provides a mock function with given fields: startTime, startFileID, limit
|
||||||
func (_m *UserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
func (_m *UserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||||
ret := _m.Called(startTime, startFileID, limit)
|
ret := _m.Called(startTime, startFileID, limit)
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func TestUserStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|||||||
t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, rctx, ss) })
|
t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, rctx, ss) })
|
||||||
t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, rctx, ss) })
|
t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, rctx, ss) })
|
||||||
t.Run("UpdateLastLogin", func(t *testing.T) { testUpdateLastLogin(t, rctx, ss) })
|
t.Run("UpdateLastLogin", func(t *testing.T) { testUpdateLastLogin(t, rctx, ss) })
|
||||||
|
t.Run("GetUserReport", func(t *testing.T) { testGetUserReport(t, rctx, ss) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUserStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
|
func testUserStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||||
@@ -4904,6 +4905,7 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, rctx request.CTX, ss st
|
|||||||
CreateAt: model.GetMillis(),
|
CreateAt: model.GetMillis(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
|
||||||
|
|
||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
|
|
||||||
@@ -4913,6 +4915,7 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, rctx request.CTX, ss st
|
|||||||
CreateAt: model.GetMillis(),
|
CreateAt: model.GetMillis(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
|
||||||
_, nErr = ss.Team().SaveMember(&model.TeamMember{
|
_, nErr = ss.Team().SaveMember(&model.TeamMember{
|
||||||
UserId: u2.Id,
|
UserId: u2.Id,
|
||||||
TeamId: t1.Id,
|
TeamId: t1.Id,
|
||||||
@@ -4939,6 +4942,7 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, rctx request.CTX, ss st
|
|||||||
CreateAt: model.GetMillis(),
|
CreateAt: model.GetMillis(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
|
||||||
_, nErr = ss.Team().SaveMember(&model.TeamMember{
|
_, nErr = ss.Team().SaveMember(&model.TeamMember{
|
||||||
UserId: u3.Id,
|
UserId: u3.Id,
|
||||||
TeamId: t1.Id,
|
TeamId: t1.Id,
|
||||||
@@ -6182,3 +6186,297 @@ func testUpdateLastLogin(t *testing.T, rctx request.CTX, ss store.Store) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, int64(1234567890), user.LastLogin)
|
require.Equal(t, int64(1234567890), user.LastLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
u1 := &model.User{Username: "u1" + model.NewId(), DeleteAt: 0}
|
||||||
|
u1.Email = MakeEmail()
|
||||||
|
u1, err := ss.User().Save(u1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { require.NoError(t, ss.User().PermanentDelete(u1.Id)) }()
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
p := model.Post{UserId: u1.Id, ChannelId: model.NewId(), Message: NewTestId(), CreateAt: now.AddDate(0, 0, -i).UnixMilli()}
|
||||||
|
_, err = ss.Post().Save(&p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u2 := &model.User{Username: "u2" + model.NewId(), Roles: "system", DeleteAt: 0}
|
||||||
|
u2.Email = MakeEmail()
|
||||||
|
u2, err = ss.User().Save(u2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { require.NoError(t, ss.User().PermanentDelete(u2.Id)) }()
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
p := model.Post{UserId: u2.Id, ChannelId: model.NewId(), Message: NewTestId(), CreateAt: now.AddDate(0, 0, -i).UnixMilli()}
|
||||||
|
_, err = ss.Post().Save(&p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u3 := &model.User{Username: "u3" + model.NewId(), Roles: "guest", DeleteAt: now.UnixMilli()}
|
||||||
|
u3.Email = MakeEmail()
|
||||||
|
u3, err = ss.User().Save(u3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
team, err := ss.Team().Save(&model.Team{
|
||||||
|
DisplayName: "DisplayName",
|
||||||
|
Name: NewTestId(),
|
||||||
|
Email: MakeEmail(),
|
||||||
|
Type: model.TeamOpen,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = ss.Team().SaveMember(&model.TeamMember{UserId: u3.Id, TeamId: team.Id}, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, ss.Team().RemoveMember(rctx, team.Id, u3.Id))
|
||||||
|
require.NoError(t, ss.Team().PermanentDelete(team.Id))
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() { require.NoError(t, ss.User().PermanentDelete(u3.Id)) }()
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
p := model.Post{UserId: u3.Id, ChannelId: model.NewId(), Message: NewTestId(), CreateAt: now.AddDate(0, 0, -i).UnixMilli()}
|
||||||
|
_, err = ss.Post().Save(&p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ss.User().RefreshPostStatsForUsers()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("should return info for all the users", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, userReport)
|
||||||
|
require.Len(t, userReport, 3)
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[0])
|
||||||
|
require.Equal(t, u1.Username, userReport[0].Username)
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[1])
|
||||||
|
require.Equal(t, u2.Username, userReport[1].Username)
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[2])
|
||||||
|
require.Equal(t, u3.Username, userReport[2].Username)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return in the correct order", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
SortDesc: true,
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, userReport)
|
||||||
|
require.Equal(t, 3, len(userReport))
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[0])
|
||||||
|
require.Equal(t, u3.Username, userReport[0].Username)
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[1])
|
||||||
|
require.Equal(t, u2.Username, userReport[1].Username)
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[2])
|
||||||
|
require.Equal(t, u1.Username, userReport[2].Username)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail on invalid sort column", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "FakeColumn",
|
||||||
|
SortDesc: true,
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, userReport)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should only return amount of users in page", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, userReport)
|
||||||
|
require.Equal(t, 2, len(userReport))
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[0])
|
||||||
|
require.Equal(t, u1.Username, userReport[0].Username)
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[1])
|
||||||
|
require.Equal(t, u2.Username, userReport[1].Username)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return correct paging", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
LastSortColumnValue: u2.Username,
|
||||||
|
LastUserId: u2.Id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, userReport)
|
||||||
|
require.Equal(t, 1, len(userReport))
|
||||||
|
|
||||||
|
require.NotNil(t, userReport[0])
|
||||||
|
require.Equal(t, u3.Username, userReport[0].Username)
|
||||||
|
})
|
||||||
|
|
||||||
|
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{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 3)
|
||||||
|
|
||||||
|
require.Equal(t, 5, *userReport[0].TotalPosts)
|
||||||
|
require.Equal(t, 5, *userReport[0].DaysActive)
|
||||||
|
require.Equal(t, now.UnixMilli(), *userReport[0].LastPostDate)
|
||||||
|
require.Equal(t, 5, *userReport[1].TotalPosts)
|
||||||
|
require.Equal(t, 5, *userReport[1].DaysActive)
|
||||||
|
require.Equal(t, now.UnixMilli(), *userReport[1].LastPostDate)
|
||||||
|
require.Equal(t, 5, *userReport[2].TotalPosts)
|
||||||
|
require.Equal(t, 5, *userReport[2].DaysActive)
|
||||||
|
require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate)
|
||||||
|
|
||||||
|
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
StartAt: now.AddDate(0, 0, -2).UnixMilli(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 3)
|
||||||
|
|
||||||
|
require.Equal(t, 3, *userReport[0].TotalPosts)
|
||||||
|
require.Equal(t, 3, *userReport[0].DaysActive)
|
||||||
|
require.Equal(t, now.UnixMilli(), *userReport[0].LastPostDate)
|
||||||
|
require.Equal(t, 3, *userReport[1].TotalPosts)
|
||||||
|
require.Equal(t, 3, *userReport[1].DaysActive)
|
||||||
|
require.Equal(t, now.UnixMilli(), *userReport[1].LastPostDate)
|
||||||
|
require.Equal(t, 3, *userReport[2].TotalPosts)
|
||||||
|
require.Equal(t, 3, *userReport[2].DaysActive)
|
||||||
|
require.Equal(t, now.UnixMilli(), *userReport[2].LastPostDate)
|
||||||
|
|
||||||
|
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 3)
|
||||||
|
|
||||||
|
require.Equal(t, 2, *userReport[0].TotalPosts)
|
||||||
|
require.Equal(t, 2, *userReport[0].DaysActive)
|
||||||
|
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[0].LastPostDate)
|
||||||
|
require.Equal(t, 2, *userReport[1].TotalPosts)
|
||||||
|
require.Equal(t, 2, *userReport[1].DaysActive)
|
||||||
|
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[1].LastPostDate)
|
||||||
|
require.Equal(t, 2, *userReport[2].TotalPosts)
|
||||||
|
require.Equal(t, 2, *userReport[2].DaysActive)
|
||||||
|
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[2].LastPostDate)
|
||||||
|
|
||||||
|
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
},
|
||||||
|
StartAt: now.AddDate(0, 0, -3).UnixMilli(),
|
||||||
|
EndAt: now.AddDate(0, 0, -2).UnixMilli(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 3)
|
||||||
|
|
||||||
|
require.Equal(t, 1, *userReport[0].TotalPosts)
|
||||||
|
require.Equal(t, 1, *userReport[0].DaysActive)
|
||||||
|
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[0].LastPostDate)
|
||||||
|
require.Equal(t, 1, *userReport[1].TotalPosts)
|
||||||
|
require.Equal(t, 1, *userReport[1].DaysActive)
|
||||||
|
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[1].LastPostDate)
|
||||||
|
require.Equal(t, 1, *userReport[2].TotalPosts)
|
||||||
|
require.Equal(t, 1, *userReport[2].DaysActive)
|
||||||
|
require.Equal(t, now.AddDate(0, 0, -3).UnixMilli(), *userReport[2].LastPostDate)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should filter on roles", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
Role: "system",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 1)
|
||||||
|
require.Equal(t, u2.Id, userReport[0].Id)
|
||||||
|
require.Equal(t, u2.Roles, "system")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should filter on teams", func(t *testing.T) {
|
||||||
|
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
HasNoTeam: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 2)
|
||||||
|
require.Equal(t, u1.Id, userReport[0].Id)
|
||||||
|
require.Equal(t, u2.Id, userReport[1].Id)
|
||||||
|
|
||||||
|
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
Team: team.Id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 1)
|
||||||
|
require.Equal(t, u3.Id, userReport[0].Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 2)
|
||||||
|
require.Equal(t, u1.Id, userReport[0].Id)
|
||||||
|
require.Equal(t, u2.Id, userReport[1].Id)
|
||||||
|
|
||||||
|
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
|
||||||
|
UserReportOptionsWithoutDateRange: model.UserReportOptionsWithoutDateRange{
|
||||||
|
SortColumn: "Username",
|
||||||
|
PageSize: 50,
|
||||||
|
HideActive: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, userReport, 1)
|
||||||
|
require.Equal(t, u3.Id, userReport[0].Id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -10709,6 +10709,22 @@ func (s *TimerLayerUserStore) GetUnreadCountForChannel(userID string, channelID
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.UserStore.GetUserReport(filter)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUserReport", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
func (s *TimerLayerUserStore) GetUsersBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.UserForIndexing, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -2016,6 +2016,14 @@
|
|||||||
"id": "api.getThreadsForUser.bad_params",
|
"id": "api.getThreadsForUser.bad_params",
|
||||||
"translation": "Before and After parameters to getThreadsForUser are mutually exclusive"
|
"translation": "Before and After parameters to getThreadsForUser are mutually exclusive"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.getUsersForReporting.invalid_active_filter",
|
||||||
|
"translation": "Cannot hide both active and inactive users."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.getUsersForReporting.invalid_team_filter",
|
||||||
|
"translation": "Invalid team id provided."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.image.get.app_error",
|
"id": "api.image.get.app_error",
|
||||||
"translation": "Requested image url cannot be parsed."
|
"translation": "Requested image url cannot be parsed."
|
||||||
@@ -7054,10 +7062,26 @@
|
|||||||
"id": "app.user.get_unread_count.app_error",
|
"id": "app.user.get_unread_count.app_error",
|
||||||
"translation": "We could not get the unread message count for the user."
|
"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",
|
"id": "app.user.get_users_batch_for_indexing.get_users.app_error",
|
||||||
"translation": "Unable to get the users batch for indexing."
|
"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",
|
"id": "app.user.missing_account.const",
|
||||||
"translation": "Unable to find the user."
|
"translation": "Unable to find the user."
|
||||||
|
|||||||
@@ -1918,6 +1918,54 @@ func (c *Client4) EnableUserAccessToken(ctx context.Context, tokenId string) (*R
|
|||||||
return BuildResponse(r), nil
|
return BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client4) GetUsersForReporting(ctx context.Context, options *UserReportOptionsAPI) ([]*UserReport, *Response, error) {
|
||||||
|
values := url.Values{}
|
||||||
|
if options.SortColumn != "" {
|
||||||
|
values.Set("sort_column", options.SortColumn)
|
||||||
|
}
|
||||||
|
if options.PageSize > 0 {
|
||||||
|
values.Set("page_size", strconv.Itoa(options.PageSize))
|
||||||
|
}
|
||||||
|
if options.Team != "" {
|
||||||
|
values.Set("team_filter", options.Team)
|
||||||
|
}
|
||||||
|
if options.HideActive {
|
||||||
|
values.Set("hide_active", "true")
|
||||||
|
}
|
||||||
|
if options.HideInactive {
|
||||||
|
values.Set("hide_inactive", "true")
|
||||||
|
}
|
||||||
|
if options.SortDesc {
|
||||||
|
values.Set("sort_direction", "desc")
|
||||||
|
}
|
||||||
|
if options.LastSortColumnValue != "" {
|
||||||
|
values.Set("last_column_value", options.LastSortColumnValue)
|
||||||
|
}
|
||||||
|
if options.LastUserId != "" {
|
||||||
|
values.Set("last_id", options.LastUserId)
|
||||||
|
}
|
||||||
|
if options.Role != "" {
|
||||||
|
values.Set("role_filter", options.Role)
|
||||||
|
}
|
||||||
|
if options.HasNoTeam {
|
||||||
|
values.Set("has_no_team", "true")
|
||||||
|
}
|
||||||
|
if options.DateRange != "" {
|
||||||
|
values.Set("date_range", options.DateRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/report?"+values.Encode(), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
var list []*UserReport
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
|
||||||
|
return nil, nil, NewAppError("GetUsersForReporting", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
return list, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Bots section
|
// Bots section
|
||||||
|
|
||||||
// CreateBot creates a bot in the system based on the provided bot struct.
|
// CreateBot creates a bot in the system based on the provided bot struct.
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ const (
|
|||||||
PushThreadsNotifyProp = "push_threads"
|
PushThreadsNotifyProp = "push_threads"
|
||||||
EmailThreadsNotifyProp = "email_threads"
|
EmailThreadsNotifyProp = "email_threads"
|
||||||
|
|
||||||
|
ReportDurationLast30Days = "last_30_days"
|
||||||
|
ReportDurationPreviousMonth = "previous_month"
|
||||||
|
ReportDurationLast6Months = "last_6_months"
|
||||||
|
|
||||||
DefaultLocale = "en"
|
DefaultLocale = "en"
|
||||||
UserAuthServiceEmail = "email"
|
UserAuthServiceEmail = "email"
|
||||||
|
|
||||||
@@ -67,6 +71,10 @@ const (
|
|||||||
DesktopTokenTTL = time.Minute * 3
|
DesktopTokenTTL = time.Minute * 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UserReportSortColumns = []string{"CreateAt", "Username", "FirstName", "LastName", "Nickname", "Email", "Roles"}
|
||||||
|
)
|
||||||
|
|
||||||
//msgp:tuple User
|
//msgp:tuple User
|
||||||
|
|
||||||
// User contains the details about the user.
|
// User contains the details about the user.
|
||||||
@@ -1007,3 +1015,82 @@ type UsersWithGroupsAndCount struct {
|
|||||||
Users []*UserWithGroups `json:"users"`
|
Users []*UserWithGroups `json:"users"`
|
||||||
Count int64 `json:"total_count"`
|
Count int64 `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserPostStats struct {
|
||||||
|
LastLogin int64 `json:"last_login_at,omitempty"`
|
||||||
|
LastStatusAt *int64 `json:"last_status_at,omitempty"`
|
||||||
|
LastPostDate *int64 `json:"last_post_date,omitempty"`
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -52,7 +52,7 @@ import {
|
|||||||
ChannelSearchOpts,
|
ChannelSearchOpts,
|
||||||
ServerChannel,
|
ServerChannel,
|
||||||
} from '@mattermost/types/channels';
|
} from '@mattermost/types/channels';
|
||||||
import {Options, StatusOK, ClientResponse, LogLevel, FetchPaginatedThreadOptions} from '@mattermost/types/client4';
|
import {Options, StatusOK, ClientResponse, LogLevel, FetchPaginatedThreadOptions, UserReportOptions} from '@mattermost/types/client4';
|
||||||
import {Compliance} from '@mattermost/types/compliance';
|
import {Compliance} from '@mattermost/types/compliance';
|
||||||
import {
|
import {
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
@@ -133,6 +133,7 @@ import {
|
|||||||
UserStatus,
|
UserStatus,
|
||||||
GetFilteredUsersStatsOpts,
|
GetFilteredUsersStatsOpts,
|
||||||
UserCustomStatus,
|
UserCustomStatus,
|
||||||
|
UserReport,
|
||||||
} from '@mattermost/types/users';
|
} from '@mattermost/types/users';
|
||||||
import {DeepPartial, RelationOneToOne} from '@mattermost/types/utilities';
|
import {DeepPartial, RelationOneToOne} from '@mattermost/types/utilities';
|
||||||
import {ProductNotices} from '@mattermost/types/product_notices';
|
import {ProductNotices} from '@mattermost/types/product_notices';
|
||||||
@@ -982,6 +983,14 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getUsersForReporting = (filter: UserReportOptions) => {
|
||||||
|
const queryString = buildQueryString(filter);
|
||||||
|
return this.doFetch<UserReport[]>(
|
||||||
|
`${this.getUsersRoute()}/report${queryString}`,
|
||||||
|
{method: 'get'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,3 +37,23 @@ export type FetchPaginatedThreadOptions = {
|
|||||||
fromCreateAt?: number;
|
fromCreateAt?: number;
|
||||||
fromPost?: string;
|
fromPost?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ReportDuration {
|
||||||
|
Last30Days = 'last_30_days',
|
||||||
|
PreviousMonth = 'previous_month',
|
||||||
|
Last6Months = 'last_6_months',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserReportOptions = {
|
||||||
|
sort_column: 'CreateAt' | 'Username' | 'FirstName' | 'LastName' | 'Nickname' | 'Email',
|
||||||
|
page_size: number,
|
||||||
|
sort_direction?: 'asc' | 'desc',
|
||||||
|
date_range?: ReportDuration,
|
||||||
|
last_column_value?: string,
|
||||||
|
last_id?: string,
|
||||||
|
role_filter?: string,
|
||||||
|
has_no_team?: boolean,
|
||||||
|
team_filter?: string,
|
||||||
|
hide_active?: boolean,
|
||||||
|
hide_inactive?: boolean,
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,3 +143,16 @@ export type GetFilteredUsersStatsOpts = {
|
|||||||
export type AuthChangeResponse = {
|
export type AuthChangeResponse = {
|
||||||
follow_link: string;
|
follow_link: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserReport = {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
create_at: number;
|
||||||
|
display_name: string;
|
||||||
|
last_login_at: number;
|
||||||
|
last_status_at?: number;
|
||||||
|
last_post_date?: number;
|
||||||
|
days_active?: number;
|
||||||
|
total_posts?: number;
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user