diff --git a/keyserver/src/responders/responder-validators.test.js b/keyserver/src/responders/responder-validators.test.js new file mode 100644 --- /dev/null +++ b/keyserver/src/responders/responder-validators.test.js @@ -0,0 +1,333 @@ +// @flow + +import { + logInResponseValidator, + registerResponseValidator, + logOutResponseValidator, +} from './user-responders.js'; + +describe('user responder validators', () => { + it('should validate logout response', () => { + const response = { currentUserInfo: { id: '93078', anonymous: true } }; + expect(logOutResponseValidator.is(response)).toBe(true); + response.currentUserInfo.anonymous = false; + expect(logOutResponseValidator.is(response)).toBe(false); + }); + + it('should validate register response', () => { + const response = { + id: '93079', + rawMessageInfos: [ + { + type: 1, + threadID: '93095', + creatorID: '93079', + time: 1682086407469, + initialThreadState: { + type: 6, + name: null, + parentThreadID: '1', + color: '648caa', + memberIDs: ['256', '93079'], + }, + id: '93110', + }, + { + type: 0, + threadID: '93095', + creatorID: '256', + time: 1682086407575, + text: 'welcome to Comm!', + id: '93113', + }, + ], + currentUserInfo: { id: '93079', username: 'user' }, + cookieChange: { + threadInfos: { + '1': { + id: '1', + type: 12, + name: 'GENESIS', + description: 'desc', + color: 'c85000', + creationTime: 1672934346213, + parentThreadID: null, + members: [ + { + id: '256', + role: '83796', + permissions: { + know_of: { value: true, source: '1' }, + membership: { value: false, source: null }, + visible: { value: true, source: '1' }, + voiced: { value: true, source: '1' }, + edit_entries: { value: true, source: '1' }, + edit_thread: { value: true, source: '1' }, + edit_thread_description: { value: true, source: '1' }, + edit_thread_color: { value: true, source: '1' }, + delete_thread: { value: true, source: '1' }, + create_subthreads: { value: true, source: '1' }, + create_sidebars: { value: true, source: '1' }, + join_thread: { value: false, source: null }, + edit_permissions: { value: false, source: null }, + add_members: { value: true, source: '1' }, + remove_members: { value: true, source: '1' }, + change_role: { value: true, source: '1' }, + leave_thread: { value: false, source: null }, + react_to_message: { value: true, source: '1' }, + edit_message: { value: true, source: '1' }, + }, + isSender: false, + }, + ], + roles: { + '83795': { + id: '83795', + name: 'Members', + permissions: { + know_of: true, + visible: true, + descendant_open_know_of: true, + descendant_open_visible: true, + descendant_opentoplevel_join_thread: true, + }, + isDefault: true, + }, + }, + currentUser: { + role: '83795', + permissions: { + know_of: { value: true, source: '1' }, + membership: { value: false, source: null }, + visible: { value: true, source: '1' }, + voiced: { value: false, source: null }, + edit_entries: { value: false, source: null }, + edit_thread: { value: false, source: null }, + edit_thread_description: { value: false, source: null }, + edit_thread_color: { value: false, source: null }, + delete_thread: { value: false, source: null }, + create_subthreads: { value: false, source: null }, + create_sidebars: { value: false, source: null }, + join_thread: { value: false, source: null }, + edit_permissions: { value: false, source: null }, + add_members: { value: false, source: null }, + remove_members: { value: false, source: null }, + change_role: { value: false, source: null }, + leave_thread: { value: false, source: null }, + react_to_message: { value: false, source: null }, + edit_message: { value: false, source: null }, + }, + subscription: { home: true, pushNotifs: true }, + unread: true, + }, + repliesCount: 0, + containingThreadID: null, + community: null, + }, + }, + userInfos: [ + { id: '5', username: 'commbot' }, + { id: '256', username: 'ashoat' }, + { id: '93079', username: 'temp_user7' }, + ], + }, + }; + + expect(registerResponseValidator.is(response)).toBe(true); + response.cookieChange.userInfos = undefined; + expect(registerResponseValidator.is(response)).toBe(false); + }); + + it('should validate login response', () => { + const response = { + currentUserInfo: { id: '93079', username: 'temp_user7' }, + rawMessageInfos: [ + { + type: 0, + id: '93115', + threadID: '93094', + time: 1682086407577, + creatorID: '5', + text: 'This is your private chat, where you can set', + }, + { + type: 1, + id: '93111', + threadID: '93094', + time: 1682086407467, + creatorID: '93079', + initialThreadState: { + type: 7, + name: 'temp_user7', + parentThreadID: '1', + color: '575757', + memberIDs: ['93079'], + }, + }, + ], + truncationStatuses: { '93094': 'exhaustive', '93095': 'exhaustive' }, + serverTime: 1682086579416, + userInfos: [ + { id: '5', username: 'commbot' }, + { id: '256', username: 'ashoat' }, + { id: '93079', username: 'temp_user7' }, + ], + cookieChange: { + threadInfos: { + '1': { + id: '1', + type: 12, + name: 'GENESIS', + description: + 'This is the first community on Comm. In the future it will', + color: 'c85000', + creationTime: 1672934346213, + parentThreadID: null, + members: [ + { + id: '256', + role: '83796', + permissions: { + know_of: { value: true, source: '1' }, + membership: { value: false, source: null }, + visible: { value: true, source: '1' }, + voiced: { value: true, source: '1' }, + edit_entries: { value: true, source: '1' }, + edit_thread: { value: true, source: '1' }, + edit_thread_description: { value: true, source: '1' }, + edit_thread_color: { value: true, source: '1' }, + delete_thread: { value: true, source: '1' }, + create_subthreads: { value: true, source: '1' }, + create_sidebars: { value: true, source: '1' }, + join_thread: { value: false, source: null }, + edit_permissions: { value: false, source: null }, + add_members: { value: true, source: '1' }, + remove_members: { value: true, source: '1' }, + change_role: { value: true, source: '1' }, + leave_thread: { value: false, source: null }, + react_to_message: { value: true, source: '1' }, + edit_message: { value: true, source: '1' }, + }, + isSender: false, + }, + { + id: '93079', + role: '83795', + permissions: { + know_of: { value: true, source: '1' }, + membership: { value: false, source: null }, + visible: { value: true, source: '1' }, + voiced: { value: false, source: null }, + edit_entries: { value: false, source: null }, + edit_thread: { value: false, source: null }, + edit_thread_description: { value: false, source: null }, + edit_thread_color: { value: false, source: null }, + delete_thread: { value: false, source: null }, + create_subthreads: { value: false, source: null }, + create_sidebars: { value: false, source: null }, + join_thread: { value: false, source: null }, + edit_permissions: { value: false, source: null }, + add_members: { value: false, source: null }, + remove_members: { value: false, source: null }, + change_role: { value: false, source: null }, + leave_thread: { value: false, source: null }, + react_to_message: { value: false, source: null }, + edit_message: { value: false, source: null }, + }, + isSender: false, + }, + ], + roles: { + '83795': { + id: '83795', + name: 'Members', + permissions: { + know_of: true, + visible: true, + descendant_open_know_of: true, + descendant_open_visible: true, + descendant_opentoplevel_join_thread: true, + }, + isDefault: true, + }, + '83796': { + id: '83796', + name: 'Admins', + permissions: { + know_of: true, + visible: true, + voiced: true, + react_to_message: true, + edit_message: true, + edit_entries: true, + edit_thread: true, + edit_thread_color: true, + edit_thread_description: true, + create_subthreads: true, + create_sidebars: true, + add_members: true, + delete_thread: true, + remove_members: true, + change_role: true, + descendant_know_of: true, + descendant_visible: true, + descendant_toplevel_join_thread: true, + child_join_thread: true, + descendant_voiced: true, + descendant_edit_entries: true, + descendant_edit_thread: true, + descendant_edit_thread_color: true, + descendant_edit_thread_description: true, + descendant_toplevel_create_subthreads: true, + descendant_toplevel_create_sidebars: true, + descendant_add_members: true, + descendant_delete_thread: true, + descendant_edit_permissions: true, + descendant_remove_members: true, + descendant_change_role: true, + }, + isDefault: false, + }, + }, + currentUser: { + role: '83795', + permissions: { + know_of: { value: true, source: '1' }, + membership: { value: false, source: null }, + visible: { value: true, source: '1' }, + voiced: { value: false, source: null }, + edit_entries: { value: false, source: null }, + edit_thread: { value: false, source: null }, + edit_thread_description: { value: false, source: null }, + edit_thread_color: { value: false, source: null }, + delete_thread: { value: false, source: null }, + create_subthreads: { value: false, source: null }, + create_sidebars: { value: false, source: null }, + join_thread: { value: false, source: null }, + edit_permissions: { value: false, source: null }, + add_members: { value: false, source: null }, + remove_members: { value: false, source: null }, + change_role: { value: false, source: null }, + leave_thread: { value: false, source: null }, + react_to_message: { value: false, source: null }, + edit_message: { value: false, source: null }, + }, + subscription: { home: true, pushNotifs: true }, + unread: true, + }, + repliesCount: 0, + containingThreadID: null, + community: null, + }, + }, + userInfos: [], + }, + rawEntryInfos: [], + }; + + expect(logInResponseValidator.is(response)).toBe(true); + expect( + logInResponseValidator.is({ ...response, currentUserInfo: undefined }), + ).toBe(false); + }); +}); diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -3,10 +3,14 @@ import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { ErrorTypes, SiweMessage } from 'siwe'; -import t from 'tcomb'; +import t, { type TInterface } from 'tcomb'; import bcrypt from 'twin-bcrypt'; -import { baseLegalPolicies, policies } from 'lib/facts/policies.js'; +import { + baseLegalPolicies, + policies, + policyTypeValidator, +} from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { ResetPasswordRequest, @@ -33,18 +37,33 @@ IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; -import type { CalendarQuery } from 'lib/types/entry-types.js'; -import { defaultNumberPerThread } from 'lib/types/message-types.js'; +import { + type CalendarQuery, + rawEntryInfoValidator, +} from 'lib/types/entry-types.js'; +import { + defaultNumberPerThread, + rawMessageInfoValidator, + messageTruncationStatusesValidator, +} from 'lib/types/message-types.js'; import type { SIWEAuthRequest, SIWEMessage, SIWESocialProof, } from 'lib/types/siwe-types.js'; -import type { - SubscriptionUpdateRequest, - SubscriptionUpdateResponse, +import { + type SubscriptionUpdateRequest, + type SubscriptionUpdateResponse, + threadSubscriptionValidator, } from 'lib/types/subscription-types.js'; -import type { PasswordUpdate } from 'lib/types/user-types.js'; +import { rawThreadInfoValidator } from 'lib/types/thread-types.js'; +import { + type PasswordUpdate, + loggedOutUserInfoValidator, + loggedInUserInfoValidator, + oldLoggedInUserInfoValidator, + userInfoValidator, +} from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { identityKeysBlobValidator, @@ -66,6 +85,7 @@ tEmail, tOldValidUsername, tRegex, + tID, } from 'lib/utils/validation-utils.js'; import { @@ -118,6 +138,11 @@ }), }); +export const subscriptionUpdateResponseValidator: TInterface = + tShape({ + threadSubscription: threadSubscriptionValidator, + }); + async function userSubscriptionUpdateResponder( viewer: Viewer, input: any, @@ -163,6 +188,11 @@ await checkAndSendPasswordResetEmail(request); } +export const logOutResponseValidator: TInterface = + tShape({ + currentUserInfo: loggedOutUserInfoValidator, + }); + async function logOutResponder(viewer: Viewer): Promise { await validateInput(viewer, null, null); if (viewer.loggedIn) { @@ -216,6 +246,20 @@ signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), }); +export const registerResponseValidator: TInterface = + tShape({ + id: t.String, + rawMessageInfos: t.list(rawMessageInfoValidator), + currentUserInfo: t.union([ + oldLoggedInUserInfoValidator, + loggedInUserInfoValidator, + ]), + cookieChange: tShape({ + threadInfos: t.dict(t.String, rawThreadInfoValidator), + userInfos: t.list(userInfoValidator), + }), + }); + async function accountCreationResponder( viewer: Viewer, input: any, @@ -363,6 +407,24 @@ signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), }); +export const logInResponseValidator: TInterface = + tShape({ + currentUserInfo: t.union([ + loggedInUserInfoValidator, + oldLoggedInUserInfoValidator, + ]), + rawMessageInfos: t.list(rawMessageInfoValidator), + truncationStatuses: messageTruncationStatusesValidator, + userInfos: t.list(userInfoValidator), + rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), + serverTime: t.Number, + cookieChange: tShape({ + threadInfos: t.dict(tID, rawThreadInfoValidator), + userInfos: t.list(userInfoValidator), + }), + notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), + }); + async function logInResponder( viewer: Viewer, input: any, diff --git a/lib/facts/policies.js b/lib/facts/policies.js --- a/lib/facts/policies.js +++ b/lib/facts/policies.js @@ -1,5 +1,7 @@ // @flow +import t, { type TEnums } from 'tcomb'; + import { values } from '../utils/objects.js'; export const policyTypes = Object.freeze({ @@ -9,5 +11,6 @@ export const policies: $ReadOnlyArray = values(policyTypes); export type PolicyType = $Values; +export const policyTypeValidator: TEnums = t.enums.of(policies); export const baseLegalPolicies = [policyTypes.tosAndPrivacyPolicy]; diff --git a/lib/types/account-types.js b/lib/types/account-types.js --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,5 +1,7 @@ // @flow +import t, { type TInterface } from 'tcomb'; + import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { @@ -7,21 +9,22 @@ CalendarResult, RawEntryInfo, } from './entry-types.js'; -import type { - RawMessageInfo, - MessageTruncationStatuses, - GenericMessagesResult, +import { + type RawMessageInfo, + type MessageTruncationStatuses, + type GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState } from './session-types.js'; -import type { RawThreadInfo } from './thread-types.js'; -import type { - UserInfo, - LoggedOutUserInfo, - LoggedInUserInfo, - OldLoggedInUserInfo, +import { type RawThreadInfo } from './thread-types.js'; +import { + type UserInfo, + type LoggedOutUserInfo, + type LoggedInUserInfo, + type OldLoggedInUserInfo, } from './user-types.js'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; +import { tShape } from '../utils/validation-utils.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, @@ -175,10 +178,6 @@ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); -export type DefaultNotificationPayload = { - +default_user_notifications: ?NotificationTypes, -}; - export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', @@ -189,3 +188,12 @@ export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); + +export type DefaultNotificationPayload = { + +default_user_notifications: ?NotificationTypes, +}; + +export const defaultNotificationPayloadValidator: TInterface = + tShape({ + default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), + }); diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -1,7 +1,12 @@ // @flow import invariant from 'invariant'; -import t, { type TUnion, type TInterface } from 'tcomb'; +import t, { + type TUnion, + type TDict, + type TEnums, + type TInterface, +} from 'tcomb'; import { type ClientDBMediaInfo } from './media-types.js'; import { messageTypes, type MessageType } from './message-types-enum.js'; @@ -133,6 +138,7 @@ } from './messages/update-relationship.js'; import { type RelativeUserInfo, type UserInfos } from './user-types.js'; import type { CallServerEndpointResultInfoInterface } from '../utils/call-server-endpoint.js'; +import { values } from '../utils/objects.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; const composableMessageTypes = new Set([ @@ -532,9 +538,14 @@ ); return ourMessageTruncationStatus; } +export const messageTruncationStatusValidator: TEnums = t.enums.of( + values(messageTruncationStatus), +); export type MessageTruncationStatuses = { [threadID: string]: MessageTruncationStatus, }; +export const messageTruncationStatusesValidator: TDict = + t.dict(tID, messageTruncationStatusValidator); export type ThreadCursors = { +[threadID: string]: ?string }; diff --git a/lib/types/relationship-types.js b/lib/types/relationship-types.js --- a/lib/types/relationship-types.js +++ b/lib/types/relationship-types.js @@ -1,7 +1,10 @@ // @flow +import type { TRefinement } from 'tcomb'; + import type { AccountUserInfo } from './user-types.js'; import { values } from '../utils/objects.js'; +import { tNumEnum } from '../utils/validation-utils.js'; export const undirectedStatus = Object.freeze({ KNOW_OF: 0, @@ -24,6 +27,9 @@ BOTH_BLOCKED: 6, }); export type UserRelationshipStatus = $Values; +export const userRelationshipStatusValidator: TRefinement = tNumEnum( + values(userRelationshipStatus), +); export const relationshipActions = Object.freeze({ FRIEND: 'friend', diff --git a/lib/types/user-types.js b/lib/types/user-types.js --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -1,9 +1,18 @@ // @flow -import type { DefaultNotificationPayload } from './account-types.js'; -import type { ClientAvatar } from './avatar-types.js'; -import type { UserRelationshipStatus } from './relationship-types.js'; +import t, { type TInterface } from 'tcomb'; + +import { + type DefaultNotificationPayload, + defaultNotificationPayloadValidator, +} from './account-types.js'; +import { type ClientAvatar, clientAvatarValidator } from './avatar-types.js'; +import { + type UserRelationshipStatus, + userRelationshipStatusValidator, +} from './relationship-types.js'; import type { UserInconsistencyReportCreationRequest } from './report-types.js'; +import { tBool, tShape } from '../utils/validation-utils.js'; export type GlobalUserInfo = { +id: string, @@ -23,6 +32,12 @@ +relationshipStatus?: UserRelationshipStatus, +avatar?: ?ClientAvatar, }; +export const userInfoValidator: TInterface = tShape({ + id: t.String, + username: t.maybe(t.String), + relationshipStatus: t.maybe(userRelationshipStatusValidator), + avatar: t.maybe(clientAvatarValidator), +}); export type UserInfos = { +[id: string]: UserInfo }; export type AccountUserInfo = { @@ -50,6 +65,13 @@ +email: string, +emailVerified: boolean, }; +export const oldLoggedInUserInfoValidator: TInterface = + tShape({ + id: t.String, + username: t.String, + email: t.String, + emailVerified: t.Boolean, + }); export type LoggedInUserInfo = { +id: string, @@ -57,11 +79,20 @@ +settings?: DefaultNotificationPayload, +avatar?: ?ClientAvatar, }; +export const loggedInUserInfoValidator: TInterface = + tShape({ + id: t.String, + username: t.String, + settings: t.maybe(defaultNotificationPayloadValidator), + avatar: t.maybe(clientAvatarValidator), + }); export type LoggedOutUserInfo = { +id: string, +anonymous: true, }; +export const loggedOutUserInfoValidator: TInterface = + tShape({ id: t.String, anonymous: tBool(true) }); export type OldCurrentUserInfo = OldLoggedInUserInfo | LoggedOutUserInfo; export type CurrentUserInfo = LoggedInUserInfo | LoggedOutUserInfo;