diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js index 438a76273..6a3c02d35 100644 --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -1,330 +1,562 @@ // @flow +import t from 'tcomb'; + import { baseLegalPolicies } from 'lib/facts/policies.js'; +import { + setThreadUnreadStatusResultValidator, + updateActivityResultValidator, +} from 'lib/types/activity-types.js'; import type { Endpoint } from 'lib/types/endpoints.js'; +import { inviteLinkValidator } from 'lib/types/link-types.js'; +import { uploadMultimediaResultValidator } from 'lib/types/media-types.js'; +import { getOlmSessionInitializationDataResponseValidator } from 'lib/types/request-types.js'; +import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { updateActivityResponder, threadSetUnreadStatusResponder, + setThreadUnreadStatusValidator, + updateActivityResponderInputValidator, } from './responders/activity-responders.js'; -import { deviceTokenUpdateResponder } from './responders/device-responders.js'; +import { + deviceTokenUpdateResponder, + deviceTokenUpdateRequestInputValidator, +} from './responders/device-responders.js'; import { entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, + createEntryRequestInputValidator, + saveEntryResponseValidator, + deleteEntryRequestInputValidator, + deleteEntryResponseValidator, + entryQueryInputValidator, + entryRevisionHistoryFetchInputValidator, + fetchEntryInfosResponseValidator, + fetchEntryRevisionInfosResultValidator, + deltaEntryInfosResultValidator, + newEntryQueryInputValidator, + restoreEntryRequestInputValidator, + restoreEntryResponseValidator, + saveEntryRequestInputValidator, } from './responders/entry-responders.js'; import type { JSONResponder } from './responders/handlers.js'; +import { createJSONResponder } from './responders/handlers.js'; import { getSessionPublicKeysResponder, getOlmSessionInitializationDataResponder, + getSessionPublicKeysInputValidator, + getSessionPublicKeysResponseValidator, } from './responders/keys-responders.js'; import { createOrUpdatePublicLinkResponder, disableInviteLinkResponder, fetchPrimaryInviteLinksResponder, inviteLinkVerificationResponder, + createOrUpdatePublicLinkInputValidator, + disableInviteLinkInputValidator, + fetchInviteLinksResponseValidator, + inviteLinkVerificationRequestInputValidator, + inviteLinkVerificationResponseValidator, } from './responders/link-responders.js'; -import { messageReportCreationResponder } from './responders/message-report-responder.js'; +import { + messageReportCreationResponder, + messageReportCreationRequestInputValidator, + messageReportCreationResultValidator, +} from './responders/message-report-responder.js'; import { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, + sendMessageResponseValidator, + sendMultimediaMessageRequestInputValidator, + sendReactionMessageRequestInputValidator, + editMessageRequestInputValidator, + sendEditMessageResponseValidator, + sendTextMessageRequestInputValidator, + fetchMessageInfosRequestInputValidator, + fetchMessageInfosResponseValidator, + fetchPinnedMessagesResponderInputValidator, + fetchPinnedMessagesResultValidator, + searchMessagesResponderInputValidator, + searchMessagesResponseValidator, } from './responders/message-responders.js'; -import { updateRelationshipsResponder } from './responders/relationship-responders.js'; +import { + updateRelationshipsResponder, + relationshipErrorsValidator, + updateRelationshipInputValidator, +} from './responders/relationship-responders.js'; import { reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, + reportCreationRequestInputValidator, + reportCreationResponseValidator, + fetchErrorReportInfosRequestInputValidator, + fetchErrorReportInfosResponseValidator, + reportMultiCreationRequestInputValidator, } from './responders/report-responders.js'; import { userSearchResponder, exactUserSearchResponder, + exactUserSearchRequestInputValidator, + exactUserSearchResultValidator, + userSearchRequestInputValidator, + userSearchResultValidator, } from './responders/search-responders.js'; -import { siweNonceResponder } from './responders/siwe-nonce-responders.js'; +import { + siweNonceResponder, + siweNonceResponseValidator, +} from './responders/siwe-nonce-responders.js'; import { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadFetchMediaResponder, threadJoinResponder, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, + leaveThreadResultValidator, + newThreadRequestInputValidator, + newThreadResponseValidator, + threadDeletionRequestInputValidator, + joinThreadRequestInputValidator, + leaveThreadRequestInputValidator, + threadFetchMediaRequestInputValidator, + threadFetchMediaResultValidator, + threadJoinResultValidator, + changeThreadSettingsResultValidator, + removeMembersRequestInputValidator, + roleChangeRequestInputValidator, + toggleMessagePinRequestInputValidator, + toggleMessagePinResultValidator, + updateThreadRequestInputValidator, + roleDeletionRequestInputValidator, + roleDeletionResultValidator, + roleModificationRequestInputValidator, + roleModificationResultValidator, } from './responders/thread-responders.js'; import { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, + registerRequestInputValidator, + registerResponseValidator, + deleteAccountRequestInputValidator, + logOutResponseValidator, + logInRequestInputValidator, + logInResponseValidator, + policyAcknowledgmentRequestInputValidator, + accountUpdateInputValidator, + resetPasswordRequestInputValidator, + siweAuthRequestInputValidator, + subscriptionUpdateRequestInputValidator, + subscriptionUpdateResponseValidator, + updatePasswordRequestInputValidator, + updateUserAvatarResponderValidator, + updateUserSettingsInputValidator, } from './responders/user-responders.js'; -import { codeVerificationResponder } from './responders/verification-responders.js'; -import { versionResponder } from './responders/version-responders.js'; +import { + codeVerificationResponder, + codeVerificationRequestInputValidator, +} from './responders/verification-responders.js'; +import { + versionResponder, + versionResponseValidator, +} from './responders/version-responders.js'; import { uploadMediaMetadataResponder, uploadDeletionResponder, + UploadDeletionRequestInputValidator, + uploadMediaMetadataInputValidator, } from './uploads/uploads.js'; +const ignoredArgumentValidator = t.irreducible('Ignored argument', () => true); + const jsonEndpoints: { [id: Endpoint]: JSONResponder } = { - create_account: { - responder: accountCreationResponder, - requiredPolicies: [], - }, - create_entry: { - responder: entryCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - create_error_report: { - responder: reportCreationResponder, - requiredPolicies: [], - }, - create_message_report: { - responder: messageReportCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - create_multimedia_message: { - responder: multimediaMessageCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - create_or_update_public_link: { - responder: createOrUpdatePublicLinkResponder, - requiredPolicies: baseLegalPolicies, - }, - create_reaction_message: { - responder: reactionMessageCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - disable_invite_link: { - responder: disableInviteLinkResponder, - requiredPolicies: baseLegalPolicies, - }, - edit_message: { - responder: editMessageCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - create_report: { - responder: reportCreationResponder, - requiredPolicies: [], - }, - create_reports: { - responder: reportMultiCreationResponder, - requiredPolicies: [], - }, - create_text_message: { - responder: textMessageCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - create_thread: { - responder: threadCreationResponder, - requiredPolicies: baseLegalPolicies, - }, - delete_account: { - responder: accountDeletionResponder, - requiredPolicies: [], - }, - delete_entry: { - responder: entryDeletionResponder, - requiredPolicies: baseLegalPolicies, - }, - delete_community_role: { - responder: roleDeletionResponder, - requiredPolicies: baseLegalPolicies, - }, - delete_thread: { - responder: threadDeletionResponder, - requiredPolicies: baseLegalPolicies, - }, - delete_upload: { - responder: uploadDeletionResponder, - requiredPolicies: baseLegalPolicies, - }, - exact_search_user: { - responder: exactUserSearchResponder, - requiredPolicies: [], - }, - fetch_entries: { - responder: entryFetchResponder, - requiredPolicies: baseLegalPolicies, - }, - fetch_entry_revisions: { - responder: entryRevisionFetchResponder, - requiredPolicies: baseLegalPolicies, - }, - fetch_error_report_infos: { - responder: errorReportFetchInfosResponder, - requiredPolicies: baseLegalPolicies, - }, - fetch_messages: { - responder: messageFetchResponder, - requiredPolicies: baseLegalPolicies, - }, - fetch_pinned_messages: { - responder: fetchPinnedMessagesResponder, - requiredPolicies: baseLegalPolicies, - }, - fetch_primary_invite_links: { - responder: fetchPrimaryInviteLinksResponder, - requiredPolicies: baseLegalPolicies, - }, - fetch_thread_media: { - responder: threadFetchMediaResponder, - requiredPolicies: baseLegalPolicies, - }, - get_session_public_keys: { - responder: getSessionPublicKeysResponder, - requiredPolicies: baseLegalPolicies, - }, - join_thread: { - responder: threadJoinResponder, - requiredPolicies: baseLegalPolicies, - }, - leave_thread: { - responder: threadLeaveResponder, - requiredPolicies: baseLegalPolicies, - }, - log_in: { - responder: logInResponder, - requiredPolicies: [], - }, - log_out: { - responder: logOutResponder, - requiredPolicies: [], - }, - modify_community_role: { - responder: roleModificationResponder, - requiredPolicies: baseLegalPolicies, - }, - policy_acknowledgment: { - responder: policyAcknowledgmentResponder, - requiredPolicies: [], - }, - remove_members: { - responder: memberRemovalResponder, - requiredPolicies: baseLegalPolicies, - }, - restore_entry: { - responder: entryRestorationResponder, - requiredPolicies: baseLegalPolicies, - }, - search_messages: { - responder: searchMessagesResponder, - requiredPolicies: baseLegalPolicies, - }, - search_users: { - responder: userSearchResponder, - requiredPolicies: baseLegalPolicies, - }, - send_password_reset_email: { - responder: sendPasswordResetEmailResponder, - requiredPolicies: [], - }, - send_verification_email: { - responder: sendVerificationEmailResponder, - requiredPolicies: [], - }, - set_thread_unread_status: { - responder: threadSetUnreadStatusResponder, - requiredPolicies: baseLegalPolicies, - }, - toggle_message_pin: { - responder: toggleMessagePinResponder, - requiredPolicies: baseLegalPolicies, - }, - update_account: { - responder: passwordUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - update_activity: { - responder: updateActivityResponder, - requiredPolicies: baseLegalPolicies, - }, - update_calendar_query: { - responder: calendarQueryUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - update_user_settings: { - responder: updateUserSettingsResponder, - requiredPolicies: baseLegalPolicies, - }, - update_device_token: { - responder: deviceTokenUpdateResponder, - requiredPolicies: [], - }, - update_entry: { - responder: entryUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - update_password: { - responder: oldPasswordUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - update_relationships: { - responder: updateRelationshipsResponder, - requiredPolicies: baseLegalPolicies, - }, - update_role: { - responder: roleUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - update_thread: { - responder: threadUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - update_user_subscription: { - responder: userSubscriptionUpdateResponder, - requiredPolicies: baseLegalPolicies, - }, - verify_code: { - responder: codeVerificationResponder, - requiredPolicies: baseLegalPolicies, - }, - verify_invite_link: { - responder: inviteLinkVerificationResponder, - requiredPolicies: baseLegalPolicies, - }, - siwe_nonce: { - responder: siweNonceResponder, - requiredPolicies: [], - }, - siwe_auth: { - responder: siweAuthResponder, - requiredPolicies: [], - }, - update_user_avatar: { - responder: updateUserAvatarResponder, - requiredPolicies: baseLegalPolicies, - }, - upload_media_metadata: { - responder: uploadMediaMetadataResponder, - requiredPolicies: baseLegalPolicies, - }, - get_olm_session_initialization_data: { - responder: getOlmSessionInitializationDataResponder, - requiredPolicies: [], - }, - version: { - responder: versionResponder, - requiredPolicies: [], - }, + create_account: createJSONResponder( + accountCreationResponder, + registerRequestInputValidator, + registerResponseValidator, + [], + ), + create_entry: createJSONResponder( + entryCreationResponder, + createEntryRequestInputValidator, + saveEntryResponseValidator, + baseLegalPolicies, + ), + create_error_report: createJSONResponder( + reportCreationResponder, + reportCreationRequestInputValidator, + reportCreationResponseValidator, + [], + ), + create_message_report: createJSONResponder( + messageReportCreationResponder, + messageReportCreationRequestInputValidator, + messageReportCreationResultValidator, + baseLegalPolicies, + ), + create_multimedia_message: createJSONResponder( + multimediaMessageCreationResponder, + sendMultimediaMessageRequestInputValidator, + sendMessageResponseValidator, + baseLegalPolicies, + ), + create_or_update_public_link: createJSONResponder( + createOrUpdatePublicLinkResponder, + createOrUpdatePublicLinkInputValidator, + inviteLinkValidator, + baseLegalPolicies, + ), + create_reaction_message: createJSONResponder( + reactionMessageCreationResponder, + sendReactionMessageRequestInputValidator, + sendMessageResponseValidator, + baseLegalPolicies, + ), + disable_invite_link: createJSONResponder( + disableInviteLinkResponder, + disableInviteLinkInputValidator, + t.Nil, + baseLegalPolicies, + ), + edit_message: createJSONResponder( + editMessageCreationResponder, + editMessageRequestInputValidator, + sendEditMessageResponseValidator, + baseLegalPolicies, + ), + create_report: createJSONResponder( + reportCreationResponder, + reportCreationRequestInputValidator, + reportCreationResponseValidator, + [], + ), + create_reports: createJSONResponder( + reportMultiCreationResponder, + reportMultiCreationRequestInputValidator, + t.Nil, + [], + ), + create_text_message: createJSONResponder( + textMessageCreationResponder, + sendTextMessageRequestInputValidator, + sendMessageResponseValidator, + baseLegalPolicies, + ), + create_thread: createJSONResponder( + threadCreationResponder, + newThreadRequestInputValidator, + newThreadResponseValidator, + baseLegalPolicies, + ), + delete_account: createJSONResponder( + accountDeletionResponder, + deleteAccountRequestInputValidator, + logOutResponseValidator, + [], + ), + delete_entry: createJSONResponder( + entryDeletionResponder, + deleteEntryRequestInputValidator, + deleteEntryResponseValidator, + baseLegalPolicies, + ), + delete_community_role: createJSONResponder( + roleDeletionResponder, + roleDeletionRequestInputValidator, + roleDeletionResultValidator, + baseLegalPolicies, + ), + delete_thread: createJSONResponder( + threadDeletionResponder, + threadDeletionRequestInputValidator, + leaveThreadResultValidator, + baseLegalPolicies, + ), + delete_upload: createJSONResponder( + uploadDeletionResponder, + UploadDeletionRequestInputValidator, + t.Nil, + baseLegalPolicies, + ), + exact_search_user: createJSONResponder( + exactUserSearchResponder, + exactUserSearchRequestInputValidator, + exactUserSearchResultValidator, + [], + ), + fetch_entries: createJSONResponder( + entryFetchResponder, + entryQueryInputValidator, + fetchEntryInfosResponseValidator, + baseLegalPolicies, + ), + fetch_entry_revisions: createJSONResponder( + entryRevisionFetchResponder, + entryRevisionHistoryFetchInputValidator, + fetchEntryRevisionInfosResultValidator, + baseLegalPolicies, + ), + fetch_error_report_infos: createJSONResponder( + errorReportFetchInfosResponder, + fetchErrorReportInfosRequestInputValidator, + fetchErrorReportInfosResponseValidator, + baseLegalPolicies, + ), + fetch_messages: createJSONResponder( + messageFetchResponder, + fetchMessageInfosRequestInputValidator, + fetchMessageInfosResponseValidator, + baseLegalPolicies, + ), + fetch_pinned_messages: createJSONResponder( + fetchPinnedMessagesResponder, + fetchPinnedMessagesResponderInputValidator, + fetchPinnedMessagesResultValidator, + baseLegalPolicies, + ), + fetch_primary_invite_links: createJSONResponder( + fetchPrimaryInviteLinksResponder, + ignoredArgumentValidator, + fetchInviteLinksResponseValidator, + baseLegalPolicies, + ), + fetch_thread_media: createJSONResponder( + threadFetchMediaResponder, + threadFetchMediaRequestInputValidator, + threadFetchMediaResultValidator, + baseLegalPolicies, + ), + get_session_public_keys: createJSONResponder( + getSessionPublicKeysResponder, + getSessionPublicKeysInputValidator, + getSessionPublicKeysResponseValidator, + baseLegalPolicies, + ), + join_thread: createJSONResponder( + threadJoinResponder, + joinThreadRequestInputValidator, + threadJoinResultValidator, + baseLegalPolicies, + ), + leave_thread: createJSONResponder( + threadLeaveResponder, + leaveThreadRequestInputValidator, + leaveThreadResultValidator, + baseLegalPolicies, + ), + log_in: createJSONResponder( + logInResponder, + logInRequestInputValidator, + logInResponseValidator, + [], + ), + log_out: createJSONResponder( + logOutResponder, + ignoredArgumentValidator, + logOutResponseValidator, + [], + ), + modify_community_role: createJSONResponder( + roleModificationResponder, + roleModificationRequestInputValidator, + roleModificationResultValidator, + baseLegalPolicies, + ), + policy_acknowledgment: createJSONResponder( + policyAcknowledgmentResponder, + policyAcknowledgmentRequestInputValidator, + t.Nil, + [], + ), + remove_members: createJSONResponder( + memberRemovalResponder, + removeMembersRequestInputValidator, + changeThreadSettingsResultValidator, + baseLegalPolicies, + ), + restore_entry: createJSONResponder( + entryRestorationResponder, + restoreEntryRequestInputValidator, + restoreEntryResponseValidator, + baseLegalPolicies, + ), + search_messages: createJSONResponder( + searchMessagesResponder, + searchMessagesResponderInputValidator, + searchMessagesResponseValidator, + baseLegalPolicies, + ), + search_users: createJSONResponder( + userSearchResponder, + userSearchRequestInputValidator, + userSearchResultValidator, + baseLegalPolicies, + ), + send_password_reset_email: createJSONResponder( + sendPasswordResetEmailResponder, + resetPasswordRequestInputValidator, + t.Nil, + [], + ), + send_verification_email: createJSONResponder( + sendVerificationEmailResponder, + ignoredArgumentValidator, + t.Nil, + [], + ), + set_thread_unread_status: createJSONResponder( + threadSetUnreadStatusResponder, + setThreadUnreadStatusValidator, + setThreadUnreadStatusResultValidator, + baseLegalPolicies, + ), + toggle_message_pin: createJSONResponder( + toggleMessagePinResponder, + toggleMessagePinRequestInputValidator, + toggleMessagePinResultValidator, + baseLegalPolicies, + ), + update_account: createJSONResponder( + passwordUpdateResponder, + accountUpdateInputValidator, + t.Nil, + baseLegalPolicies, + ), + update_activity: createJSONResponder( + updateActivityResponder, + updateActivityResponderInputValidator, + updateActivityResultValidator, + baseLegalPolicies, + ), + update_calendar_query: createJSONResponder( + calendarQueryUpdateResponder, + newEntryQueryInputValidator, + deltaEntryInfosResultValidator, + baseLegalPolicies, + ), + update_user_settings: createJSONResponder( + updateUserSettingsResponder, + updateUserSettingsInputValidator, + t.Nil, + baseLegalPolicies, + ), + update_device_token: createJSONResponder( + deviceTokenUpdateResponder, + deviceTokenUpdateRequestInputValidator, + t.Nil, + [], + ), + update_entry: createJSONResponder( + entryUpdateResponder, + saveEntryRequestInputValidator, + saveEntryResponseValidator, + baseLegalPolicies, + ), + update_password: createJSONResponder( + oldPasswordUpdateResponder, + updatePasswordRequestInputValidator, + logInResponseValidator, + baseLegalPolicies, + ), + update_relationships: createJSONResponder( + updateRelationshipsResponder, + updateRelationshipInputValidator, + relationshipErrorsValidator, + baseLegalPolicies, + ), + update_role: createJSONResponder( + roleUpdateResponder, + roleChangeRequestInputValidator, + changeThreadSettingsResultValidator, + baseLegalPolicies, + ), + update_thread: createJSONResponder( + threadUpdateResponder, + updateThreadRequestInputValidator, + changeThreadSettingsResultValidator, + baseLegalPolicies, + ), + update_user_subscription: createJSONResponder( + userSubscriptionUpdateResponder, + subscriptionUpdateRequestInputValidator, + subscriptionUpdateResponseValidator, + baseLegalPolicies, + ), + verify_code: createJSONResponder( + codeVerificationResponder, + codeVerificationRequestInputValidator, + t.Nil, + baseLegalPolicies, + ), + verify_invite_link: createJSONResponder( + inviteLinkVerificationResponder, + inviteLinkVerificationRequestInputValidator, + inviteLinkVerificationResponseValidator, + baseLegalPolicies, + ), + siwe_nonce: createJSONResponder( + siweNonceResponder, + ignoredArgumentValidator, + siweNonceResponseValidator, + [], + ), + siwe_auth: createJSONResponder( + siweAuthResponder, + siweAuthRequestInputValidator, + logInResponseValidator, + [], + ), + update_user_avatar: createJSONResponder( + updateUserAvatarResponder, + updateUserAvatarRequestValidator, + updateUserAvatarResponderValidator, + baseLegalPolicies, + ), + upload_media_metadata: createJSONResponder( + uploadMediaMetadataResponder, + uploadMediaMetadataInputValidator, + uploadMultimediaResultValidator, + baseLegalPolicies, + ), + get_olm_session_initialization_data: createJSONResponder( + getOlmSessionInitializationDataResponder, + ignoredArgumentValidator, + getOlmSessionInitializationDataResponseValidator, + [], + ), + version: createJSONResponder( + versionResponder, + ignoredArgumentValidator, + versionResponseValidator, + [], + ), }; export { jsonEndpoints }; diff --git a/keyserver/src/responders/activity-responders.js b/keyserver/src/responders/activity-responders.js index 42758d7d2..f8d9f2227 100644 --- a/keyserver/src/responders/activity-responders.js +++ b/keyserver/src/responders/activity-responders.js @@ -1,76 +1,57 @@ // @flow -import t from 'tcomb'; -import type { TList } from 'tcomb'; +import t, { type TInterface, type TList } from 'tcomb'; import { type UpdateActivityResult, type UpdateActivityRequest, type SetThreadUnreadStatusRequest, type SetThreadUnreadStatusResult, type ActivityUpdate, - setThreadUnreadStatusResult, - updateActivityResultValidator, } from 'lib/types/activity-types.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; import { activityUpdater, setThreadUnreadStatus, } from '../updaters/activity-updaters.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; const activityUpdatesInputValidator: TList> = t.list( tShape({ focus: t.Bool, threadID: tID, latestMessage: t.maybe(tID), }), ); -const inputValidator = tShape({ - updates: activityUpdatesInputValidator, -}); +export const updateActivityResponderInputValidator: TInterface = + tShape({ + updates: activityUpdatesInputValidator, + }); async function updateActivityResponder( viewer: Viewer, - input: mixed, + request: UpdateActivityRequest, ): Promise { - const request = await validateInput(viewer, inputValidator, input); - const result = await activityUpdater(viewer, request); - return validateOutput( - viewer.platformDetails, - updateActivityResultValidator, - result, - ); + return await activityUpdater(viewer, request); } -const setThreadUnreadStatusValidator = tShape({ - threadID: tID, - unread: t.Bool, - latestMessage: t.maybe(tID), -}); +export const setThreadUnreadStatusValidator: TInterface = + tShape({ + threadID: tID, + unread: t.Bool, + latestMessage: t.maybe(tID), + }); async function threadSetUnreadStatusResponder( viewer: Viewer, - input: mixed, + request: SetThreadUnreadStatusRequest, ): Promise { - const request = await validateInput( - viewer, - setThreadUnreadStatusValidator, - input, - ); - - const result = await setThreadUnreadStatus(viewer, request); - return validateOutput( - viewer.platformDetails, - setThreadUnreadStatusResult, - result, - ); + return await setThreadUnreadStatus(viewer, request); } export { activityUpdatesInputValidator, updateActivityResponder, threadSetUnreadStatusResponder, }; diff --git a/keyserver/src/responders/device-responders.js b/keyserver/src/responders/device-responders.js index 5c4022b60..592054174 100644 --- a/keyserver/src/responders/device-responders.js +++ b/keyserver/src/responders/device-responders.js @@ -1,29 +1,26 @@ // @flow import t from 'tcomb'; import type { TInterface } from 'tcomb'; import type { DeviceTokenUpdateRequest } from 'lib/types/device-types.js'; import { tShape, tPlatformDetails } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; import { deviceTokenUpdater } from '../updaters/device-token-updaters.js'; -import { validateInput } from '../utils/validation-utils.js'; const deviceTokenUpdateRequestInputValidator: TInterface = - tShape({ + tShape({ deviceToken: t.maybe(t.String), deviceType: t.maybe(t.enums.of(['ios', 'android'])), platformDetails: t.maybe(tPlatformDetails), }); async function deviceTokenUpdateResponder( viewer: Viewer, - input: any, + request: DeviceTokenUpdateRequest, ): Promise { - const request: DeviceTokenUpdateRequest = input; - await validateInput(viewer, deviceTokenUpdateRequestInputValidator, request); await deviceTokenUpdater(viewer, request); } export { deviceTokenUpdateRequestInputValidator, deviceTokenUpdateResponder }; diff --git a/keyserver/src/responders/entry-responders.js b/keyserver/src/responders/entry-responders.js index 270758f73..a625cf37b 100644 --- a/keyserver/src/responders/entry-responders.js +++ b/keyserver/src/responders/entry-responders.js @@ -1,365 +1,300 @@ // @flow import t from 'tcomb'; import type { TInterface } from 'tcomb'; import { filteredThreadIDs } from 'lib/selectors/calendar-filter-selectors.js'; import { type CalendarQuery, type SaveEntryRequest, type CreateEntryRequest, type DeleteEntryRequest, type DeleteEntryResponse, type RestoreEntryRequest, type RestoreEntryResponse, type FetchEntryInfosResponse, type DeltaEntryInfosResult, type SaveEntryResponse, rawEntryInfoValidator, } from 'lib/types/entry-types.js'; import { type CalendarFilter, calendarThreadFilterTypes, } from 'lib/types/filter-types.js'; import { type FetchEntryRevisionInfosResult, type FetchEntryRevisionInfosRequest, historyRevisionInfoValidator, } from 'lib/types/history-types.js'; import { rawMessageInfoValidator } from 'lib/types/message-types.js'; import { serverCreateUpdatesResponseValidator } from 'lib/types/update-types.js'; import { accountUserInfoValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tString, tShape, tDate, tID } from 'lib/utils/validation-utils.js'; import createEntry from '../creators/entry-creator.js'; import { deleteEntry, restoreEntry } from '../deleters/entry-deleters.js'; import { fetchEntryInfos, fetchEntryRevisionInfo, fetchEntriesForSession, } from '../fetchers/entry-fetchers.js'; import { verifyThreadIDs } from '../fetchers/thread-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateEntry, compareNewCalendarQuery, } from '../updaters/entry-updaters.js'; import { commitSessionUpdate } from '../updaters/session-updaters.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; type EntryQueryInput = { +startDate: string, +endDate: string, +navID?: ?string, +includeDeleted?: ?boolean, +filters?: ?$ReadOnlyArray, }; -const entryQueryInputValidator: TInterface = tShape({ - navID: t.maybe(t.String), - startDate: tDate, - endDate: tDate, - includeDeleted: t.maybe(t.Boolean), - filters: t.maybe( - t.list( - t.union([ - tShape({ - type: tString(calendarThreadFilterTypes.NOT_DELETED), - }), - tShape({ - type: tString(calendarThreadFilterTypes.THREAD_LIST), - threadIDs: t.list(tID), - }), - ]), +const entryQueryInputValidator: TInterface = + tShape({ + navID: t.maybe(t.String), + startDate: tDate, + endDate: tDate, + includeDeleted: t.maybe(t.Boolean), + filters: t.maybe( + t.list( + t.union([ + tShape({ + type: tString(calendarThreadFilterTypes.NOT_DELETED), + }), + tShape({ + type: tString(calendarThreadFilterTypes.THREAD_LIST), + threadIDs: t.list(tID), + }), + ]), + ), ), - ), -}); + }); const newEntryQueryInputValidator: TInterface = tShape({ startDate: tDate, endDate: tDate, filters: t.list( t.union([ tShape({ type: tString(calendarThreadFilterTypes.NOT_DELETED), }), tShape({ type: tString(calendarThreadFilterTypes.THREAD_LIST), threadIDs: t.list(tID), }), ]), ), }); function normalizeCalendarQuery(input: any): CalendarQuery { if (input.filters) { return { startDate: input.startDate, endDate: input.endDate, filters: input.filters, }; } const filters = []; if (!input.includeDeleted) { filters.push({ type: calendarThreadFilterTypes.NOT_DELETED }); } if (input.navID !== 'home') { filters.push({ type: calendarThreadFilterTypes.THREAD_LIST, threadIDs: [input.navID], }); } return { startDate: input.startDate, endDate: input.endDate, filters, }; } async function verifyCalendarQueryThreadIDs( request: CalendarQuery, ): Promise { const threadIDsToFilterTo = filteredThreadIDs(request.filters); if (threadIDsToFilterTo && threadIDsToFilterTo.size > 0) { const verifiedThreadIDs = await verifyThreadIDs([...threadIDsToFilterTo]); if (verifiedThreadIDs.length !== threadIDsToFilterTo.size) { throw new ServerError('invalid_parameters'); } } } export const fetchEntryInfosResponseValidator: TInterface = tShape({ rawEntryInfos: t.list(rawEntryInfoValidator), userInfos: t.dict(t.String, accountUserInfoValidator), }); async function entryFetchResponder( viewer: Viewer, - input: mixed, + inputQuery: EntryQueryInput, ): Promise { - const inputQuery = await validateInput( - viewer, - entryQueryInputValidator, - input, - ); const request = normalizeCalendarQuery(inputQuery); await verifyCalendarQueryThreadIDs(request); const response = await fetchEntryInfos(viewer, [request]); - return validateOutput( - viewer.platformDetails, - fetchEntryInfosResponseValidator, - { - ...response, - userInfos: {}, - }, - ); + return { + ...response, + userInfos: {}, + }; } -const entryRevisionHistoryFetchInputValidator = +export const entryRevisionHistoryFetchInputValidator: TInterface = tShape({ id: tID, }); export const fetchEntryRevisionInfosResultValidator: TInterface = tShape({ result: t.list(historyRevisionInfoValidator), }); async function entryRevisionFetchResponder( viewer: Viewer, - input: mixed, + request: FetchEntryRevisionInfosRequest, ): Promise { - const request = await validateInput( - viewer, - entryRevisionHistoryFetchInputValidator, - input, - ); const entryHistory = await fetchEntryRevisionInfo(viewer, request.id); - const response = { result: entryHistory }; - return validateOutput( - viewer.platformDetails, - fetchEntryRevisionInfosResultValidator, - response, - ); + return { result: entryHistory }; } -const createEntryRequestInputValidator = tShape({ - text: t.String, - sessionID: t.maybe(t.String), - timestamp: t.Number, - date: tDate, - threadID: tID, - localID: t.maybe(t.String), - calendarQuery: t.maybe(newEntryQueryInputValidator), -}); +export const createEntryRequestInputValidator: TInterface = + tShape({ + text: t.String, + sessionID: t.maybe(t.String), + timestamp: t.Number, + date: tDate, + threadID: tID, + localID: t.maybe(t.String), + calendarQuery: t.maybe(newEntryQueryInputValidator), + }); export const saveEntryResponseValidator: TInterface = tShape({ entryID: tID, newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: serverCreateUpdatesResponseValidator, }); async function entryCreationResponder( viewer: Viewer, - input: mixed, + request: CreateEntryRequest, ): Promise { - const request = await validateInput( - viewer, - createEntryRequestInputValidator, - input, - ); - const response = await createEntry(viewer, request); - return validateOutput( - viewer.platformDetails, - saveEntryResponseValidator, - response, - ); + return await createEntry(viewer, request); } -const saveEntryRequestInputValidator = tShape({ - entryID: tID, - text: t.String, - prevText: t.String, - sessionID: t.maybe(t.String), - timestamp: t.Number, - calendarQuery: t.maybe(newEntryQueryInputValidator), -}); +export const saveEntryRequestInputValidator: TInterface = + tShape({ + entryID: tID, + text: t.String, + prevText: t.String, + sessionID: t.maybe(t.String), + timestamp: t.Number, + calendarQuery: t.maybe(newEntryQueryInputValidator), + }); async function entryUpdateResponder( viewer: Viewer, - input: mixed, + request: SaveEntryRequest, ): Promise { - const request = await validateInput( - viewer, - saveEntryRequestInputValidator, - input, - ); - const response = await updateEntry(viewer, request); - return validateOutput( - viewer.platformDetails, - saveEntryResponseValidator, - response, - ); + return await updateEntry(viewer, request); } -const deleteEntryRequestInputValidator = tShape({ - entryID: tID, - prevText: t.String, - sessionID: t.maybe(t.String), - timestamp: t.Number, - calendarQuery: t.maybe(newEntryQueryInputValidator), -}); +export const deleteEntryRequestInputValidator: TInterface = + tShape({ + entryID: tID, + prevText: t.String, + sessionID: t.maybe(t.String), + timestamp: t.Number, + calendarQuery: t.maybe(newEntryQueryInputValidator), + }); export const deleteEntryResponseValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, updatesResult: serverCreateUpdatesResponseValidator, }); async function entryDeletionResponder( viewer: Viewer, - input: mixed, + request: DeleteEntryRequest, ): Promise { - const request = await validateInput( - viewer, - deleteEntryRequestInputValidator, - input, - ); - const response = await deleteEntry(viewer, request); - return validateOutput( - viewer.platformDetails, - deleteEntryResponseValidator, - response, - ); + return await deleteEntry(viewer, request); } -const restoreEntryRequestInputValidator = tShape({ - entryID: tID, - sessionID: t.maybe(t.String), - timestamp: t.Number, - calendarQuery: t.maybe(newEntryQueryInputValidator), -}); +export const restoreEntryRequestInputValidator: TInterface = + tShape({ + entryID: tID, + sessionID: t.maybe(t.String), + timestamp: t.Number, + calendarQuery: t.maybe(newEntryQueryInputValidator), + }); export const restoreEntryResponseValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: serverCreateUpdatesResponseValidator, }); async function entryRestorationResponder( viewer: Viewer, - input: mixed, + request: RestoreEntryRequest, ): Promise { - const request = await validateInput( - viewer, - restoreEntryRequestInputValidator, - input, - ); - const response = await restoreEntry(viewer, request); - return validateOutput( - viewer.platformDetails, - restoreEntryResponseValidator, - response, - ); + return await restoreEntry(viewer, request); } export const deltaEntryInfosResultValidator: TInterface = tShape({ rawEntryInfos: t.list(rawEntryInfoValidator), deletedEntryIDs: t.list(tID), userInfos: t.list(accountUserInfoValidator), }); async function calendarQueryUpdateResponder( viewer: Viewer, - input: mixed, + request: CalendarQuery, ): Promise { - const request = await validateInput( - viewer, - newEntryQueryInputValidator, - input, - ); - await verifyCalendarQueryThreadIDs(request); if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const { difference, oldCalendarQuery, sessionUpdate } = compareNewCalendarQuery(viewer, request); const [response] = await Promise.all([ fetchEntriesForSession(viewer, difference, oldCalendarQuery), commitSessionUpdate(viewer, sessionUpdate), ]); - return validateOutput( - viewer.platformDetails, - deltaEntryInfosResultValidator, - { - rawEntryInfos: response.rawEntryInfos, - deletedEntryIDs: response.deletedEntryIDs, - // Old clients expect userInfos object - userInfos: [], - }, - ); + return { + rawEntryInfos: response.rawEntryInfos, + deletedEntryIDs: response.deletedEntryIDs, + // Old clients expect userInfos object + userInfos: [], + }; } export { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, }; diff --git a/keyserver/src/responders/handlers.js b/keyserver/src/responders/handlers.js index 4b064b764..2ecc83081 100644 --- a/keyserver/src/responders/handlers.js +++ b/keyserver/src/responders/handlers.js @@ -1,266 +1,290 @@ // @flow import type { $Response, $Request } from 'express'; +import type { TType } from 'tcomb'; import { ServerError } from 'lib/utils/errors.js'; import { assertWithValidator, tPlatformDetails, } from 'lib/utils/validation-utils.js'; import { getMessageForException } from './utils.js'; import { deleteCookie } from '../deleters/cookie-deleters.js'; import type { PolicyType } from '../lib/facts/policies.js'; import { fetchViewerForJSONRequest, addCookieToJSONResponse, fetchViewerForHomeRequest, addCookieToHomeResponse, createNewAnonymousCookie, setCookiePlatformDetails, } from '../session/cookies.js'; import type { Viewer } from '../session/viewer.js'; import { type AppURLFacts, getAppURLFactsFromRequestURL, } from '../utils/urls.js'; -import { policiesValidator } from '../utils/validation-utils.js'; +import { + policiesValidator, + validateInput, + validateOutput, +} from '../utils/validation-utils.js'; -export type JSONResponder = { +type InnerJSONResponder = { responder: (viewer: Viewer, input: any) => Promise<*>, requiredPolicies: $ReadOnlyArray, }; +export opaque type JSONResponder: InnerJSONResponder = InnerJSONResponder; + +function createJSONResponder( + responder: (Viewer, input: I) => Promise, + inputValidator: TType, + outputValidator: TType, + requiredPolicies: $ReadOnlyArray, +): JSONResponder { + return { + responder: async (viewer, input) => { + const request = await validateInput(viewer, inputValidator, input); + const result = await responder(viewer, request); + return validateOutput(viewer.platformDetails, outputValidator, result); + }, + requiredPolicies, + }; +} + export type DownloadResponder = ( viewer: Viewer, req: $Request, res: $Response, ) => Promise; export type HTMLResponder = DownloadResponder; export type HTTPGetResponder = DownloadResponder; function jsonHandler( responder: JSONResponder, expectCookieInvalidation: boolean, ): (req: $Request, res: $Response) => Promise { return async (req: $Request, res: $Response) => { let viewer; try { if (!req.body || typeof req.body !== 'object') { throw new ServerError('invalid_parameters'); } const { input, platformDetails } = req.body; viewer = await fetchViewerForJSONRequest(req); const promises = [policiesValidator(viewer, responder.requiredPolicies)]; if (platformDetails) { if (!tPlatformDetails.is(platformDetails)) { throw new ServerError('invalid_platform_details'); } promises.push( setCookiePlatformDetails( viewer, assertWithValidator(platformDetails, tPlatformDetails), ), ); } await Promise.all(promises); const responderResult = await responder.responder(viewer, input); if (res.headersSent) { return; } const result = { ...responderResult }; addCookieToJSONResponse( viewer, res, result, expectCookieInvalidation, getAppURLFactsFromRequestURL(req.originalUrl), ); res.json({ success: true, ...result }); } catch (e) { await handleException( e, res, getAppURLFactsFromRequestURL(req.originalUrl), viewer, expectCookieInvalidation, ); } }; } function httpGetHandler( responder: HTTPGetResponder, ): (req: $Request, res: $Response) => Promise { return async (req: $Request, res: $Response) => { let viewer; try { viewer = await fetchViewerForJSONRequest(req); await responder(viewer, req, res); } catch (e) { await handleException( e, res, getAppURLFactsFromRequestURL(req.originalUrl), viewer, ); } }; } function downloadHandler( responder: DownloadResponder, ): (req: $Request, res: $Response) => Promise { return async (req: $Request, res: $Response) => { try { const viewer = await fetchViewerForJSONRequest(req); await responder(viewer, req, res); } catch (e) { // Passing viewer in only makes sense if we want to handle failures as // JSON. We don't, and presume all download handlers avoid ServerError. await handleException( e, res, getAppURLFactsFromRequestURL(req.originalUrl), ); } }; } async function handleException( error: Error, res: $Response, appURLFacts: AppURLFacts, viewer?: ?Viewer, expectCookieInvalidation?: boolean, ) { console.warn(error); if (res.headersSent) { return; } if (!(error instanceof ServerError)) { res.status(500).send(getMessageForException(error)); return; } const result: Object = error.payload ? { error: error.message, payload: error.payload } : { error: error.message }; if (viewer) { if (error.message === 'client_version_unsupported' && viewer.loggedIn) { // If the client version is unsupported, log the user out const { platformDetails } = error; const [data] = await Promise.all([ createNewAnonymousCookie({ platformDetails, deviceToken: viewer.deviceToken, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(data); viewer.cookieInvalidated = true; } // This can mutate the result object addCookieToJSONResponse( viewer, res, result, !!expectCookieInvalidation, appURLFacts, ); } res.json(result); } function htmlHandler( responder: HTMLResponder, ): (req: $Request, res: $Response) => Promise { return async (req: $Request, res: $Response) => { try { const viewer = await fetchViewerForHomeRequest(req); addCookieToHomeResponse( viewer, res, getAppURLFactsFromRequestURL(req.originalUrl), ); res.type('html'); await responder(viewer, req, res); } catch (e) { console.warn(e); if (!res.headersSent) { res.status(500).send(getMessageForException(e)); } } }; } type MulterFile = { fieldname: string, originalname: string, encoding: string, mimetype: string, buffer: Buffer, size: number, }; export type MulterRequest = $Request & { files?: $ReadOnlyArray, ... }; type UploadResponder = (viewer: Viewer, req: MulterRequest) => Promise; function uploadHandler( responder: UploadResponder, ): (req: $Request, res: $Response) => Promise { return async (req: $Request, res: $Response) => { let viewer; try { if (!req.body || typeof req.body !== 'object') { throw new ServerError('invalid_parameters'); } viewer = await fetchViewerForJSONRequest(req); const responderResult = await responder( viewer, ((req: any): MulterRequest), ); if (res.headersSent) { return; } const result = { ...responderResult }; addCookieToJSONResponse( viewer, res, result, false, getAppURLFactsFromRequestURL(req.originalUrl), ); res.json({ success: true, ...result }); } catch (e) { await handleException( e, res, getAppURLFactsFromRequestURL(req.originalUrl), viewer, ); } }; } async function handleAsyncPromise(promise: Promise) { try { await promise; } catch (error) { console.warn(error); } } export { + createJSONResponder, jsonHandler, httpGetHandler, downloadHandler, htmlHandler, uploadHandler, handleAsyncPromise, }; diff --git a/keyserver/src/responders/keys-responders.js b/keyserver/src/responders/keys-responders.js index 254efd98e..c4fca7794 100644 --- a/keyserver/src/responders/keys-responders.js +++ b/keyserver/src/responders/keys-responders.js @@ -1,149 +1,139 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; -import t, { type TUnion } from 'tcomb'; +import t, { type TUnion, type TInterface } from 'tcomb'; import type { OlmSessionInitializationInfo, GetOlmSessionInitializationDataResponse, GetSessionPublicKeysArgs, } from 'lib/types/request-types.js'; import { type SessionPublicKeys, sessionPublicKeysValidator, } from 'lib/types/session-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tShape, tNull } from 'lib/utils/validation-utils.js'; import { fetchSessionPublicKeys } from '../fetchers/key-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; import { validateAccountPrekey } from '../utils/olm-utils.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; type AccountKeysSet = { +identityKeys: string, ...OlmSessionInitializationInfo, }; -const getSessionPublicKeysInputValidator = tShape({ - session: t.String, -}); +export const getSessionPublicKeysInputValidator: TInterface = + tShape({ + session: t.String, + }); type GetSessionPublicKeysResponse = SessionPublicKeys | null; export const getSessionPublicKeysResponseValidator: TUnion = t.union([sessionPublicKeysValidator, tNull]); async function getSessionPublicKeysResponder( viewer: Viewer, - input: mixed, + request: GetSessionPublicKeysArgs, ): Promise { if (!viewer.loggedIn) { return null; } - const request = await validateInput( - viewer, - getSessionPublicKeysInputValidator, - input, - ); - const response = await fetchSessionPublicKeys(request.session); - return validateOutput( - viewer.platformDetails, - getSessionPublicKeysResponseValidator, - response, - ); + return await fetchSessionPublicKeys(request.session); } async function retrieveAccountKeysSet( account: OlmAccount, ): Promise { const identityKeys = account.identity_keys(); await validateAccountPrekey(account); const prekey = account.prekey(); // Until transfer of prekeys to the identity service is implemented // prekeys will be marked as published each time it is accessed // to establish olm notifs session to mitigate the risk of prekeys // being in use for long enough to cause security concerns account.mark_prekey_as_published(); const prekeySignature = account.prekey_signature(); if (!prekeySignature) { throw new ServerError('prekey_validation_failure'); } account.generate_one_time_keys(1); const oneTimeKey = account.one_time_keys(); account.mark_keys_as_published(); return { identityKeys, oneTimeKey, prekey, prekeySignature }; } async function getOlmSessionInitializationDataResponder( viewer: Viewer, ): Promise { await verifyClientSupported(viewer); const { identityKeys: notificationsIdentityKeys, prekey: notificationsPrekey, prekeySignature: notificationsPrekeySignature, oneTimeKey: notificationsOneTimeKey, } = await fetchCallUpdateOlmAccount('notifications', retrieveAccountKeysSet); const contentAccountCallback = async (account: OlmAccount) => { const { identityKeys: contentIdentityKeys, oneTimeKey, prekey, prekeySignature, } = await retrieveAccountKeysSet(account); const identityKeysBlob = { primaryIdentityPublicKeys: JSON.parse(contentIdentityKeys), notificationIdentityPublicKeys: JSON.parse(notificationsIdentityKeys), }; const identityKeysBlobPayload = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob = { payload: identityKeysBlobPayload, signature: account.sign(identityKeysBlobPayload), }; return { signedIdentityKeysBlob, oneTimeKey, prekey, prekeySignature, }; }; const { signedIdentityKeysBlob, prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKey: contentOneTimeKey, } = await fetchCallUpdateOlmAccount('content', contentAccountCallback); const notifInitializationInfo = { prekey: notificationsPrekey, prekeySignature: notificationsPrekeySignature, oneTimeKey: notificationsOneTimeKey, }; const contentInitializationInfo = { prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKey: contentOneTimeKey, }; return { signedIdentityKeysBlob, contentInitializationInfo, notifInitializationInfo, }; } export { getSessionPublicKeysResponder, getOlmSessionInitializationDataResponder, }; diff --git a/keyserver/src/responders/link-responders.js b/keyserver/src/responders/link-responders.js index c43f6feaf..e26b5f7a0 100644 --- a/keyserver/src/responders/link-responders.js +++ b/keyserver/src/responders/link-responders.js @@ -1,119 +1,95 @@ // @flow import t, { type TUnion, type TInterface } from 'tcomb'; import { type InviteLinkVerificationRequest, type InviteLinkVerificationResponse, type FetchInviteLinksResponse, type InviteLink, inviteLinkValidator, type CreateOrUpdatePublicLinkRequest, + type DisableInviteLinkRequest, } from 'lib/types/link-types.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import { createOrUpdatePublicLink } from '../creators/invite-link-creator.js'; import { deleteInviteLink } from '../deleters/link-deleters.js'; import { fetchPrimaryInviteLinks, verifyInviteLink, } from '../fetchers/link-fetchers.js'; import { Viewer } from '../session/viewer.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const inviteLinkVerificationRequestInputValidator: TInterface = +export const inviteLinkVerificationRequestInputValidator: TInterface = tShape({ secret: t.String, }); export const inviteLinkVerificationResponseValidator: TUnion = t.union([ tShape({ status: t.enums.of(['valid', 'already_joined']), community: tShape({ name: t.String, id: tID, }), }), tShape({ status: t.enums.of(['invalid', 'expired']), }), ]); async function inviteLinkVerificationResponder( viewer: Viewer, - input: mixed, + request: InviteLinkVerificationRequest, ): Promise { - const request = await validateInput( - viewer, - inviteLinkVerificationRequestInputValidator, - input, - ); - const response = await verifyInviteLink(viewer, request); - return validateOutput( - viewer.platformDetails, - inviteLinkVerificationResponseValidator, - response, - ); + return await verifyInviteLink(viewer, request); } export const fetchInviteLinksResponseValidator: TInterface = tShape({ links: t.list(inviteLinkValidator), }); async function fetchPrimaryInviteLinksResponder( viewer: Viewer, ): Promise { const primaryLinks = await fetchPrimaryInviteLinks(viewer); - return validateOutput( - viewer.platformDetails, - fetchInviteLinksResponseValidator, - { - links: primaryLinks, - }, - ); + return { + links: primaryLinks, + }; } -const createOrUpdatePublicLinkInputValidator: TInterface = - tShape({ +export const createOrUpdatePublicLinkInputValidator: TInterface = + tShape({ name: t.String, communityID: tID, }); async function createOrUpdatePublicLinkResponder( viewer: Viewer, - input: mixed, + request: CreateOrUpdatePublicLinkRequest, ): Promise { - const request = await validateInput( - viewer, - createOrUpdatePublicLinkInputValidator, - input, - ); - const response = await createOrUpdatePublicLink(viewer, request); - return validateOutput(viewer.platformDetails, inviteLinkValidator, response); + return await createOrUpdatePublicLink(viewer, request); } -const disableInviteLinkInputValidator = tShape({ - name: t.String, - communityID: tID, -}); +export const disableInviteLinkInputValidator: TInterface = + tShape({ + name: t.String, + communityID: tID, + }); async function disableInviteLinkResponder( viewer: Viewer, - input: mixed, + request: DisableInviteLinkRequest, ): Promise { - const request = await validateInput( - viewer, - disableInviteLinkInputValidator, - input, - ); await deleteInviteLink(viewer, request); } export { inviteLinkVerificationResponder, fetchPrimaryInviteLinksResponder, createOrUpdatePublicLinkResponder, disableInviteLinkResponder, }; diff --git a/keyserver/src/responders/message-report-responder.js b/keyserver/src/responders/message-report-responder.js index 11fa8f0cf..f739c1ff4 100644 --- a/keyserver/src/responders/message-report-responder.js +++ b/keyserver/src/responders/message-report-responder.js @@ -1,43 +1,31 @@ // @flow import type { TInterface } from 'tcomb'; import { type MessageReportCreationRequest, type MessageReportCreationResult, } from 'lib/types/message-report-types.js'; import { rawMessageInfoValidator } from 'lib/types/message-types.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import createMessageReport from '../creators/message-report-creator.js'; import type { Viewer } from '../session/viewer.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const messageReportCreationRequestInputValidator = +export const messageReportCreationRequestInputValidator: TInterface = tShape({ messageID: tID, }); export const messageReportCreationResultValidator: TInterface = tShape({ messageInfo: rawMessageInfoValidator }); async function messageReportCreationResponder( viewer: Viewer, - input: mixed, + request: MessageReportCreationRequest, ): Promise { - const request = await validateInput( - viewer, - messageReportCreationRequestInputValidator, - input, - ); - const rawMessageInfos = await createMessageReport(viewer, request); - const result = { messageInfo: rawMessageInfos[0] }; - return validateOutput( - viewer.platformDetails, - messageReportCreationResultValidator, - result, - ); + return { messageInfo: rawMessageInfos[0] }; } export { messageReportCreationResponder }; diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js index 81737e414..00f6c20bd 100644 --- a/keyserver/src/responders/message-responders.js +++ b/keyserver/src/responders/message-responders.js @@ -1,525 +1,452 @@ // @flow import invariant from 'invariant'; -import t, { type TInterface } from 'tcomb'; +import t, { type TInterface, type TUnion } from 'tcomb'; import { onlyOneEmojiRegex } from 'lib/shared/emojis.js'; import { createMediaMessageData, trimMessage, } from 'lib/shared/message-utils.js'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils.js'; import type { Media } from 'lib/types/media-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { type SendTextMessageRequest, type SendMultimediaMessageRequest, type SendReactionMessageRequest, type SendEditMessageRequest, type FetchMessageInfosResponse, type FetchMessageInfosRequest, defaultNumberPerThread, type SendMessageResponse, type SendEditMessageResponse, type FetchPinnedMessagesRequest, type FetchPinnedMessagesResult, messageTruncationStatusesValidator, rawMessageInfoValidator, type SearchMessagesResponse, type SearchMessagesRequest, } from 'lib/types/message-types.js'; import type { EditMessageData } from 'lib/types/messages/edit.js'; import type { ReactionMessageData } from 'lib/types/messages/reaction.js'; import type { TextMessageData } from 'lib/types/messages/text.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { tRegex, tShape, tMediaMessageMedia, tID, } from 'lib/utils/validation-utils.js'; import createMessages from '../creators/message-creator.js'; import { fetchMessageInfos, fetchMessageInfoForLocalID, fetchMessageInfoByID, fetchThreadMessagesCount, fetchPinnedMessageInfos, searchMessagesInSingleChat, } from '../fetchers/message-fetchers.js'; import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { fetchImages, fetchMediaFromMediaMessageContent, } from '../fetchers/upload-fetchers.js'; import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { assignImages, assignMessageContainerToMedia, } from '../updaters/upload-updaters.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const sendTextMessageRequestInputValidator = tShape({ - threadID: tID, - localID: t.maybe(t.String), - text: t.String, - sidebarCreation: t.maybe(t.Boolean), -}); +export const sendTextMessageRequestInputValidator: TInterface = + tShape({ + threadID: tID, + localID: t.maybe(t.String), + text: t.String, + sidebarCreation: t.maybe(t.Boolean), + }); export const sendMessageResponseValidator: TInterface = tShape({ newMessageInfo: rawMessageInfoValidator }); async function textMessageCreationResponder( viewer: Viewer, - input: mixed, + request: SendTextMessageRequest, ): Promise { - const request = await validateInput( - viewer, - sendTextMessageRequestInputValidator, - input, - ); - const { threadID, localID, text: rawText, sidebarCreation } = request; const text = trimMessage(rawText); if (!text) { throw new ServerError('invalid_parameters'); } const hasPermission = await checkThreadPermission( viewer, threadID, threadPermissions.VOICED, ); if (!hasPermission) { throw new ServerError('invalid_parameters'); } let messageData: TextMessageData = { type: messageTypes.TEXT, threadID, creatorID: viewer.id, time: Date.now(), text, }; if (localID) { messageData = { ...messageData, localID }; } if (sidebarCreation) { const numMessages = await fetchThreadMessagesCount(threadID); if (numMessages === 2) { // sidebarCreation is set below to prevent double notifs from a sidebar // creation. We expect precisely two messages to appear before a // sidebarCreation: a SIDEBAR_SOURCE and a CREATE_SIDEBAR. If two users // attempt to create a sidebar at the same time, then both clients will // attempt to set sidebarCreation here, but we only want to suppress // notifs for the client that won the race. messageData = { ...messageData, sidebarCreation }; } } const rawMessageInfos = await createMessages(viewer, [messageData]); - const response = { newMessageInfo: rawMessageInfos[0] }; - return validateOutput( - viewer.platformDetails, - sendMessageResponseValidator, - response, - ); + return { newMessageInfo: rawMessageInfos[0] }; } -const fetchMessageInfosRequestInputValidator = tShape( - { +export const fetchMessageInfosRequestInputValidator: TInterface = + tShape({ cursors: t.dict(tID, t.maybe(tID)), numberPerThread: t.maybe(t.Number), - }, -); + }); export const fetchMessageInfosResponseValidator: TInterface = tShape({ rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); async function messageFetchResponder( viewer: Viewer, - input: mixed, + request: FetchMessageInfosRequest, ): Promise { - const request = await validateInput( - viewer, - fetchMessageInfosRequestInputValidator, - input, - ); const response = await fetchMessageInfos( viewer, { threadCursors: request.cursors }, request.numberPerThread ? request.numberPerThread : defaultNumberPerThread, ); - return validateOutput( - viewer.platformDetails, - fetchMessageInfosResponseValidator, - { - ...response, - userInfos: {}, - }, - ); + return { + ...response, + userInfos: {}, + }; } -const sendMultimediaMessageRequestInputValidator = +export const sendMultimediaMessageRequestInputValidator: TUnion = t.union([ // This option is only used for messageTypes.IMAGES tShape({ threadID: tID, localID: t.String, sidebarCreation: t.maybe(t.Boolean), mediaIDs: t.list(tID), }), tShape({ threadID: tID, localID: t.String, sidebarCreation: t.maybe(t.Boolean), mediaMessageContents: t.list(tMediaMessageMedia), }), ]); async function multimediaMessageCreationResponder( viewer: Viewer, - input: mixed, + request: SendMultimediaMessageRequest, ): Promise { - const request = await validateInput( - viewer, - sendMultimediaMessageRequestInputValidator, - input, - ); - if ( (request.mediaIDs && request.mediaIDs.length === 0) || (request.mediaMessageContents && request.mediaMessageContents.length === 0) ) { throw new ServerError('invalid_parameters'); } const { threadID, localID, sidebarCreation } = request; const hasPermission = await checkThreadPermission( viewer, threadID, threadPermissions.VOICED, ); if (!hasPermission) { throw new ServerError('invalid_parameters'); } const existingMessageInfoPromise = fetchMessageInfoForLocalID( viewer, localID, ); const mediaPromise: Promise<$ReadOnlyArray> = request.mediaIDs ? fetchImages(viewer, request.mediaIDs) : fetchMediaFromMediaMessageContent(viewer, request.mediaMessageContents); const [existingMessageInfo, media] = await Promise.all([ existingMessageInfoPromise, mediaPromise, ]); if (media.length === 0 && !existingMessageInfo) { throw new ServerError('invalid_parameters'); } // We use the MULTIMEDIA type for encrypted photos const containsEncryptedMedia = media.some( m => m.type === 'encrypted_photo' || m.type === 'encrypted_video', ); const messageData = createMediaMessageData( { localID, threadID, creatorID: viewer.id, media, sidebarCreation, }, { forceMultimediaMessageType: containsEncryptedMedia }, ); const [newMessageInfo] = await createMessages(viewer, [messageData]); const { id } = newMessageInfo; invariant( id !== null && id !== undefined, 'serverID should be set in createMessages result', ); if (request.mediaIDs) { await assignImages(viewer, request.mediaIDs, id, threadID); } else { await assignMessageContainerToMedia( viewer, request.mediaMessageContents, id, threadID, ); } - const response = { newMessageInfo }; - return validateOutput( - viewer.platformDetails, - sendMessageResponseValidator, - response, - ); + return { newMessageInfo }; } -const sendReactionMessageRequestInputValidator = +export const sendReactionMessageRequestInputValidator: TInterface = tShape({ threadID: tID, localID: t.maybe(t.String), targetMessageID: tID, reaction: tRegex(onlyOneEmojiRegex), action: t.enums.of(['add_reaction', 'remove_reaction']), }); async function reactionMessageCreationResponder( viewer: Viewer, - input: mixed, + request: SendReactionMessageRequest, ): Promise { - const request = await validateInput( - viewer, - sendReactionMessageRequestInputValidator, - input, - ); - const { threadID, localID, targetMessageID, reaction, action } = request; if (!targetMessageID || !reaction) { throw new ServerError('invalid_parameters'); } const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID); if (!targetMessageInfo || !targetMessageInfo.id) { throw new ServerError('invalid_parameters'); } const [serverThreadInfos, hasPermission, targetMessageUserInfos] = await Promise.all([ fetchServerThreadInfos({ threadID }), checkThreadPermission( viewer, threadID, threadPermissions.REACT_TO_MESSAGE, ), fetchKnownUserInfos(viewer, [targetMessageInfo.creatorID]), ]); const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID]; if (targetMessageThreadInfo.sourceMessageID === targetMessageID) { throw new ServerError('invalid_parameters'); } const targetMessageCreator = targetMessageUserInfos[targetMessageInfo.creatorID]; const targetMessageCreatorRelationship = targetMessageCreator?.relationshipStatus; const creatorRelationshipHasBlock = targetMessageCreatorRelationship && relationshipBlockedInEitherDirection(targetMessageCreatorRelationship); if (!hasPermission || creatorRelationshipHasBlock) { throw new ServerError('invalid_parameters'); } let messageData: ReactionMessageData = { type: messageTypes.REACTION, threadID, creatorID: viewer.id, time: Date.now(), targetMessageID, reaction, action, }; if (localID) { messageData = { ...messageData, localID }; } const rawMessageInfos = await createMessages(viewer, [messageData]); - const response = { newMessageInfo: rawMessageInfos[0] }; - return validateOutput( - viewer.platformDetails, - sendMessageResponseValidator, - response, - ); + return { newMessageInfo: rawMessageInfos[0] }; } -const editMessageRequestInputValidator = tShape({ - targetMessageID: tID, - text: t.String, -}); +export const editMessageRequestInputValidator: TInterface = + tShape({ + targetMessageID: tID, + text: t.String, + }); export const sendEditMessageResponseValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), }); async function editMessageCreationResponder( viewer: Viewer, - input: mixed, + request: SendEditMessageRequest, ): Promise { - const request = await validateInput( - viewer, - editMessageRequestInputValidator, - input, - ); - const { targetMessageID, text: rawText } = request; const text = trimMessage(rawText); if (!targetMessageID || !text) { throw new ServerError('invalid_parameters'); } const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID); if (!targetMessageInfo || !targetMessageInfo.id) { throw new ServerError('invalid_parameters'); } if (targetMessageInfo.type !== messageTypes.TEXT) { throw new ServerError('invalid_parameters'); } const { threadID } = targetMessageInfo; const [serverThreadInfos, hasPermission, rawSidebarThreadInfos] = await Promise.all([ fetchServerThreadInfos({ threadID }), checkThreadPermission(viewer, threadID, threadPermissions.EDIT_MESSAGE), fetchServerThreadInfos({ parentThreadID: threadID, sourceMessageID: targetMessageID, }), ]); const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID]; if (targetMessageThreadInfo.sourceMessageID === targetMessageID) { // We are editing first message of the sidebar // If client wants to do that it sends id of the sourceMessage instead throw new ServerError('invalid_parameters'); } if (!hasPermission) { throw new ServerError('invalid_parameters'); } if (targetMessageInfo.creatorID !== viewer.id) { throw new ServerError('invalid_parameters'); } const time = Date.now(); const messagesData = []; let messageData: EditMessageData = { type: messageTypes.EDIT_MESSAGE, threadID, creatorID: viewer.id, time, targetMessageID, text, }; messagesData.push(messageData); const sidebarThreadValues = values(rawSidebarThreadInfos.threadInfos); for (const sidebarThreadValue of sidebarThreadValues) { if (sidebarThreadValue && sidebarThreadValue.id) { messageData = { type: messageTypes.EDIT_MESSAGE, threadID: sidebarThreadValue.id, creatorID: viewer.id, time, targetMessageID, text: text, }; messagesData.push(messageData); } } const newMessageInfos = await createMessages(viewer, messagesData); - const response = { newMessageInfos }; - return validateOutput( - viewer.platformDetails, - sendEditMessageResponseValidator, - response, - ); + return { newMessageInfos }; } -const fetchPinnedMessagesResponderInputValidator = +export const fetchPinnedMessagesResponderInputValidator: TInterface = tShape({ threadID: tID, }); export const fetchPinnedMessagesResultValidator: TInterface = tShape({ pinnedMessages: t.list(rawMessageInfoValidator), }); async function fetchPinnedMessagesResponder( viewer: Viewer, - input: mixed, + request: FetchPinnedMessagesRequest, ): Promise { - const request = await validateInput( - viewer, - fetchPinnedMessagesResponderInputValidator, - input, - ); - const response = await fetchPinnedMessageInfos(viewer, request); - return validateOutput( - viewer.platformDetails, - fetchPinnedMessagesResultValidator, - response, - ); + return await fetchPinnedMessageInfos(viewer, request); } -const searchMessagesResponderInputValidator = tShape({ - query: t.String, - threadID: tID, - cursor: t.maybe(tID), -}); +export const searchMessagesResponderInputValidator: TInterface = + tShape({ + query: t.String, + threadID: tID, + cursor: t.maybe(tID), + }); -const searchMessagesResponseValidator: TInterface = +export const searchMessagesResponseValidator: TInterface = tShape({ messages: t.list(rawMessageInfoValidator), endReached: t.Boolean, }); async function searchMessagesResponder( viewer: Viewer, - input: mixed, + request: SearchMessagesRequest, ): Promise { - const request: SearchMessagesRequest = await validateInput( - viewer, - searchMessagesResponderInputValidator, - input, - ); - - const response = await searchMessagesInSingleChat( + return await searchMessagesInSingleChat( request.query, request.threadID, viewer, request.cursor, ); - return validateOutput( - viewer.platformDetails, - searchMessagesResponseValidator, - response, - ); } export { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, }; diff --git a/keyserver/src/responders/relationship-responders.js b/keyserver/src/responders/relationship-responders.js index 87a48c16d..da8bb9067 100644 --- a/keyserver/src/responders/relationship-responders.js +++ b/keyserver/src/responders/relationship-responders.js @@ -1,45 +1,35 @@ // @flow import t, { type TInterface } from 'tcomb'; import { type RelationshipRequest, type RelationshipErrors, relationshipActionsList, } from 'lib/types/relationship-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; import { updateRelationships } from '../updaters/relationship-updaters.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const updateRelationshipInputValidator = tShape({ - action: t.enums.of(relationshipActionsList, 'relationship action'), - userIDs: t.list(t.String), -}); +export const updateRelationshipInputValidator: TInterface = + tShape({ + action: t.enums.of(relationshipActionsList, 'relationship action'), + userIDs: t.list(t.String), + }); export const relationshipErrorsValidator: TInterface = tShape({ invalid_user: t.maybe(t.list(t.String)), already_friends: t.maybe(t.list(t.String)), user_blocked: t.maybe(t.list(t.String)), }); async function updateRelationshipsResponder( viewer: Viewer, - input: mixed, + request: RelationshipRequest, ): Promise { - const request = await validateInput( - viewer, - updateRelationshipInputValidator, - input, - ); - const response = await updateRelationships(viewer, request); - return validateOutput( - viewer.platformDetails, - relationshipErrorsValidator, - response, - ); + return await updateRelationships(viewer, request); } export { updateRelationshipsResponder }; diff --git a/keyserver/src/responders/report-responders.js b/keyserver/src/responders/report-responders.js index e67cb739e..5614a58ad 100644 --- a/keyserver/src/responders/report-responders.js +++ b/keyserver/src/responders/report-responders.js @@ -1,269 +1,245 @@ // @flow import type { $Response, $Request } from 'express'; import t from 'tcomb'; -import type { TInterface, TStructProps } from 'tcomb'; +import type { TInterface, TStructProps, TUnion } from 'tcomb'; import { type ReportCreationResponse, type ReportCreationRequest, type FetchErrorReportInfosResponse, type FetchErrorReportInfosRequest, type ThreadInconsistencyReportShape, type EntryInconsistencyReportShape, reportTypes, reportInfoValidator, } from 'lib/types/report-types.js'; import { userInfoValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tShape, tPlatform, tPlatformDetails, } from 'lib/utils/validation-utils.js'; import { newEntryQueryInputValidator } from './entry-responders.js'; import createReport from '../creators/report-creator.js'; import { fetchErrorReportInfos, fetchReduxToolsImport, } from '../fetchers/report-fetchers.js'; import type { Viewer } from '../session/viewer.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; const tActionSummary = tShape({ type: t.String, time: t.Number, summary: t.String, }); const threadInconsistencyReportValidatorShape: TStructProps = { platformDetails: tPlatformDetails, beforeAction: t.Object, action: t.Object, pollResult: t.maybe(t.Object), pushResult: t.Object, lastActionTypes: t.maybe(t.list(t.String)), lastActions: t.maybe(t.list(tActionSummary)), time: t.maybe(t.Number), }; const entryInconsistencyReportValidatorShape: TStructProps = { platformDetails: tPlatformDetails, beforeAction: t.Object, action: t.Object, calendarQuery: newEntryQueryInputValidator, pollResult: t.maybe(t.Object), pushResult: t.Object, lastActionTypes: t.maybe(t.list(t.String)), lastActions: t.maybe(t.list(tActionSummary)), time: t.Number, }; const userInconsistencyReportValidatorShape = { platformDetails: tPlatformDetails, action: t.Object, beforeStateCheck: t.Object, afterStateCheck: t.Object, lastActions: t.list(tActionSummary), time: t.Number, }; const threadInconsistencyReportCreationRequest = tShape({ ...threadInconsistencyReportValidatorShape, type: t.irreducible( 'reportTypes.THREAD_INCONSISTENCY', x => x === reportTypes.THREAD_INCONSISTENCY, ), }); const entryInconsistencyReportCreationRquest = tShape({ ...entryInconsistencyReportValidatorShape, type: t.irreducible( 'reportTypes.ENTRY_INCONSISTENCY', x => x === reportTypes.ENTRY_INCONSISTENCY, ), }); const mediaMissionReportCreationRequest = tShape({ type: t.irreducible( 'reportTypes.MEDIA_MISSION', x => x === reportTypes.MEDIA_MISSION, ), platformDetails: tPlatformDetails, time: t.Number, mediaMission: t.Object, uploadServerID: t.maybe(t.String), uploadLocalID: t.maybe(t.String), mediaLocalID: t.maybe(t.String), messageServerID: t.maybe(t.String), messageLocalID: t.maybe(t.String), }); const userInconsistencyReportCreationRequest = tShape({ ...userInconsistencyReportValidatorShape, type: t.irreducible( 'reportTypes.USER_INCONSISTENCY', x => x === reportTypes.USER_INCONSISTENCY, ), }); -const reportCreationRequestInputValidator = t.union([ - tShape({ - type: t.maybe( - t.irreducible('reportTypes.ERROR', x => x === reportTypes.ERROR), - ), - platformDetails: t.maybe(tPlatformDetails), - deviceType: t.maybe(tPlatform), - codeVersion: t.maybe(t.Number), - stateVersion: t.maybe(t.Number), - errors: t.list( - tShape({ - errorMessage: t.String, - stack: t.maybe(t.String), - componentStack: t.maybe(t.String), - }), - ), - preloadedState: t.Object, - currentState: t.Object, - actions: t.list(t.union([t.Object, t.String])), - }), - threadInconsistencyReportCreationRequest, - entryInconsistencyReportCreationRquest, - mediaMissionReportCreationRequest, - userInconsistencyReportCreationRequest, -]); +export const reportCreationRequestInputValidator: TUnion = + t.union([ + tShape({ + type: t.maybe( + t.irreducible('reportTypes.ERROR', x => x === reportTypes.ERROR), + ), + platformDetails: t.maybe(tPlatformDetails), + deviceType: t.maybe(tPlatform), + codeVersion: t.maybe(t.Number), + stateVersion: t.maybe(t.Number), + errors: t.list( + tShape({ + errorMessage: t.String, + stack: t.maybe(t.String), + componentStack: t.maybe(t.String), + }), + ), + preloadedState: t.Object, + currentState: t.Object, + actions: t.list(t.union([t.Object, t.String])), + }), + threadInconsistencyReportCreationRequest, + entryInconsistencyReportCreationRquest, + mediaMissionReportCreationRequest, + userInconsistencyReportCreationRequest, + ]); export const reportCreationResponseValidator: TInterface = tShape({ id: t.String }); async function reportCreationResponder( viewer: Viewer, - input: mixed, + request: ReportCreationRequest, ): Promise { - let request = await validateInput( - viewer, - reportCreationRequestInputValidator, - input, - ); if (request.type === null || request.type === undefined) { request.type = reportTypes.ERROR; } if (!request.platformDetails && request.deviceType) { const { deviceType, codeVersion, stateVersion, ...rest } = request; request = { ...rest, platformDetails: { platform: deviceType, codeVersion, stateVersion }, }; } const response = await createReport(viewer, request); if (!response) { throw new ServerError('ignored_report'); } - return validateOutput( - viewer.platformDetails, - reportCreationResponseValidator, - response, - ); + return response; } -const reportMultiCreationRequestInputValidator = +export const reportMultiCreationRequestInputValidator: TInterface = tShape({ reports: t.list( t.union([ tShape({ type: t.irreducible( 'reportTypes.ERROR', x => x === reportTypes.ERROR, ), platformDetails: tPlatformDetails, errors: t.list( tShape({ errorMessage: t.String, stack: t.maybe(t.String), componentStack: t.maybe(t.String), }), ), preloadedState: t.Object, currentState: t.Object, actions: t.list(t.union([t.Object, t.String])), }), threadInconsistencyReportCreationRequest, entryInconsistencyReportCreationRquest, mediaMissionReportCreationRequest, userInconsistencyReportCreationRequest, ]), ), }); type ReportMultiCreationRequest = { reports: $ReadOnlyArray, }; async function reportMultiCreationResponder( viewer: Viewer, - input: mixed, + request: ReportMultiCreationRequest, ): Promise { - const request = await validateInput( - viewer, - reportMultiCreationRequestInputValidator, - input, - ); await Promise.all( request.reports.map(reportCreationRequest => createReport(viewer, reportCreationRequest), ), ); } -const fetchErrorReportInfosRequestInputValidator = +export const fetchErrorReportInfosRequestInputValidator: TInterface = tShape({ cursor: t.maybe(t.String), }); export const fetchErrorReportInfosResponseValidator: TInterface = tShape({ reports: t.list(reportInfoValidator), userInfos: t.list(userInfoValidator), }); async function errorReportFetchInfosResponder( viewer: Viewer, - input: mixed, + request: FetchErrorReportInfosRequest, ): Promise { - const request = await validateInput( - viewer, - fetchErrorReportInfosRequestInputValidator, - input, - ); - const response = await fetchErrorReportInfos(viewer, request); - return validateOutput( - viewer.platformDetails, - fetchErrorReportInfosResponseValidator, - response, - ); + return await fetchErrorReportInfos(viewer, request); } async function errorReportDownloadResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const id = req.params.reportID; if (!id) { throw new ServerError('invalid_parameters'); } const result = await fetchReduxToolsImport(viewer, id); res.set('Content-Disposition', `attachment; filename=report-${id}.json`); res.json({ preloadedState: JSON.stringify(result.preloadedState), payload: JSON.stringify(result.payload), }); } export { threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, errorReportDownloadResponder, }; diff --git a/keyserver/src/responders/responder-validators.test.js b/keyserver/src/responders/responder-validators.test.js index f16915a83..e0f8dd93d 100644 --- a/keyserver/src/responders/responder-validators.test.js +++ b/keyserver/src/responders/responder-validators.test.js @@ -1,976 +1,976 @@ // @flow import { - setThreadUnreadStatusResult, + setThreadUnreadStatusResultValidator, updateActivityResultValidator, } from 'lib/types/activity-types.js'; import { fetchEntryInfosResponseValidator, fetchEntryRevisionInfosResultValidator, saveEntryResponseValidator, deleteEntryResponseValidator, deltaEntryInfosResultValidator, restoreEntryResponseValidator, } from './entry-responders.js'; import { getSessionPublicKeysResponseValidator } from './keys-responders.js'; import { inviteLinkVerificationResponseValidator, fetchInviteLinksResponseValidator, } from './link-responders.js'; import { messageReportCreationResultValidator } from './message-report-responder.js'; import { fetchMessageInfosResponseValidator, fetchPinnedMessagesResultValidator, sendEditMessageResponseValidator, sendMessageResponseValidator, } from './message-responders.js'; import { relationshipErrorsValidator } from './relationship-responders.js'; import { reportCreationResponseValidator } from './report-responders.js'; import { userSearchResultValidator } from './search-responders.js'; import { siweNonceResponseValidator } from './siwe-nonce-responders.js'; import { changeThreadSettingsResultValidator, leaveThreadResultValidator, newThreadResponseValidator, threadFetchMediaResultValidator, threadJoinResultValidator, toggleMessagePinResultValidator, roleChangeRequestInputValidator, } from './thread-responders.js'; 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); }); }); describe('search responder', () => { it('should validate search response', () => { const response = { userInfos: [ { id: '83817', username: 'temp_user0' }, { id: '83853', username: 'temp_user1' }, { id: '83890', username: 'temp_user2' }, { id: '83928', username: 'temp_user3' }, ], }; expect(userSearchResultValidator.is(response)).toBe(true); response.userInfos.push({ id: 123 }); expect(userSearchResultValidator.is(response)).toBe(false); }); }); describe('message report responder', () => { it('should validate message report response', () => { const response = { messageInfo: { type: 0, threadID: '101113', creatorID: '5', time: 1682429699746, text: 'text', id: '101121', }, }; expect(messageReportCreationResultValidator.is(response)).toBe(true); response.messageInfo.type = -2; expect(messageReportCreationResultValidator.is(response)).toBe(false); }); }); describe('relationship responder', () => { it('should validate relationship response', () => { const response = { invalid_user: ['83817', '83890'], already_friends: ['83890'], }; expect(relationshipErrorsValidator.is(response)).toBe(true); expect( relationshipErrorsValidator.is({ ...response, user_blocked: {} }), ).toBe(false); }); }); describe('activity responder', () => { it('should validate update activity response', () => { const response = { unfocusedToUnread: ['93095'] }; expect(updateActivityResultValidator.is(response)).toBe(true); response.unfocusedToUnread.push(123); expect(updateActivityResultValidator.is(response)).toBe(false); }); it('should validate set thread unread response', () => { const response = { resetToUnread: false }; - expect(setThreadUnreadStatusResult.is(response)).toBe(true); - expect(setThreadUnreadStatusResult.is({ ...response, unread: false })).toBe( - false, - ); + expect(setThreadUnreadStatusResultValidator.is(response)).toBe(true); + expect( + setThreadUnreadStatusResultValidator.is({ ...response, unread: false }), + ).toBe(false); }); }); describe('keys responder', () => { it('should validate get session public keys response', () => { const response = { identityKey: 'key', oneTimeKey: 'key', }; expect(getSessionPublicKeysResponseValidator.is(response)).toBe(true); expect(getSessionPublicKeysResponseValidator.is(null)).toBe(true); expect( getSessionPublicKeysResponseValidator.is({ ...response, identityKey: undefined, }), ).toBe(false); }); }); describe('siwe nonce responders', () => { it('should validate siwe nonce response', () => { const response = { nonce: 'nonce' }; expect(siweNonceResponseValidator.is(response)).toBe(true); expect(siweNonceResponseValidator.is({ nonce: 123 })).toBe(false); }); }); describe('entry reponders', () => { it('should validate entry fetch response', () => { const response = { rawEntryInfos: [ { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }, ], userInfos: { '123': { id: '123', username: 'username', }, }, }; expect(fetchEntryInfosResponseValidator.is(response)).toBe(true); expect( fetchEntryInfosResponseValidator.is({ ...response, userInfos: undefined, }), ).toBe(false); }); it('should validate entry revision fetch response', () => { const response = { result: [ { id: '93297', authorID: '83853', text: 'text', lastUpdate: 1682603494202, deleted: false, threadID: '83859', entryID: '93270', }, { id: '93284', authorID: '83853', text: 'text', lastUpdate: 1682603426996, deleted: true, threadID: '83859', entryID: '93270', }, ], }; expect(fetchEntryRevisionInfosResultValidator.is(response)).toBe(true); expect( fetchEntryRevisionInfosResultValidator.is({ ...response, result: {}, }), ).toBe(false); }); it('should validate entry save response', () => { const response = { entryID: '93270', newMessageInfos: [ { type: 9, threadID: '83859', creatorID: '83853', time: 1682603362817, entryID: '93270', date: '2023-04-03', text: 'text', id: '93272', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(saveEntryResponseValidator.is(response)).toBe(true); expect( saveEntryResponseValidator.is({ ...response, entryID: undefined, }), ).toBe(false); }); it('should validate entry delete response', () => { const response = { threadID: '83859', newMessageInfos: [ { type: 11, threadID: '83859', creatorID: '83853', time: 1682603427038, entryID: '93270', date: '2023-04-03', text: 'text', id: '93285', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(deleteEntryResponseValidator.is(response)).toBe(true); expect( deleteEntryResponseValidator.is({ ...response, threadID: undefined, }), ).toBe(false); }); it('should validate entry restore response', () => { const response = { newMessageInfos: [ { type: 11, threadID: '83859', creatorID: '83853', time: 1682603427038, entryID: '93270', date: '2023-04-03', text: 'text', id: '93285', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(restoreEntryResponseValidator.is(response)).toBe(true); expect( restoreEntryResponseValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate entry delta response', () => { const response = { rawEntryInfos: [ { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }, ], deletedEntryIDs: ['92860'], userInfos: [ { id: '123', username: 'username', }, ], }; expect(deltaEntryInfosResultValidator.is(response)).toBe(true); expect( deltaEntryInfosResultValidator.is({ ...response, rawEntryInfos: undefined, }), ).toBe(false); }); }); describe('thread responders', () => { it('should validate change thread settings response', () => { const response = { updatesResult: { newUpdates: [ { type: 1, id: '93601', time: 1682759546258, threadInfo: { id: '92796', type: 6, name: '', description: '', color: 'b8753d', creationTime: 1682076700918, parentThreadID: '1', members: [], roles: {}, currentUser: { role: '85172', permissions: {}, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }, }, ], }, newMessageInfos: [ { type: 4, threadID: '92796', creatorID: '83928', time: 1682759546275, field: 'color', value: 'b8753d', id: '93602', }, ], }; expect(changeThreadSettingsResultValidator.is(response)).toBe(true); expect( changeThreadSettingsResultValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate leave thread response', () => { const response = { updatesResult: { newUpdates: [ { type: 3, id: '93595', time: 1682759498811, threadID: '93561' }, ], }, }; expect(leaveThreadResultValidator.is(response)).toBe(true); expect( leaveThreadResultValidator.is({ ...response, updatedResult: undefined, }), ).toBe(false); }); it('should validate new thread response', () => { const response = { newThreadID: '93619', updatesResult: { newUpdates: [ { type: 4, id: '93621', time: 1682759805331, threadInfo: { id: '93619', type: 5, name: 'a', description: '', color: 'b8753d', creationTime: 1682759805298, parentThreadID: '92796', members: [], roles: {}, currentUser: { role: '85172', permissions: {}, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '92796', community: '1', sourceMessageID: '93614', pinnedCount: 0, }, rawMessageInfos: [], truncationStatus: 'exhaustive', rawEntryInfos: [], }, ], }, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, newMessageInfos: [], }; expect(newThreadResponseValidator.is(response)).toBe(true); expect( newThreadResponseValidator.is({ ...response, newMessageInfos: {}, }), ).toBe(false); }); it('should validate thread join response', () => { const response = { rawMessageInfos: [ { type: 8, threadID: '93619', creatorID: '83928', time: 1682759915935, id: '93640', }, ], truncationStatuses: {}, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, updatesResult: { newUpdates: [], }, }; expect(threadJoinResultValidator.is(response)).toBe(true); expect( threadJoinResultValidator.is({ ...response, updatesResult: [], }), ).toBe(false); }); it('should validate thread fetch media response', () => { const response = { media: [ { type: 'photo', id: '93642', uri: 'http://0.0.0.0:3000/comm/upload/93642/1e0d7a5262952e3b', dimensions: { width: 220, height: 220 }, }, ], }; expect(threadFetchMediaResultValidator.is(response)).toBe(true); expect( threadFetchMediaResultValidator.is({ ...response, media: undefined }), ).toBe(false); }); it('should validate toggle message pin response', () => { const response = { threadID: '123', newMessageInfos: [] }; expect(toggleMessagePinResultValidator.is(response)).toBe(true); expect( toggleMessagePinResultValidator.is({ ...response, threadID: undefined }), ).toBe(false); }); it('should validate role change request input', () => { const input = { threadID: '123', memberIDs: [], role: '1', }; expect(roleChangeRequestInputValidator.is(input)).toBe(true); expect(roleChangeRequestInputValidator.is({ ...input, role: '2|1' })).toBe( true, ); expect(roleChangeRequestInputValidator.is({ ...input, role: '-1' })).toBe( false, ); expect(roleChangeRequestInputValidator.is({ ...input, role: '2|-1' })).toBe( false, ); }); }); describe('message responders', () => { it('should validate send message response', () => { const response = { newMessageInfo: { type: 0, threadID: '93619', creatorID: '83928', time: 1682761023640, text: 'a', localID: 'local3', id: '93649', }, }; expect(sendMessageResponseValidator.is(response)).toBe(true); expect( sendMessageResponseValidator.is({ ...response, newMEssageInfos: undefined, }), ).toBe(false); }); it('should validate fetch message infos response', () => { const response = { rawMessageInfos: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], truncationStatuses: { '83938': 'exhaustive' }, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, }; expect(fetchMessageInfosResponseValidator.is(response)).toBe(true); expect( fetchMessageInfosResponseValidator.is({ ...response, userInfos: undefined, }), ).toBe(false); }); it('should validate send edit message response', () => { const response = { newMessageInfos: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], }; expect(sendEditMessageResponseValidator.is(response)).toBe(true); expect( sendEditMessageResponseValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate fetch pinned message response', () => { const response = { pinnedMessages: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], }; expect(fetchPinnedMessagesResultValidator.is(response)).toBe(true); expect( fetchPinnedMessagesResultValidator.is({ ...response, pinnedMessages: undefined, }), ).toBe(false); }); }); describe('report responders', () => { it('should validate report creation response', () => { const response = { id: '123' }; expect(reportCreationResponseValidator.is(response)).toBe(true); expect(reportCreationResponseValidator.is({})).toBe(false); }); }); describe('link responders', () => { it('should validate invite link verification response', () => { const response = { status: 'already_joined', community: { name: 'name', id: '123', }, }; expect(inviteLinkVerificationResponseValidator.is(response)).toBe(true); expect(inviteLinkVerificationResponseValidator.is({})).toBe(false); }); it('should validate invite link verification response', () => { const response = { links: [ { name: 'name', primary: true, role: '123', communityID: '123', expirationTime: 123, limitOfUses: 123, numberOfUses: 123, }, ], }; expect(fetchInviteLinksResponseValidator.is(response)).toBe(true); expect(fetchInviteLinksResponseValidator.is({ links: {} })).toBe(false); }); }); diff --git a/keyserver/src/responders/search-responders.js b/keyserver/src/responders/search-responders.js index 2addade89..dd6724d79 100644 --- a/keyserver/src/responders/search-responders.js +++ b/keyserver/src/responders/search-responders.js @@ -1,71 +1,53 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { UserSearchRequest, UserSearchResult, ExactUserSearchRequest, ExactUserSearchResult, } from 'lib/types/search-types.js'; import { globalAccountUserInfoValidator } from 'lib/types/user-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import { searchForUsers, searchForUser } from '../search/users.js'; import type { Viewer } from '../session/viewer.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const userSearchRequestInputValidator = tShape({ - prefix: t.maybe(t.String), -}); +export const userSearchRequestInputValidator: TInterface = + tShape({ + prefix: t.maybe(t.String), + }); export const userSearchResultValidator: TInterface = tShape({ userInfos: t.list(globalAccountUserInfoValidator), }); async function userSearchResponder( viewer: Viewer, - input: mixed, + request: UserSearchRequest, ): Promise { - const request = await validateInput( - viewer, - userSearchRequestInputValidator, - input, - ); const searchResults = await searchForUsers(request); - const result = { userInfos: searchResults }; - return validateOutput( - viewer.platformDetails, - userSearchResultValidator, - result, - ); + return { userInfos: searchResults }; } -const exactUserSearchRequestInputValidator = tShape({ - username: t.String, -}); +export const exactUserSearchRequestInputValidator: TInterface = + tShape({ + username: t.String, + }); -const exactUserSearchResultValidator = tShape({ - userInfo: t.maybe(globalAccountUserInfoValidator), -}); +export const exactUserSearchResultValidator: TInterface = + tShape({ + userInfo: t.maybe(globalAccountUserInfoValidator), + }); async function exactUserSearchResponder( viewer: Viewer, - input: mixed, + request: ExactUserSearchRequest, ): Promise { - const request = await validateInput( - viewer, - exactUserSearchRequestInputValidator, - input, - ); const searchResult = await searchForUser(request.username); - const result = { userInfo: searchResult }; - return validateOutput( - viewer.platformDetails, - exactUserSearchResultValidator, - result, - ); + return { userInfo: searchResult }; } export { userSearchResponder, exactUserSearchResponder }; diff --git a/keyserver/src/responders/siwe-nonce-responders.js b/keyserver/src/responders/siwe-nonce-responders.js index ef2b692c6..20aac7401 100644 --- a/keyserver/src/responders/siwe-nonce-responders.js +++ b/keyserver/src/responders/siwe-nonce-responders.js @@ -1,24 +1,22 @@ // @flow import { generateNonce } from 'siwe'; import t, { type TInterface } from 'tcomb'; import type { SIWENonceResponse } from 'lib/types/siwe-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import { createSIWENonceEntry } from '../creators/siwe-nonce-creator.js'; -import type { Viewer } from '../session/viewer.js'; -import { validateOutput } from '../utils/validation-utils.js'; export const siweNonceResponseValidator: TInterface = tShape({ nonce: t.String }); -async function siweNonceResponder(viewer: Viewer): Promise { +async function siweNonceResponder(): Promise { const generatedNonce = generateNonce(); await createSIWENonceEntry(generatedNonce); - return validateOutput(viewer.platformDetails, siweNonceResponseValidator, { + return { nonce: generatedNonce, - }); + }; } export { siweNonceResponder }; diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js index ae4f6a55f..09b02798a 100644 --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -1,442 +1,337 @@ // @flow import t from 'tcomb'; import type { TInterface, TUnion } from 'tcomb'; import { mediaValidator } from 'lib/types/media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; import { userSurfacedPermissionValidator } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadDeletionRequest, type RoleChangeRequest, type ChangeThreadSettingsResult, type RemoveMembersRequest, type LeaveThreadRequest, type LeaveThreadResult, type UpdateThreadRequest, type ServerNewThreadRequest, type NewThreadResponse, type ServerThreadJoinRequest, type ThreadJoinResult, type ThreadFetchMediaResult, type ThreadFetchMediaRequest, type ToggleMessagePinRequest, type ToggleMessagePinResult, type RoleModificationRequest, type RoleModificationResult, type RoleDeletionRequest, type RoleDeletionResult, rawThreadInfoValidator, } from 'lib/types/thread-types.js'; import { serverUpdateInfoValidator } from 'lib/types/update-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { values } from 'lib/utils/objects.js'; import { tShape, tNumEnum, tColor, tPassword, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { modifyRole } from '../creators/role-creator.js'; import { createThread } from '../creators/thread-creator.js'; import { deleteRole } from '../deleters/role-deleters.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { fetchMediaForThread } from '../fetchers/upload-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateRole, removeMembers, leaveThread, updateThread, joinThread, toggleMessagePinForThread, } from '../updaters/thread-updaters.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const threadDeletionRequestInputValidator = tShape({ - threadID: tID, - accountPassword: t.maybe(tPassword), -}); +export const threadDeletionRequestInputValidator: TInterface = + tShape({ + threadID: tID, + accountPassword: t.maybe(tPassword), + }); export const leaveThreadResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function threadDeletionResponder( viewer: Viewer, - input: mixed, + request: ThreadDeletionRequest, ): Promise { - const request = await validateInput( - viewer, - threadDeletionRequestInputValidator, - input, - ); - const result = await deleteThread(viewer, request); - return validateOutput( - viewer.platformDetails, - leaveThreadResultValidator, - result, - ); + return await deleteThread(viewer, request); } export const roleChangeRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), role: t.refinement(tID, str => { if (str.indexOf('|') !== -1) { str = str.split('|')[1]; } const int = parseInt(str, 10); return String(int) === str && int > 0; }), }); export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleUpdateResponder( viewer: Viewer, - input: mixed, + request: RoleChangeRequest, ): Promise { - const request = await validateInput( - viewer, - roleChangeRequestInputValidator, - input, - ); - const result = await updateRole(viewer, request); - return validateOutput( - viewer.platformDetails, - changeThreadSettingsResultValidator, - result, - ); + return await updateRole(viewer, request); } -const removeMembersRequestInputValidator = tShape({ - threadID: tID, - memberIDs: t.list(t.String), -}); +export const removeMembersRequestInputValidator: TInterface = + tShape({ + threadID: tID, + memberIDs: t.list(t.String), + }); async function memberRemovalResponder( viewer: Viewer, - input: mixed, + request: RemoveMembersRequest, ): Promise { - const request = await validateInput( - viewer, - removeMembersRequestInputValidator, - input, - ); - const result = await removeMembers(viewer, request); - return validateOutput( - viewer.platformDetails, - changeThreadSettingsResultValidator, - result, - ); + return await removeMembers(viewer, request); } -const leaveThreadRequestInputValidator = tShape({ - threadID: tID, -}); +export const leaveThreadRequestInputValidator: TInterface = + tShape({ + threadID: tID, + }); async function threadLeaveResponder( viewer: Viewer, - input: mixed, + request: LeaveThreadRequest, ): Promise { - const request = await validateInput( - viewer, - leaveThreadRequestInputValidator, - input, - ); - const result = await leaveThread(viewer, request); - return validateOutput( - viewer.platformDetails, - leaveThreadResultValidator, - result, - ); + return await leaveThread(viewer, request); } -const updateThreadRequestInputValidator = tShape({ - threadID: tID, - changes: tShape({ - type: t.maybe(tNumEnum(values(threadTypes))), - name: t.maybe(t.String), - description: t.maybe(t.String), - color: t.maybe(tColor), - parentThreadID: t.maybe(tID), - newMemberIDs: t.maybe(t.list(t.String)), - avatar: t.maybe(updateUserAvatarRequestValidator), - }), - accountPassword: t.maybe(tPassword), -}); +export const updateThreadRequestInputValidator: TInterface = + tShape({ + threadID: tID, + changes: tShape({ + type: t.maybe(tNumEnum(values(threadTypes))), + name: t.maybe(t.String), + description: t.maybe(t.String), + color: t.maybe(tColor), + parentThreadID: t.maybe(tID), + newMemberIDs: t.maybe(t.list(t.String)), + avatar: t.maybe(updateUserAvatarRequestValidator), + }), + accountPassword: t.maybe(tPassword), + }); async function threadUpdateResponder( viewer: Viewer, - input: mixed, + request: UpdateThreadRequest, ): Promise { - const request = await validateInput( - viewer, - updateThreadRequestInputValidator, - input, - ); - const result = await updateThread(viewer, request); - return validateOutput( - viewer.platformDetails, - changeThreadSettingsResultValidator, - result, - ); + return await updateThread(viewer, request); } const threadRequestValidationShape = { name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), initialMemberIDs: t.maybe(t.list(t.String)), calendarQuery: t.maybe(entryQueryInputValidator), }; const newThreadRequestInputValidator: TUnion = t.union([ tShape({ type: tNumEnum([threadTypes.SIDEBAR]), sourceMessageID: tID, ...threadRequestValidationShape, }), tShape({ type: tNumEnum([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.PERSONAL, threadTypes.LOCAL, threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, ]), ...threadRequestValidationShape, }), ]); export const newThreadResponseValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), newMessageInfos: t.list(rawMessageInfoValidator), userInfos: userInfosValidator, newThreadID: tID, }); async function threadCreationResponder( viewer: Viewer, - input: mixed, + request: ServerNewThreadRequest, ): Promise { - const request = await validateInput( - viewer, - newThreadRequestInputValidator, - input, - ); - - const result = await createThread(viewer, request, { + return await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, }); - return validateOutput( - viewer.platformDetails, - newThreadResponseValidator, - result, - ); } -const joinThreadRequestInputValidator = tShape({ - threadID: tID, - calendarQuery: t.maybe(entryQueryInputValidator), - inviteLinkSecret: t.maybe(t.String), -}); +export const joinThreadRequestInputValidator: TInterface = + tShape({ + threadID: tID, + calendarQuery: t.maybe(entryQueryInputValidator), + inviteLinkSecret: t.maybe(t.String), + }); export const threadJoinResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); async function threadJoinResponder( viewer: Viewer, - input: mixed, + request: ServerThreadJoinRequest, ): Promise { - const request = await validateInput( - viewer, - joinThreadRequestInputValidator, - input, - ); - if (request.calendarQuery) { await verifyCalendarQueryThreadIDs(request.calendarQuery); } - const result = await joinThread(viewer, request); - return validateOutput( - viewer.platformDetails, - threadJoinResultValidator, - result, - ); + return await joinThread(viewer, request); } -const threadFetchMediaRequestInputValidator = tShape({ - threadID: tID, - limit: t.Number, - offset: t.Number, -}); +export const threadFetchMediaRequestInputValidator: TInterface = + tShape({ + threadID: tID, + limit: t.Number, + offset: t.Number, + }); export const threadFetchMediaResultValidator: TInterface = tShape({ media: t.list(mediaValidator) }); async function threadFetchMediaResponder( viewer: Viewer, - input: mixed, + request: ThreadFetchMediaRequest, ): Promise { - const request = await validateInput( - viewer, - threadFetchMediaRequestInputValidator, - input, - ); - const result = await fetchMediaForThread(viewer, request); - return validateOutput( - viewer.platformDetails, - threadFetchMediaResultValidator, - result, - ); + return await fetchMediaForThread(viewer, request); } -const toggleMessagePinRequestInputValidator = tShape({ - messageID: tID, - action: t.enums.of(['pin', 'unpin']), -}); +export const toggleMessagePinRequestInputValidator: TInterface = + tShape({ + messageID: tID, + action: t.enums.of(['pin', 'unpin']), + }); export const toggleMessagePinResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, }); async function toggleMessagePinResponder( viewer: Viewer, - input: mixed, + request: ToggleMessagePinRequest, ): Promise { - const request = await validateInput( - viewer, - toggleMessagePinRequestInputValidator, - input, - ); - const result = await toggleMessagePinForThread(viewer, request); - return validateOutput( - viewer.platformDetails, - toggleMessagePinResultValidator, - result, - ); + return await toggleMessagePinForThread(viewer, request); } -const roleModificationRequestInputValidator: TUnion = +export const roleModificationRequestInputValidator: TUnion = t.union([ tShape({ community: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['create_role']), }), tShape({ community: tID, existingRoleID: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['edit_role']), }), ]); export const roleModificationResultValidator: TInterface = tShape({ threadInfo: t.maybe(rawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleModificationResponder( viewer: Viewer, - input: mixed, + request: RoleModificationRequest, ): Promise { - const request = await validateInput( - viewer, - roleModificationRequestInputValidator, - input, - ); - const response = await modifyRole(viewer, request); - return validateOutput( - viewer.platformDetails, - roleModificationResultValidator, - response, - ); + return await modifyRole(viewer, request); } -const roleDeletionRequestInputValidator = tShape({ - community: tID, - roleID: tID, -}); +export const roleDeletionRequestInputValidator: TInterface = + tShape({ + community: tID, + roleID: tID, + }); export const roleDeletionResultValidator: TInterface = tShape({ threadInfo: t.maybe(rawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleDeletionResponder( viewer: Viewer, - input: mixed, + request: RoleDeletionRequest, ): Promise { - const request = await validateInput( - viewer, - roleDeletionRequestInputValidator, - input, - ); - const response = await deleteRole(viewer, request); - return validateOutput( - viewer.platformDetails, - roleDeletionResultValidator, - response, - ); + return await deleteRole(viewer, request); } export { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, threadFetchMediaResponder, newThreadRequestInputValidator, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, }; diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js index 4d6281ed0..4cc52f595 100644 --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,830 +1,743 @@ // @flow import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { ErrorTypes, SiweMessage } from 'siwe'; -import t, { type TInterface } from 'tcomb'; +import t, { type TInterface, type TUnion } from 'tcomb'; import bcrypt from 'twin-bcrypt'; import { baseLegalPolicies, policies, policyTypeValidator, } from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { ResetPasswordRequest, LogOutResponse, DeleteAccountRequest, RegisterResponse, RegisterRequest, LogInResponse, LogInRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, } from 'lib/types/account-types.js'; import { userSettingsTypes, notificationTypeValues, logInActionSources, } from 'lib/types/account-types.js'; import { type ClientAvatar, clientAvatarValidator, type UpdateUserAvatarResponse, + type UpdateUserAvatarRequest, } from 'lib/types/avatar-types.js'; import type { IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-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, type SubscriptionUpdateResponse, threadSubscriptionValidator, } from 'lib/types/subscription-types.js'; import { rawThreadInfoValidator } from 'lib/types/thread-types.js'; import { createUpdatesResultValidator } from 'lib/types/update-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, signedIdentityKeysBlobValidator, } from 'lib/utils/crypto-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { promiseAll } from 'lib/utils/promises.js'; import { getPublicKeyFromSIWEStatement, isValidSIWEMessage, isValidSIWEStatementWithPublicKey, primaryIdentityPublicKeyRegex, } from 'lib/utils/siwe-utils.js'; import { tShape, tPlatformDetails, tPassword, tEmail, tOldValidUsername, tRegex, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { createAccount, processSIWEAccountCreation, } from '../creators/account-creator.js'; import { createOlmSession } from '../creators/olm-session-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { deleteAccount } from '../deleters/account-deleters.js'; import { deleteCookie } from '../deleters/cookie-deleters.js'; import { checkAndInvalidateSIWENonceEntry } from '../deleters/siwe-nonce-deleters.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchKnownUserInfos, fetchLoggedInUserInfo, fetchUserIDForEthereumAddress, } from '../fetchers/user-fetchers.js'; import { createNewAnonymousCookie, createNewUserCookie, setNewSession, } from '../session/cookies.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; import { accountUpdater, checkAndSendVerificationEmail, checkAndSendPasswordResetEmail, updatePassword, updateUserSettings, updateUserAvatar, } from '../updaters/account-updaters.js'; import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; import { getOlmUtility } from '../utils/olm-utils.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const subscriptionUpdateRequestInputValidator = +export const subscriptionUpdateRequestInputValidator: TInterface = tShape({ threadID: tID, updatedFields: tShape({ pushNotifs: t.maybe(t.Boolean), home: t.maybe(t.Boolean), }), }); export const subscriptionUpdateResponseValidator: TInterface = tShape({ threadSubscription: threadSubscriptionValidator, }); async function userSubscriptionUpdateResponder( viewer: Viewer, - input: mixed, + request: SubscriptionUpdateRequest, ): Promise { - const request = await validateInput( - viewer, - subscriptionUpdateRequestInputValidator, - input, - ); const threadSubscription = await userSubscriptionUpdater(viewer, request); - return validateOutput( - viewer.platformDetails, - subscriptionUpdateResponseValidator, - { - threadSubscription, - }, - ); + return { + threadSubscription, + }; } -const accountUpdateInputValidator = tShape({ - updatedFields: tShape({ - email: t.maybe(tEmail), - password: t.maybe(tPassword), - }), - currentPassword: tPassword, -}); +export const accountUpdateInputValidator: TInterface = + tShape({ + updatedFields: tShape({ + email: t.maybe(tEmail), + password: t.maybe(tPassword), + }), + currentPassword: tPassword, + }); async function passwordUpdateResponder( viewer: Viewer, - input: mixed, + request: PasswordUpdate, ): Promise { - const request = await validateInput( - viewer, - accountUpdateInputValidator, - input, - ); await accountUpdater(viewer, request); } async function sendVerificationEmailResponder(viewer: Viewer): Promise { if (!viewer.isSocket) { await verifyClientSupported(viewer, viewer.platformDetails); } await checkAndSendVerificationEmail(viewer); } -const resetPasswordRequestInputValidator = tShape({ - usernameOrEmail: t.union([tEmail, tOldValidUsername]), -}); +export const resetPasswordRequestInputValidator: TInterface = + tShape({ + usernameOrEmail: t.union([tEmail, tOldValidUsername]), + }); async function sendPasswordResetEmailResponder( viewer: Viewer, - input: mixed, + request: ResetPasswordRequest, ): Promise { - const request = await validateInput( - viewer, - resetPasswordRequestInputValidator, - input, - ); await checkAndSendPasswordResetEmail(request); } export const logOutResponseValidator: TInterface = tShape({ currentUserInfo: loggedOutUserInfoValidator, }); async function logOutResponder(viewer: Viewer): Promise { if (!viewer.isSocket) { await verifyClientSupported(viewer, viewer.platformDetails); } if (viewer.loggedIn) { const [anonymousViewerData] = await Promise.all([ createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(anonymousViewerData); } - const response = { + return { currentUserInfo: { id: viewer.id, anonymous: true, }, }; - return validateOutput( - viewer.platformDetails, - logOutResponseValidator, - response, - ); } -const deleteAccountRequestInputValidator = tShape({ - password: t.maybe(tPassword), -}); +export const deleteAccountRequestInputValidator: TInterface = + tShape({ + password: t.maybe(tPassword), + }); async function accountDeletionResponder( viewer: Viewer, - input: mixed, + request: DeleteAccountRequest, ): Promise { - const request = await validateInput( - viewer, - deleteAccountRequestInputValidator, - input, - ); const result = await deleteAccount(viewer, request); invariant(result, 'deleteAccount should return result if handed request'); - return validateOutput( - viewer.platformDetails, - logOutResponseValidator, - result, - ); + return result; } const deviceTokenUpdateRequestInputValidator = tShape({ deviceType: t.maybe(t.enums.of(['ios', 'android'])), deviceToken: t.String, }); -const registerRequestInputValidator = tShape({ - username: t.String, - email: t.maybe(tEmail), - password: tPassword, - calendarQuery: t.maybe(newEntryQueryInputValidator), - deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), - platformDetails: tPlatformDetails, - // We include `primaryIdentityPublicKey` to avoid breaking - // old clients, but we no longer do anything with it. - primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), - signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), - initialNotificationsEncryptedMessage: t.maybe(t.String), -}); +export const registerRequestInputValidator: TInterface = + tShape({ + username: t.String, + email: t.maybe(tEmail), + password: tPassword, + calendarQuery: t.maybe(newEntryQueryInputValidator), + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + // We include `primaryIdentityPublicKey` to avoid breaking + // old clients, but we no longer do anything with it. + primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), + signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), + initialNotificationsEncryptedMessage: t.maybe(t.String), + }); export const registerResponseValidator: TInterface = tShape({ id: t.String, rawMessageInfos: t.list(rawMessageInfoValidator), currentUserInfo: t.union([ oldLoggedInUserInfoValidator, loggedInUserInfoValidator, ]), cookieChange: tShape({ threadInfos: t.dict(tID, rawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), }); async function accountCreationResponder( viewer: Viewer, - input: mixed, + request: RegisterRequest, ): Promise { - const request = await validateInput( - viewer, - registerRequestInputValidator, - input, - ); const { signedIdentityKeysBlob } = request; if (signedIdentityKeysBlob) { const identityKeys: IdentityKeysBlob = JSON.parse( signedIdentityKeysBlob.payload, ); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } - const response = await createAccount(viewer, request); - return validateOutput( - viewer.platformDetails, - registerResponseValidator, - response, - ); + return await createAccount(viewer, request); } type ProcessSuccessfulLoginParams = { +viewer: Viewer, +input: any, +userID: string, +calendarQuery: ?CalendarQuery, +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; async function processSuccessfulLogin( params: ProcessSuccessfulLoginParams, ): Promise { const { viewer, input, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, } = params; const request: LogInRequest = input; const newServerTime = Date.now(); const deviceToken = request.deviceTokenUpdateRequest ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const [userViewerData, notAcknowledgedPolicies] = await Promise.all([ createNewUserCookie(userID, { platformDetails: request.platformDetails, deviceToken, socialProof, signedIdentityKeysBlob, }), fetchNotAcknowledgedPolicies(userID, baseLegalPolicies), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(userViewerData); if ( notAcknowledgedPolicies.length && hasMinCodeVersion(viewer.platformDetails, { native: 181 }) ) { const currentUserInfo = await fetchLoggedInUserInfo(viewer); return { notAcknowledgedPolicies, currentUserInfo: currentUserInfo, rawMessageInfos: [], truncationStatuses: {}, userInfos: [], rawEntryInfos: [], serverTime: 0, cookieChange: { threadInfos: {}, userInfos: [], }, }; } if (calendarQuery) { await setNewSession(viewer, calendarQuery, newServerTime); } const olmSessionPromise = (async () => { if ( userViewerData.cookieID && initialNotificationsEncryptedMessage && signedIdentityKeysBlob ) { await createOlmSession( initialNotificationsEncryptedMessage, 'notifications', userViewerData.cookieID, ); } })(); const threadCursors = {}; for (const watchedThreadID of request.watchedIDs) { threadCursors[watchedThreadID] = null; } const messageSelectionCriteria = { threadCursors, joinedThreads: true }; const [ threadsResult, messagesResult, entriesResult, userInfos, currentUserInfo, ] = await Promise.all([ fetchThreadInfos(viewer), fetchMessageInfos(viewer, messageSelectionCriteria, defaultNumberPerThread), calendarQuery ? fetchEntryInfos(viewer, [calendarQuery]) : undefined, fetchKnownUserInfos(viewer), fetchLoggedInUserInfo(viewer), olmSessionPromise, ]); const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null; const response: LogInResponse = { currentUserInfo, rawMessageInfos: messagesResult.rawMessageInfos, truncationStatuses: messagesResult.truncationStatuses, serverTime: newServerTime, userInfos: values(userInfos), cookieChange: { threadInfos: threadsResult.threadInfos, userInfos: [], }, }; if (rawEntryInfos) { return { ...response, rawEntryInfos, }; } return response; } -const logInRequestInputValidator = tShape({ - username: t.maybe(t.String), - usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), - password: tPassword, - watchedIDs: t.list(tID), - calendarQuery: t.maybe(entryQueryInputValidator), - deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), - platformDetails: tPlatformDetails, - source: t.maybe(t.enums.of(values(logInActionSources))), - // We include `primaryIdentityPublicKey` to avoid breaking - // old clients, but we no longer do anything with it. - primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), - signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), - initialNotificationsEncryptedMessage: t.maybe(t.String), -}); +export const logInRequestInputValidator: TInterface = + tShape({ + username: t.maybe(t.String), + usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), + password: tPassword, + watchedIDs: t.list(tID), + calendarQuery: t.maybe(entryQueryInputValidator), + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + source: t.maybe(t.enums.of(values(logInActionSources))), + // We include `primaryIdentityPublicKey` to avoid breaking + // old clients, but we no longer do anything with it. + primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), + signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), + initialNotificationsEncryptedMessage: t.maybe(t.String), + }); 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: mixed, + request: LogInRequest, ): Promise { - const request = await validateInput( - viewer, - logInRequestInputValidator, - input, - ); - let identityKeys: ?IdentityKeysBlob; const { signedIdentityKeysBlob, initialNotificationsEncryptedMessage } = request; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } const calendarQuery = request.calendarQuery ? normalizeCalendarQuery(request.calendarQuery) : null; const promises = {}; if (calendarQuery) { promises.verifyCalendarQueryThreadIDs = verifyCalendarQueryThreadIDs(calendarQuery); } const username = request.username ?? request.usernameOrEmail; if (!username) { if (hasMinCodeVersion(viewer.platformDetails, { native: 150 })) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userQuery = SQL` SELECT id, hash, username FROM users WHERE LCASE(username) = LCASE(${username}) `; promises.userQuery = dbQuery(userQuery); const { userQuery: [userResult], } = await promiseAll(promises); if (userResult.length === 0) { if (hasMinCodeVersion(viewer.platformDetails, { native: 150 })) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userRow = userResult[0]; if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) { throw new ServerError('invalid_credentials'); } const id = userRow.id.toString(); - const response = await processSuccessfulLogin({ + return await processSuccessfulLogin({ viewer, - input, + input: request, userID: id, calendarQuery, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, }); - return validateOutput( - viewer.platformDetails, - logInResponseValidator, - response, - ); } -const siweAuthRequestInputValidator = tShape({ - signature: t.String, - message: t.String, - calendarQuery: entryQueryInputValidator, - deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), - platformDetails: tPlatformDetails, - watchedIDs: t.list(tID), - signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), - initialNotificationsEncryptedMessage: t.maybe(t.String), -}); +export const siweAuthRequestInputValidator: TInterface = + tShape({ + signature: t.String, + message: t.String, + calendarQuery: entryQueryInputValidator, + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + watchedIDs: t.list(tID), + signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), + initialNotificationsEncryptedMessage: t.maybe(t.String), + }); async function siweAuthResponder( viewer: Viewer, - input: mixed, + request: SIWEAuthRequest, ): Promise { - const request = await validateInput( - viewer, - siweAuthRequestInputValidator, - input, - ); - const { message, signature, deviceTokenUpdateRequest, platformDetails, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, } = request; const calendarQuery = normalizeCalendarQuery(request.calendarQuery); // 1. Ensure that `message` is a well formed Comm SIWE Auth message. const siweMessage: SIWEMessage = new SiweMessage(message); if (!isValidSIWEMessage(siweMessage)) { throw new ServerError('invalid_parameters'); } // 2. Ensure that the `nonce` exists in the `siwe_nonces` table // AND hasn't expired. If those conditions are met, delete the entry to // ensure that the same `nonce` can't be re-used in a future request. const wasNonceCheckedAndInvalidated = await checkAndInvalidateSIWENonceEntry( siweMessage.nonce, ); if (!wasNonceCheckedAndInvalidated) { throw new ServerError('invalid_parameters'); } // 3. Validate SIWEMessage signature and handle possible errors. try { await siweMessage.validate(signature); } catch (error) { if (error === ErrorTypes.EXPIRED_MESSAGE) { // Thrown when the `expirationTime` is present and in the past. throw new ServerError('expired_message'); } else if (error === ErrorTypes.INVALID_SIGNATURE) { // Thrown when the `validate()` function can't verify the message. throw new ServerError('invalid_signature'); } else if (error === ErrorTypes.MALFORMED_SESSION) { // Thrown when some required field is missing. throw new ServerError('malformed_session'); } else { throw new ServerError('unknown_error'); } } // 4. Pull `primaryIdentityPublicKey` out from SIWEMessage `statement`. // We expect it to be included for BOTH native and web clients. const { statement } = siweMessage; const primaryIdentityPublicKey = statement && isValidSIWEStatementWithPublicKey(statement) ? getPublicKeyFromSIWEStatement(statement) : null; if (!primaryIdentityPublicKey) { throw new ServerError('invalid_siwe_statement_public_key'); } // 5. Verify `signedIdentityKeysBlob.payload` with included `signature` // if `signedIdentityKeysBlob` was included in the `SIWEAuthRequest`. let identityKeys: ?IdentityKeysBlob; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } // 6. Ensure that `primaryIdentityPublicKeys.ed25519` matches SIWE // statement `primaryIdentityPublicKey` if `identityKeys` exists. if ( identityKeys && identityKeys.primaryIdentityPublicKeys.ed25519 !== primaryIdentityPublicKey ) { throw new ServerError('primary_public_key_mismatch'); } // 7. Construct `SIWESocialProof` object with the stringified // SIWEMessage and the corresponding signature. const socialProof: SIWESocialProof = { siweMessage: siweMessage.toMessage(), siweMessageSignature: signature, }; // 8. Create account with call to `processSIWEAccountCreation(...)` // if address does not correspond to an existing user. let userID = await fetchUserIDForEthereumAddress(siweMessage.address); if (!userID) { const siweAccountCreationRequest = { address: siweMessage.address, calendarQuery, deviceTokenUpdateRequest, platformDetails, socialProof, }; userID = await processSIWEAccountCreation( viewer, siweAccountCreationRequest, ); } // 9. Complete login with call to `processSuccessfulLogin(...)`. - const response = await processSuccessfulLogin({ + return await processSuccessfulLogin({ viewer, - input, + input: request, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, }); - return validateOutput( - viewer.platformDetails, - logInResponseValidator, - response, - ); } -const updatePasswordRequestInputValidator = tShape({ - code: t.String, - password: tPassword, - watchedIDs: t.list(tID), - calendarQuery: t.maybe(entryQueryInputValidator), - deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), - platformDetails: tPlatformDetails, -}); +export const updatePasswordRequestInputValidator: TInterface = + tShape({ + code: t.String, + password: tPassword, + watchedIDs: t.list(tID), + calendarQuery: t.maybe(entryQueryInputValidator), + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + }); async function oldPasswordUpdateResponder( viewer: Viewer, - input: mixed, + request: UpdatePasswordRequest, ): Promise { - const request = await validateInput( - viewer, - updatePasswordRequestInputValidator, - input, - ); - if (request.calendarQuery) { request.calendarQuery = normalizeCalendarQuery(request.calendarQuery); } - const response = await updatePassword(viewer, request); - return validateOutput( - viewer.platformDetails, - logInResponseValidator, - response, - ); + return await updatePassword(viewer, request); } -const updateUserSettingsInputValidator = tShape({ - name: t.irreducible( - userSettingsTypes.DEFAULT_NOTIFICATIONS, - x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, - ), - data: t.enums.of(notificationTypeValues), -}); +export const updateUserSettingsInputValidator: TInterface = + tShape({ + name: t.irreducible( + userSettingsTypes.DEFAULT_NOTIFICATIONS, + x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, + ), + data: t.enums.of(notificationTypeValues), + }); async function updateUserSettingsResponder( viewer: Viewer, - input: mixed, + request: UpdateUserSettingsRequest, ): Promise { - const request = await validateInput( - viewer, - updateUserSettingsInputValidator, - input, - ); await updateUserSettings(viewer, request); } -const policyAcknowledgmentRequestInputValidator = +export const policyAcknowledgmentRequestInputValidator: TInterface = tShape({ policy: t.maybe(t.enums.of(policies)), }); async function policyAcknowledgmentResponder( viewer: Viewer, - input: mixed, + request: PolicyAcknowledgmentRequest, ): Promise { - const request = await validateInput( - viewer, - policyAcknowledgmentRequestInputValidator, - input, - ); await viewerAcknowledgmentUpdater(viewer, request.policy); } export const updateUserAvatarResponseValidator: TInterface = tShape({ updates: createUpdatesResultValidator, }); -const updateUserAvatarResponderValidator = t.union([ +export const updateUserAvatarResponderValidator: TUnion< + ?ClientAvatar | UpdateUserAvatarResponse, +> = t.union([ t.maybe(clientAvatarValidator), updateUserAvatarResponseValidator, ]); async function updateUserAvatarResponder( viewer: Viewer, - input: mixed, + request: UpdateUserAvatarRequest, ): Promise { - const request = await validateInput( - viewer, - updateUserAvatarRequestValidator, - input, - ); - const result = await updateUserAvatar(viewer, request); - return validateOutput( - viewer.platformDetails, - updateUserAvatarResponderValidator, - result, - ); + return await updateUserAvatar(viewer, request); } export { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, }; diff --git a/keyserver/src/responders/verification-responders.js b/keyserver/src/responders/verification-responders.js index 7f5813ec5..ba42f8523 100644 --- a/keyserver/src/responders/verification-responders.js +++ b/keyserver/src/responders/verification-responders.js @@ -1,27 +1,28 @@ // @flow import t from 'tcomb'; +import type { TInterface } from 'tcomb'; import type { HandleVerificationCodeResult } from 'lib/types/verify-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tShape } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; -import { validateInput } from '../utils/validation-utils.js'; -const codeVerificationRequestInputValidator = tShape({ - code: t.String, -}); +export type CodeVerificationRequest = { code: string }; + +export const codeVerificationRequestInputValidator: TInterface = + tShape({ + code: t.String, + }); /* eslint-disable no-unused-vars */ async function codeVerificationResponder( viewer: Viewer, - input: any, + request: CodeVerificationRequest, ): Promise { - /* eslint-enable no-unused-vars */ - await validateInput(viewer, codeVerificationRequestInputValidator, input); // We have no way to handle this request anymore throw new ServerError('deprecated'); } export { codeVerificationResponder }; diff --git a/keyserver/src/responders/version-responders.js b/keyserver/src/responders/version-responders.js index 6ab06ef76..e6e9554b6 100644 --- a/keyserver/src/responders/version-responders.js +++ b/keyserver/src/responders/version-responders.js @@ -1,25 +1,18 @@ // @flow import t, { type TInterface } from 'tcomb'; import { webAndKeyserverCodeVersion } from 'lib/facts/version.js'; import type { VersionResponse } from 'lib/types/device-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; -import type { Viewer } from '../session/viewer.js'; -import { validateOutput } from '../utils/validation-utils.js'; - -const versionResponseValidator: TInterface = +export const versionResponseValidator: TInterface = tShape({ codeVersion: t.Number }); const versionResponse = { codeVersion: webAndKeyserverCodeVersion }; -async function versionResponder(viewer: Viewer): Promise { - return validateOutput( - viewer.platformDetails, - versionResponseValidator, - versionResponse, - ); +async function versionResponder(): Promise { + return versionResponse; } export { versionResponder }; diff --git a/keyserver/src/uploads/uploads.js b/keyserver/src/uploads/uploads.js index 83b83887e..1fe59e8dd 100644 --- a/keyserver/src/uploads/uploads.js +++ b/keyserver/src/uploads/uploads.js @@ -1,259 +1,244 @@ // @flow import type { $Request, $Response, Middleware } from 'express'; import invariant from 'invariant'; import multer from 'multer'; import { Readable } from 'stream'; import t, { type TInterface } from 'tcomb'; import { type UploadMediaMetadataRequest, type UploadMultimediaResult, uploadMultimediaResultValidator, type UploadDeletionRequest, type Dimensions, } from 'lib/types/media-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import { getMediaType, validateAndConvert } from './media-utils.js'; import type { UploadInput } from '../creators/upload-creator.js'; import createUploads from '../creators/upload-creator.js'; import { deleteUpload } from '../deleters/upload-deleters.js'; import { fetchUpload, fetchUploadChunk, getUploadSize, } from '../fetchers/upload-fetchers.js'; import type { MulterRequest } from '../responders/handlers.js'; import type { Viewer } from '../session/viewer.js'; -import { validateInput, validateOutput } from '../utils/validation-utils.js'; +import { validateOutput } from '../utils/validation-utils.js'; const upload = multer(); const multerProcessor: Middleware<> = upload.array('multimedia'); type MultimediaUploadResult = { results: UploadMultimediaResult[], }; const MultimediaUploadResultValidator = tShape({ results: t.list(uploadMultimediaResultValidator), }); async function multimediaUploadResponder( viewer: Viewer, req: MulterRequest, ): Promise { const { files, body } = req; if (!files || !body || typeof body !== 'object') { throw new ServerError('invalid_parameters'); } const overrideFilename = files.length === 1 && body.filename ? body.filename : null; if (overrideFilename && typeof overrideFilename !== 'string') { throw new ServerError('invalid_parameters'); } const inputHeight = files.length === 1 && body.height ? parseInt(body.height) : null; const inputWidth = files.length === 1 && body.width ? parseInt(body.width) : null; if (!!inputHeight !== !!inputWidth) { throw new ServerError('invalid_parameters'); } const inputDimensions: ?Dimensions = inputHeight && inputWidth ? { height: inputHeight, width: inputWidth } : null; const inputLoop = !!(files.length === 1 && body.loop); const inputEncryptionKey = files.length === 1 && body.encryptionKey ? body.encryptionKey : null; if (inputEncryptionKey && typeof inputEncryptionKey !== 'string') { throw new ServerError('invalid_parameters'); } const inputMimeType = files.length === 1 && body.mimeType ? body.mimeType : null; if (inputMimeType && typeof inputMimeType !== 'string') { throw new ServerError('invalid_parameters'); } const inputThumbHash = files.length === 1 && body.thumbHash ? body.thumbHash : null; if (inputThumbHash && typeof inputThumbHash !== 'string') { throw new ServerError('invalid_parameters'); } const validationResults = await Promise.all( files.map(({ buffer, size, originalname }) => validateAndConvert({ initialBuffer: buffer, initialName: overrideFilename ? overrideFilename : originalname, inputDimensions, inputLoop, inputEncryptionKey, inputMimeType, inputThumbHash, size, }), ), ); const uploadInfos = validationResults.filter(Boolean); if (uploadInfos.length === 0) { throw new ServerError('invalid_parameters'); } const results = await createUploads(viewer, uploadInfos); return validateOutput( viewer.platformDetails, MultimediaUploadResultValidator, { results, }, ); } -const uploadMediaMetadataInputValidator = tShape({ - filename: t.String, - width: t.Number, - height: t.Number, - blobHolder: t.String, - blobHash: t.String, - encryptionKey: t.String, - mimeType: t.String, - loop: t.maybe(t.Boolean), - thumbHash: t.maybe(t.String), -}); +export const uploadMediaMetadataInputValidator: TInterface = + tShape({ + filename: t.String, + width: t.Number, + height: t.Number, + blobHolder: t.String, + blobHash: t.String, + encryptionKey: t.String, + mimeType: t.String, + loop: t.maybe(t.Boolean), + thumbHash: t.maybe(t.String), + }); async function uploadMediaMetadataResponder( viewer: Viewer, - input: mixed, + request: UploadMediaMetadataRequest, ): Promise { - const request = await validateInput( - viewer, - uploadMediaMetadataInputValidator, - input, - ); - const mediaType = getMediaType(request.mimeType); if (!mediaType) { throw new ServerError('invalid_parameters'); } const { filename, blobHolder, blobHash, encryptionKey, mimeType, width, height, loop, } = request; const uploadInfo: UploadInput = { name: filename, mime: mimeType, mediaType, content: { storage: 'blob_service', blobHolder, blobHash }, encryptionKey, dimensions: { width, height }, loop: loop ?? false, thumbHash: request.thumbHash, }; const [result] = await createUploads(viewer, [uploadInfo]); - return validateOutput( - viewer.platformDetails, - uploadMultimediaResultValidator, - result, - ); + return result; } async function uploadDownloadResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const { uploadID, secret } = req.params; if (!uploadID || !secret) { throw new ServerError('invalid_parameters'); } if (!req.headers.range) { const { content, mime } = await fetchUpload(viewer, uploadID, secret); res.type(mime); res.set('Cache-Control', 'public, max-age=31557600, immutable'); if (process.env.NODE_ENV === 'development') { // Add a CORS header to allow local development using localhost const port = process.env.PORT || '3000'; res.set('Access-Control-Allow-Origin', `http://localhost:${port}`); res.set('Access-Control-Allow-Methods', 'GET'); } res.send(content); } else { const totalUploadSize = await getUploadSize(uploadID, secret); const range = req.range(totalUploadSize); if (typeof range === 'number' && range < 0) { throw new ServerError( range === -1 ? 'unsatisfiable_range' : 'malformed_header_string', ); } invariant( Array.isArray(range), 'range should be Array in uploadDownloadResponder!', ); const { start, end } = range[0]; const respWidth = end - start + 1; const { content, mime } = await fetchUploadChunk( uploadID, secret, start, respWidth, ); const respRange = `${start}-${end}/${totalUploadSize}`; const respHeaders: { [key: string]: string } = { 'Accept-Ranges': 'bytes', 'Content-Range': `bytes ${respRange}`, 'Content-Type': mime, 'Content-Length': respWidth.toString(), }; if (process.env.NODE_ENV === 'development') { // Add a CORS header to allow local development using localhost const port = process.env.PORT || '3000'; respHeaders['Access-Control-Allow-Origin'] = `http://localhost:${port}`; respHeaders['Access-Control-Allow-Methods'] = 'GET'; } // HTTP 206 Partial Content // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 res.writeHead(206, respHeaders); const stream = new Readable(); stream.push(content); stream.push(null); stream.pipe(res); } } -const uploadDeletionRequestInputValidator: TInterface = +export const UploadDeletionRequestInputValidator: TInterface = tShape({ id: tID, }); async function uploadDeletionResponder( viewer: Viewer, - input: mixed, + { id }: UploadDeletionRequest, ): Promise { - const { id } = await validateInput( - viewer, - uploadDeletionRequestInputValidator, - input, - ); - await deleteUpload(viewer, id); } export { multerProcessor, multimediaUploadResponder, uploadDownloadResponder, uploadDeletionResponder, uploadMediaMetadataResponder, }; diff --git a/lib/types/activity-types.js b/lib/types/activity-types.js index dffee3bd2..52282d04c 100644 --- a/lib/types/activity-types.js +++ b/lib/types/activity-types.js @@ -1,55 +1,55 @@ // @flow import t, { type TInterface } from 'tcomb'; import { tID, tShape } from '../utils/validation-utils.js'; export type ActivityUpdate = { +focus: boolean, +threadID: string, +latestMessage: ?string, }; export const activityUpdateValidator: TInterface = tShape({ focus: t.Boolean, threadID: tID, latestMessage: t.maybe(tID), }); export type UpdateActivityRequest = { +updates: $ReadOnlyArray, }; export type UpdateActivityResult = { +unfocusedToUnread: $ReadOnlyArray, }; export const updateActivityResultValidator: TInterface = tShape({ unfocusedToUnread: t.list(tID), }); export type ActivityUpdateSuccessPayload = { +activityUpdates: $ReadOnlyArray, +result: UpdateActivityResult, }; export const queueActivityUpdatesActionType = 'QUEUE_ACTIVITY_UPDATES'; export type QueueActivityUpdatesPayload = { +activityUpdates: $ReadOnlyArray, }; export type SetThreadUnreadStatusRequest = { +unread: boolean, +threadID: string, +latestMessage: ?string, }; export type SetThreadUnreadStatusResult = { +resetToUnread: boolean, }; -export const setThreadUnreadStatusResult: TInterface = +export const setThreadUnreadStatusResultValidator: TInterface = tShape({ resetToUnread: t.Boolean }); export type SetThreadUnreadStatusPayload = { ...SetThreadUnreadStatusResult, +threadID: string, }; diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 1c83407d8..80c0fb9c1 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,58 +1,67 @@ // @flow +import t, { type TInterface } from 'tcomb'; + +import { tShape } from '../utils/validation-utils.js'; + export type OLMIdentityKeys = { +ed25519: string, +curve25519: string, }; export type OLMPrekey = { +curve25519: { +id: string, +key: string, }, }; export type OLMOneTimeKeys = { +curve25519: { +[string]: string }, }; export type PickledOLMAccount = { +picklingKey: string, +pickledAccount: string, }; export type CryptoStore = { +primaryAccount: ?PickledOLMAccount, +primaryIdentityKeys: ?OLMIdentityKeys, +notificationAccount: ?PickledOLMAccount, +notificationIdentityKeys: ?OLMIdentityKeys, }; export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, }; export type SignedIdentityKeysBlob = { +payload: string, +signature: string, }; +export const signedIdentityKeysBlobValidator: TInterface = + tShape({ + payload: t.String, + signature: t.String, + }); // This type should not be changed without making equivalent changes to // `Message` in Identity service's `reserved_users` module export type ReservedUsernameMessage = | { +statement: 'Add the following usernames to reserved list', +payload: $ReadOnlyArray, +issuedAt: string, } | { +statement: 'Remove the following username from reserved list', +payload: string, +issuedAt: string, }; export const olmEncryptedMessageTypes = Object.freeze({ PREKEY: 0, TEXT: 1, }); diff --git a/lib/types/request-types.js b/lib/types/request-types.js index cdd593ced..d9d85b0b0 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,293 +1,306 @@ // @flow import invariant from 'invariant'; -import t, { type TUnion } from 'tcomb'; +import t, { type TUnion, type TInterface } from 'tcomb'; import { type ActivityUpdate } from './activity-types.js'; import type { Shape } from './core.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; +import { signedIdentityKeysBlobValidator } from './crypto-types.js'; import type { Platform, PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery, rawEntryInfoValidator, } from './entry-types.js'; import type { ThreadInconsistencyReportShape, EntryInconsistencyReportShape, ClientThreadInconsistencyReportShape, ClientEntryInconsistencyReportShape, } from './report-types.js'; import { type RawThreadInfo, rawThreadInfoValidator } from './thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type OldCurrentUserInfo, oldCurrentUserInfoValidator, type AccountUserInfo, accountUserInfoValidator, } from './user-types.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; // "Server requests" are requests for information that the server delivers to // clients. Clients then respond to those requests with a "client response". export const serverRequestTypes = Object.freeze({ PLATFORM: 0, //DEVICE_TOKEN: 1, (DEPRECATED) THREAD_INCONSISTENCY: 2, PLATFORM_DETAILS: 3, //INITIAL_ACTIVITY_UPDATE: 4, (DEPRECATED) ENTRY_INCONSISTENCY: 5, CHECK_STATE: 6, INITIAL_ACTIVITY_UPDATES: 7, MORE_ONE_TIME_KEYS: 8, SIGNED_IDENTITY_KEYS_BLOB: 9, INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE: 10, }); type ServerRequestType = $Values; export function assertServerRequestType( serverRequestType: number, ): ServerRequestType { invariant( serverRequestType === 0 || serverRequestType === 2 || serverRequestType === 3 || serverRequestType === 5 || serverRequestType === 6 || serverRequestType === 7 || serverRequestType === 8 || serverRequestType === 9 || serverRequestType === 10, 'number is not ServerRequestType enum', ); return serverRequestType; } type PlatformServerRequest = { +type: 0, }; const platformServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM), }); type PlatformClientResponse = { +type: 0, +platform: Platform, }; export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; type PlatformDetailsServerRequest = { type: 3, }; const platformDetailsServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), }); type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; export type ServerCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Shape<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Shape<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo | OldCurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; const serverCheckStateServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashesToCheck: t.dict(t.String, t.Number), failUnmentioned: t.maybe( tShape({ threadInfos: t.maybe(t.Boolean), entryInfos: t.maybe(t.Boolean), userInfos: t.maybe(t.Boolean), }), ), stateChanges: t.maybe( tShape({ rawThreadInfos: t.maybe(t.list(rawThreadInfoValidator)), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), currentUserInfo: t.maybe( t.union([currentUserInfoValidator, oldCurrentUserInfoValidator]), ), userInfos: t.maybe(t.list(accountUserInfoValidator)), deleteThreadIDs: t.maybe(t.list(tID)), deleteEntryIDs: t.maybe(t.list(tID)), deleteUserInfoIDs: t.maybe(t.list(t.String)), }), ), }); type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; type MoreOneTimeKeysServerRequest = { +type: 8, }; const moreOneTimeKeysServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.MORE_ONE_TIME_KEYS), }); type MoreOneTimeKeysClientResponse = { +type: 8, +keys: $ReadOnlyArray, }; type SignedIdentityKeysBlobServerRequest = { +type: 9, }; const signedIdentityKeysBlobServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), }); type SignedIdentityKeysBlobClientResponse = { +type: 9, +signedIdentityKeysBlob: SignedIdentityKeysBlob, }; type InitialNotificationsEncryptedMessageServerRequest = { +type: 10, }; const initialNotificationsEncryptedMessageServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), }); type InitialNotificationsEncryptedMessageClientResponse = { +type: 10, +initialNotificationsEncryptedMessage: string, }; export type ServerServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ServerCheckStateServerRequest | MoreOneTimeKeysServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; export const serverServerRequestValidator: TUnion = t.union([ platformServerRequestValidator, platformDetailsServerRequestValidator, serverCheckStateServerRequestValidator, moreOneTimeKeysServerRequestValidator, signedIdentityKeysBlobServerRequestValidator, initialNotificationsEncryptedMessageServerRequestValidator, ]); export type ClientResponse = | PlatformClientResponse | ThreadInconsistencyClientResponse | PlatformDetailsClientResponse | EntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Shape<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Shape<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; export type ClientServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ClientCheckStateServerRequest | MoreOneTimeKeysServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; // This is just the client variant of ClientResponse. The server needs to handle // multiple client versions so the type supports old versions of certain client // responses, but the client variant only need to support the latest version. type ClientThreadInconsistencyClientResponse = { ...ClientThreadInconsistencyReportShape, +type: 2, }; type ClientEntryInconsistencyClientResponse = { +type: 5, ...ClientEntryInconsistencyReportShape, }; export type ClientClientResponse = | PlatformClientResponse | ClientThreadInconsistencyClientResponse | PlatformDetailsClientResponse | ClientEntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientInconsistencyResponse = | ClientThreadInconsistencyClientResponse | ClientEntryInconsistencyClientResponse; export const processServerRequestsActionType = 'PROCESS_SERVER_REQUESTS'; export type ProcessServerRequestsPayload = { +serverRequests: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type GetSessionPublicKeysArgs = { +session: string, }; export type OlmSessionInitializationInfo = { +prekey: string, +prekeySignature: string, +oneTimeKey: string, }; +export const olmSessionInitializationInfoValidator: TInterface = + tShape({ + prekey: t.String, + prekeySignature: t.String, + oneTimeKey: t.String, + }); export type GetOlmSessionInitializationDataResponse = { +signedIdentityKeysBlob: SignedIdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, }; +export const getOlmSessionInitializationDataResponseValidator: TInterface = + tShape({ + signedIdentityKeysBlob: signedIdentityKeysBlobValidator, + contentInitializationInfo: olmSessionInitializationInfoValidator, + notifInitializationInfo: olmSessionInitializationInfoValidator, + });