diff --git a/api4/resolver_user_test.go b/api4/resolver_user_test.go index fd65a6c88f..c10176be2c 100644 --- a/api4/resolver_user_test.go +++ b/api4/resolver_user_test.go @@ -33,6 +33,10 @@ func TestGraphQLUser(t *testing.T) { IsBot bool `json:"isBot"` IsSystemAdmin bool `json:"isSystemAdmin"` CreateAt float64 `json:"createAt"` + DeleteAt float64 `json:"deleteAt"` + UpdateAt float64 `json:"updateAt"` + AuthData *string `json:"authData"` + EmailVerified bool `json:"emailVerified"` CustomStatus struct { Emoji string `json:"emoji"` Text string `json:"text"` @@ -49,6 +53,9 @@ func TestGraphQLUser(t *testing.T) { Permissions []string `json:"permissions"` SchemeManaged bool `json:"schemeManaged"` BuiltIn bool `json:"builtIn"` + CreateAt float64 `json:"createAt"` + DeleteAt float64 `json:"deleteAt"` + UpdateAt float64 `json:"updateAt"` } `json:"roles"` Preferences []struct { UserID string `json:"userId"` @@ -57,8 +64,11 @@ func TestGraphQLUser(t *testing.T) { Value string `json:"value"` } `json:"preferences"` Sessions []struct { - ID string `json:"id"` - CreateAt float64 `json:"createAt"` + ID string `json:"id"` + CreateAt float64 `json:"createAt"` + LastActivityAt float64 `json:"lastActivityAt"` + DeviceId string `json:"deviceId"` + Roles string `json:"roles"` } `json:"sessions"` } `json:"user"` } @@ -72,8 +82,12 @@ func TestGraphQLUser(t *testing.T) { id username email + createAt + updateAt + deleteAt firstName lastName + emailVerified isBot isGuest isSystemAdmin @@ -83,6 +97,9 @@ func TestGraphQLUser(t *testing.T) { roles { id name + createAt + updateAt + deleteAt } preferences { name @@ -91,6 +108,8 @@ func TestGraphQLUser(t *testing.T) { sessions { id createAt + lastActivityAt + roles } } } @@ -106,6 +125,9 @@ func TestGraphQLUser(t *testing.T) { assert.Equal(t, th.BasicUser.Email, q.User.Email) assert.Equal(t, th.BasicUser.FirstName, q.User.FirstName) assert.Equal(t, th.BasicUser.IsBot, q.User.IsBot) + assert.Equal(t, float64(th.BasicUser.CreateAt), q.User.CreateAt) + assert.Equal(t, float64(th.BasicUser.DeleteAt), q.User.DeleteAt) + assert.NotZero(t, q.User.UpdateAt) assert.Equal(t, th.BasicUser.IsSystemAdmin(), q.User.IsSystemAdmin) assert.Equal(t, th.BasicUser.Timezone, q.User.Timezone) assert.Equal(t, th.BasicUser.Props, q.User.Props) @@ -118,6 +140,9 @@ func TestGraphQLUser(t *testing.T) { assert.Len(t, roles, 1) assert.Equal(t, roles[0].Id, q.User.Roles[0].ID) assert.Equal(t, roles[0].Name, q.User.Roles[0].Name) + assert.Equal(t, float64(roles[0].CreateAt), q.User.Roles[0].CreateAt) + assert.Equal(t, float64(roles[0].UpdateAt), q.User.Roles[0].UpdateAt) + assert.Equal(t, float64(roles[0].DeleteAt), q.User.Roles[0].DeleteAt) prefs, _, err := th.Client.GetPreferences(th.BasicUser.Id) require.NoError(t, err) @@ -140,6 +165,8 @@ func TestGraphQLUser(t *testing.T) { for _, session := range q.User.Sessions { assert.NotEmpty(t, session.ID) assert.Less(t, session.CreateAt, now) + assert.Less(t, session.LastActivityAt, now) + assert.Equal(t, model.SystemUserRoleId, session.Roles) } }) diff --git a/api4/schema.graphqls b/api4/schema.graphqls index 9fd37d686f..71b222ee62 100644 --- a/api4/schema.graphqls +++ b/api4/schema.graphqls @@ -38,6 +38,8 @@ scalar SidebarCategorySorting scalar StringMap +scalar StringInterface + scalar Time type Channel { @@ -61,6 +63,9 @@ type Channel { totalMsgCountRoot: Float! lastRootPostAt: Float! stats: ChannelStats + extraUpdateAt: Float! + props: StringInterface! + policyId: String cursor: String } @@ -82,6 +87,7 @@ type ChannelMember { cursor: String } +# Deliberately omitting password, authData, mfaSecret. type User { id: String! username: String! @@ -89,20 +95,33 @@ type User { firstName: String! lastName: String! nickname: String! + emailVerified: Boolean! isBot: Boolean! isGuest: Boolean! isSystemAdmin: Boolean! createAt: Float! + updateAt: Float! deleteAt: Float! authService: String! customStatus: CustomStatus status: Status props: StringMap! notifyProps: StringMap! - lastPictureUpdateAt: Float! + lastPictureUpdate: Float! + lastPasswordUpdate: Float! + failedAttempts: Float! locale: String! timezone: StringMap! position: String! + mfaActive: Boolean! + allowMarketing: Boolean! + remoteId: String + lastActivityAt: Float! + botDescription: String! + botLastIconUpdate: Float! + termsOfServiceId: String! + termsOfServiceCreateAt: Float! + disableWelcomeEmail: Boolean! roles: [Role]! preferences: [Preference!]! sessions: [Session]! @@ -126,6 +145,11 @@ type Status { type Role { id: String! name: String! + displayName: String! + description: String! + createAt: Float! + updateAt: Float! + deleteAt: Float! permissions: [String!]! schemeManaged: Boolean! builtIn: Boolean! @@ -167,6 +191,7 @@ type TeamMember { schemeGuest: Boolean! schemeUser: Boolean! schemeAdmin: Boolean! + explicitRoles: String! } type SidebarCategory { @@ -180,11 +205,19 @@ type SidebarCategory { channelIds: [String!]! } +# Deliberately leaving out teamMembers. type Session { id: String! token: String! createAt: Float! expiresAt: Float! + lastActivityAt: Float! + deviceId: String! + roles: String! + isOAuth: Boolean! + expiredNotify: Boolean! + props: StringMap! + local: Boolean! } type ChannelStats { diff --git a/model/channel.go b/model/channel.go index 8c86c53763..cdf40140a5 100644 --- a/model/channel.go +++ b/model/channel.go @@ -225,6 +225,14 @@ func (o *Channel) LastRootPostAt_() float64 { return float64(o.LastRootPostAt) } +func (o *Channel) ExtraUpdateAt_() float64 { + return float64(o.ExtraUpdateAt) +} + +func (o *Channel) Props_() StringInterface { + return StringInterface(o.Props) +} + func (o *Channel) DeepCopy() *Channel { copy := *o if copy.SchemeId != nil { diff --git a/model/role.go b/model/role.go index c87e6ca303..7823f133b4 100644 --- a/model/role.go +++ b/model/role.go @@ -444,6 +444,18 @@ func (r *Role) Patch(patch *RolePatch) { } } +func (r *Role) CreateAt_() float64 { + return float64(r.CreateAt) +} + +func (r *Role) UpdateAt_() float64 { + return float64(r.UpdateAt) +} + +func (r *Role) DeleteAt_() float64 { + return float64(r.DeleteAt) +} + // MergeChannelHigherScopedPermissions is meant to be invoked on a channel scheme's role and merges the higher-scoped // channel role's permissions. func (r *Role) MergeChannelHigherScopedPermissions(higherScopedPermissions *RolePermissions) { diff --git a/model/session.go b/model/session.go index d19c310f2e..efccf7ac4b 100644 --- a/model/session.go +++ b/model/session.go @@ -244,3 +244,7 @@ func (s *Session) CreateAt_() float64 { func (s *Session) ExpiresAt_() float64 { return float64(s.ExpiresAt) } + +func (s *Session) LastActivityAt_() float64 { + return float64(s.LastActivityAt) +} diff --git a/model/user.go b/model/user.go index 39163d168d..59d0110cb4 100644 --- a/model/user.go +++ b/model/user.go @@ -477,10 +477,34 @@ func (u *User) DeleteAt_() float64 { return float64(u.DeleteAt) } -func (u *User) LastPictureUpdateAt() float64 { +func (u *User) UpdateAt_() float64 { + return float64(u.UpdateAt) +} + +func (u *User) LastPictureUpdate_() float64 { return float64(u.LastPictureUpdate) } +func (u *User) LastPasswordUpdate_() float64 { + return float64(u.LastPasswordUpdate) +} + +func (u *User) FailedAttempts_() float64 { + return float64(u.FailedAttempts) +} + +func (u *User) LastActivityAt_() float64 { + return float64(u.LastActivityAt) +} + +func (u *User) BotLastIconUpdate_() float64 { + return float64(u.BotLastIconUpdate) +} + +func (u *User) TermsOfServiceCreateAt_() float64 { + return float64(u.TermsOfServiceCreateAt) +} + // PreUpdate should be run before updating the user in the db. func (u *User) PreUpdate() { u.Username = SanitizeUnicode(u.Username) diff --git a/model/utils.go b/model/utils.go index 3cfa30470a..82c5e0953a 100644 --- a/model/utils.go +++ b/model/utils.go @@ -186,6 +186,20 @@ func (si StringInterface) Value() (driver.Value, error) { return string(j), err } +func (StringInterface) ImplementsGraphQLType(name string) bool { + return name == "StringInterface" +} + +func (si *StringInterface) UnmarshalGraphQL(input any) error { + json, ok := input.(map[string]any) + if !ok { + return errors.New("wrong type") + } + + *si = json + return nil +} + var translateFunc i18n.TranslateFunc var translateFuncOnce sync.Once