diff --git a/keyserver/src/responders/entry-responders.js b/keyserver/src/responders/entry-responders.js index 22d60ba5a..f630b97c5 100644 --- a/keyserver/src/responders/entry-responders.js +++ b/keyserver/src/responders/entry-responders.js @@ -1,263 +1,306 @@ // @flow import t from 'tcomb'; import type { TInterface } from 'tcomb'; import { filteredThreadIDs } from 'lib/selectors/calendar-filter-selectors.js'; -import type { - CalendarQuery, - SaveEntryRequest, - CreateEntryRequest, - DeleteEntryRequest, - DeleteEntryResponse, - RestoreEntryRequest, - RestoreEntryResponse, - FetchEntryInfosResponse, - DeltaEntryInfosResult, - SaveEntryResponse, +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, - FetchEntryRevisionInfosRequest, +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 } from 'lib/utils/validation-utils.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 } 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(t.String), }), ]), ), ), }); 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(t.String), }), ]), ), }); 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: any, ): Promise { await validateInput(viewer, entryQueryInputValidator, input); const request = normalizeCalendarQuery(input); await verifyCalendarQueryThreadIDs(request); const response = await fetchEntryInfos(viewer, [request]); return { ...response, userInfos: {} }; } const entryRevisionHistoryFetchInputValidator = tShape({ id: t.String, }); +export const fetchEntryRevisionInfosResultValidator: TInterface = + tShape({ + result: t.list(historyRevisionInfoValidator), + }); + async function entryRevisionFetchResponder( viewer: Viewer, input: any, ): Promise { const request: FetchEntryRevisionInfosRequest = input; await validateInput(viewer, entryRevisionHistoryFetchInputValidator, request); const entryHistory = await fetchEntryRevisionInfo(viewer, request.id); return { result: entryHistory }; } const createEntryRequestInputValidator = tShape({ text: t.String, sessionID: t.maybe(t.String), timestamp: t.Number, date: tDate, threadID: t.String, 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: any, ): Promise { const request: CreateEntryRequest = input; await validateInput(viewer, createEntryRequestInputValidator, request); return await createEntry(viewer, request); } const saveEntryRequestInputValidator = tShape({ entryID: t.String, text: t.String, prevText: t.String, sessionID: t.maybe(t.String), timestamp: t.Number, calendarQuery: t.maybe(newEntryQueryInputValidator), }); async function entryUpdateResponder( viewer: Viewer, input: any, ): Promise { const request: SaveEntryRequest = input; await validateInput(viewer, saveEntryRequestInputValidator, request); return await updateEntry(viewer, request); } const deleteEntryRequestInputValidator = tShape({ entryID: t.String, 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: any, ): Promise { const request: DeleteEntryRequest = input; await validateInput(viewer, deleteEntryRequestInputValidator, request); return await deleteEntry(viewer, request); } const restoreEntryRequestInputValidator = tShape({ entryID: t.String, 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: any, ): Promise { const request: RestoreEntryRequest = input; await validateInput(viewer, restoreEntryRequestInputValidator, request); 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: any, ): Promise { const request: CalendarQuery = input; 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 { 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/keys-responders.js b/keyserver/src/responders/keys-responders.js index 57691537c..f8719d9f0 100644 --- a/keyserver/src/responders/keys-responders.js +++ b/keyserver/src/responders/keys-responders.js @@ -1,29 +1,36 @@ // @flow -import t from 'tcomb'; +import t, { type TUnion } from 'tcomb'; import type { GetSessionPublicKeysArgs } from 'lib/types/request-types.js'; -import type { SessionPublicKeys } from 'lib/types/session-types.js'; -import { tShape } from 'lib/utils/validation-utils.js'; +import { + type SessionPublicKeys, + sessionPublicKeysValidator, +} from 'lib/types/session-types.js'; +import { tShape, tNull } from 'lib/utils/validation-utils.js'; import { fetchSessionPublicKeys } from '../fetchers/key-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput } from '../utils/validation-utils.js'; const getSessionPublicKeysInputValidator = tShape({ session: t.String, }); +type GetSessionPublicKeysResponse = SessionPublicKeys | null; +export const getSessionPublicKeysResponseValidator: TUnion = + t.union([sessionPublicKeysValidator, tNull]); + async function getSessionPublicKeysResponder( viewer: Viewer, input: any, -): Promise { +): Promise { if (!viewer.loggedIn) { return null; } const request: GetSessionPublicKeysArgs = input; await validateInput(viewer, getSessionPublicKeysInputValidator, request); return await fetchSessionPublicKeys(request.session); } export { getSessionPublicKeysResponder }; diff --git a/keyserver/src/responders/message-report-responder.js b/keyserver/src/responders/message-report-responder.js index 250088cff..a0336c7aa 100644 --- a/keyserver/src/responders/message-report-responder.js +++ b/keyserver/src/responders/message-report-responder.js @@ -1,34 +1,38 @@ // @flow -import t from 'tcomb'; +import t, { 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 } from 'lib/utils/validation-utils.js'; import createMessageReport from '../creators/message-report-creator.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput } from '../utils/validation-utils.js'; const messageReportCreationRequestInputValidator = tShape({ messageID: t.String, }); +export const messageReportCreationResultValidator: TInterface = + tShape({ messageInfo: rawMessageInfoValidator }); + async function messageReportCreationResponder( viewer: Viewer, input: any, ): Promise { await validateInput( viewer, messageReportCreationRequestInputValidator, input, ); const request: MessageReportCreationRequest = input; const rawMessageInfos = await createMessageReport(viewer, request); return { messageInfo: rawMessageInfos[0] }; } export { messageReportCreationResponder }; diff --git a/keyserver/src/responders/relationship-responders.js b/keyserver/src/responders/relationship-responders.js index 0d2130a9d..7f96bb94d 100644 --- a/keyserver/src/responders/relationship-responders.js +++ b/keyserver/src/responders/relationship-responders.js @@ -1,30 +1,37 @@ // @flow -import t from 'tcomb'; +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 } from '../utils/validation-utils.js'; const updateRelationshipInputValidator = 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: any, ): Promise { const request: RelationshipRequest = input; await validateInput(viewer, updateRelationshipInputValidator, request); return await updateRelationships(viewer, request); } export { updateRelationshipsResponder }; diff --git a/keyserver/src/responders/responder-validators.test.js b/keyserver/src/responders/responder-validators.test.js index d20d2ee70..e66743580 100644 --- a/keyserver/src/responders/responder-validators.test.js +++ b/keyserver/src/responders/responder-validators.test.js @@ -1,333 +1,619 @@ // @flow +import { + setThreadUnreadStatusResult, + 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 { messageReportCreationResultValidator } from './message-report-responder.js'; +import { relationshipErrorsValidator } from './relationship-responders.js'; +import { userSearchResultValidator } from './search-responders.js'; +import { siweNonceResponseValidator } from './siwe-nonce-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, + ); + }); +}); + +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); + }); +}); diff --git a/keyserver/src/responders/search-responders.js b/keyserver/src/responders/search-responders.js index d8c5a89e2..7008add66 100644 --- a/keyserver/src/responders/search-responders.js +++ b/keyserver/src/responders/search-responders.js @@ -1,29 +1,35 @@ // @flow -import t from 'tcomb'; +import t, { type TInterface } from 'tcomb'; import type { UserSearchRequest, UserSearchResult, } from 'lib/types/search-types.js'; +import { globalAccountUserInfoValidator } from 'lib/types/user-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import { searchForUsers } from '../search/users.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput } from '../utils/validation-utils.js'; const userSearchRequestInputValidator = tShape({ prefix: t.maybe(t.String), }); +export const userSearchResultValidator: TInterface = + tShape({ + userInfos: t.list(globalAccountUserInfoValidator), + }); + async function userSearchResponder( viewer: Viewer, input: any, ): Promise { const request: UserSearchRequest = input; await validateInput(viewer, userSearchRequestInputValidator, request); const searchResults = await searchForUsers(request); return { userInfos: searchResults }; } export { userSearchResponder }; diff --git a/keyserver/src/responders/siwe-nonce-responders.js b/keyserver/src/responders/siwe-nonce-responders.js index 69636c117..4a008ac9c 100644 --- a/keyserver/src/responders/siwe-nonce-responders.js +++ b/keyserver/src/responders/siwe-nonce-responders.js @@ -1,15 +1,20 @@ // @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'; +export const siweNonceResponseValidator: TInterface = + tShape({ nonce: t.String }); + async function siweNonceResponder(): Promise { const generatedNonce = generateNonce(); await createSIWENonceEntry(generatedNonce); return { nonce: generatedNonce }; } export { siweNonceResponder }; diff --git a/lib/types/activity-types.js b/lib/types/activity-types.js index e498a5116..13a1ac93a 100644 --- a/lib/types/activity-types.js +++ b/lib/types/activity-types.js @@ -1,38 +1,49 @@ // @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 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 = + tShape({ resetToUnread: t.Boolean }); + export type SetThreadUnreadStatusPayload = { ...SetThreadUnreadStatusResult, +threadID: string, }; diff --git a/lib/types/history-types.js b/lib/types/history-types.js index 3d7815d00..d6749b402 100644 --- a/lib/types/history-types.js +++ b/lib/types/history-types.js @@ -1,20 +1,34 @@ // @flow +import t, { type TInterface } from 'tcomb'; + +import { tID, tShape } from '../utils/validation-utils.js'; + export type HistoryMode = 'day' | 'entry'; export type HistoryRevisionInfo = { +id: string, +entryID: string, +authorID: string, +text: string, +lastUpdate: number, +deleted: boolean, +threadID: string, }; +export const historyRevisionInfoValidator: TInterface = + tShape({ + id: tID, + entryID: tID, + authorID: t.String, + text: t.String, + lastUpdate: t.Number, + deleted: t.Boolean, + threadID: tID, + }); export type FetchEntryRevisionInfosRequest = { +id: string, }; export type FetchEntryRevisionInfosResult = { +result: $ReadOnlyArray, }; diff --git a/lib/types/session-types.js b/lib/types/session-types.js index 66b87f4e3..bb0ecad6c 100644 --- a/lib/types/session-types.js +++ b/lib/types/session-types.js @@ -1,109 +1,118 @@ // @flow +import t, { type TInterface } from 'tcomb'; + import type { LogInActionSource } from './account-types.js'; import type { Shape } from './core.js'; import type { CalendarQuery } from './entry-types.js'; import type { RawThreadInfo } from './thread-types.js'; import { type UserInfo, type CurrentUserInfo, type LoggedOutUserInfo, } from './user-types.js'; +import { tShape } from '../utils/validation-utils.js'; export const cookieLifetime = 30 * 24 * 60 * 60 * 1000; // in milliseconds // Interval the server waits after a state check before starting a new one export const sessionCheckFrequency = 3 * 60 * 1000; // in milliseconds // How long the server debounces after activity before initiating a state check export const stateCheckInactivityActivationInterval = 3 * 1000; // in milliseconds // On native, we specify the cookie directly in the request and response body. // We do this because: // (1) We don't have the same XSS risks as we do on web, so there is no need to // prevent JavaScript from knowing the cookie password. // (2) In the past the internal cookie logic on Android has been buggy. // https://github.com/facebook/react-native/issues/12956 is an example // issue. By specifying the cookie in the body we retain full control of how // that data is passed, without necessitating any native modules like // react-native-cookies. export const cookieSources = Object.freeze({ BODY: 0, HEADER: 1, }); export type CookieSource = $Values; // On native, we use the cookieID as a unique session identifier. This is // because there is no way to have two instances of an app running. On the other // hand, on web it is possible to have two sessions open using the same cookie, // so we have a unique sessionID specified in the request body. export const sessionIdentifierTypes = Object.freeze({ COOKIE_ID: 0, BODY_SESSION_ID: 1, }); export type SessionIdentifierType = $Values; export const cookieTypes = Object.freeze({ USER: 'user', ANONYMOUS: 'anonymous', }); export type CookieType = $Values; export type ServerSessionChange = | { cookieInvalidated: false, threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, sessionID?: null | string, cookie?: string, } | { cookieInvalidated: true, threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, currentUserInfo: LoggedOutUserInfo, sessionID?: null | string, cookie?: string, }; export type ClientSessionChange = | { +cookieInvalidated: false, +currentUserInfo?: ?CurrentUserInfo, +sessionID?: null | string, +cookie?: string, } | { +cookieInvalidated: true, +currentUserInfo: LoggedOutUserInfo, +sessionID?: null | string, +cookie?: string, }; export type PreRequestUserState = { +currentUserInfo: ?CurrentUserInfo, +cookie: ?string, +sessionID: ?string, }; export type SetSessionPayload = { sessionChange: ClientSessionChange, preRequestUserState: ?PreRequestUserState, error: ?string, logInActionSource: ?LogInActionSource, }; export type SessionState = { calendarQuery: CalendarQuery, messagesCurrentAsOf: number, updatesCurrentAsOf: number, watchedIDs: $ReadOnlyArray, }; export type SessionIdentification = Shape<{ cookie: ?string, sessionID: ?string, }>; export type SessionPublicKeys = { +identityKey: string, +oneTimeKey?: string, }; + +export const sessionPublicKeysValidator: TInterface = + tShape({ + identityKey: t.String, + oneTimeKey: t.maybe(t.String), + }); diff --git a/lib/types/update-types.js b/lib/types/update-types.js index 1a08e62aa..84f4040ac 100644 --- a/lib/types/update-types.js +++ b/lib/types/update-types.js @@ -1,408 +1,415 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type RawEntryInfo, rawEntryInfoValidator } from './entry-types.js'; import { type RawMessageInfo, rawMessageInfoValidator, type MessageTruncationStatus, messageTruncationStatusValidator, } from './message-types.js'; import { type RawThreadInfo, rawThreadInfoValidator } from './thread-types.js'; import { type UserInfo, + userInfoValidator, type UserInfos, type LoggedInUserInfo, loggedInUserInfoValidator, type OldLoggedInUserInfo, oldLoggedInUserInfoValidator, } from './user-types.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; export const updateTypes = Object.freeze({ DELETE_ACCOUNT: 0, UPDATE_THREAD: 1, UPDATE_THREAD_READ_STATUS: 2, DELETE_THREAD: 3, JOIN_THREAD: 4, BAD_DEVICE_TOKEN: 5, UPDATE_ENTRY: 6, UPDATE_CURRENT_USER: 7, UPDATE_USER: 8, }); export type UpdateType = $Values; export function assertUpdateType(ourUpdateType: number): UpdateType { invariant( ourUpdateType === 0 || ourUpdateType === 1 || ourUpdateType === 2 || ourUpdateType === 3 || ourUpdateType === 4 || ourUpdateType === 5 || ourUpdateType === 6 || ourUpdateType === 7 || ourUpdateType === 8, 'number is not UpdateType enum', ); return ourUpdateType; } type AccountDeletionData = { +deletedUserID: string, }; type ThreadData = { +threadID: string, }; type ThreadReadStatusData = { +threadID: string, +unread: boolean, }; type ThreadDeletionData = { +threadID: string, }; type ThreadJoinData = { +threadID: string, }; type BadDeviceTokenData = { +deviceToken: string, }; type EntryData = { +entryID: string, }; type CurrentUserData = {}; type UserData = { // ID of the UserInfo being updated +updatedUserID: string, }; type SharedUpdateData = { +userID: string, +time: number, }; type AccountDeletionUpdateData = { ...SharedUpdateData, ...AccountDeletionData, +type: 0, }; type ThreadUpdateData = { ...SharedUpdateData, ...ThreadData, +type: 1, +targetSession?: string, }; type ThreadReadStatusUpdateData = { ...SharedUpdateData, ...ThreadReadStatusData, +type: 2, }; type ThreadDeletionUpdateData = { ...SharedUpdateData, ...ThreadDeletionData, +type: 3, }; type ThreadJoinUpdateData = { ...SharedUpdateData, ...ThreadJoinData, +type: 4, +targetSession?: string, }; type BadDeviceTokenUpdateData = { ...SharedUpdateData, ...BadDeviceTokenData, +type: 5, +targetCookie: string, }; type EntryUpdateData = { ...SharedUpdateData, ...EntryData, +type: 6, +targetSession: string, }; type CurrentUserUpdateData = { ...SharedUpdateData, ...CurrentUserData, +type: 7, }; type UserUpdateData = { ...SharedUpdateData, ...UserData, +type: 8, +targetSession?: string, }; export type UpdateData = | AccountDeletionUpdateData | ThreadUpdateData | ThreadReadStatusUpdateData | ThreadDeletionUpdateData | ThreadJoinUpdateData | BadDeviceTokenUpdateData | EntryUpdateData | CurrentUserUpdateData | UserUpdateData; type SharedRawUpdateInfo = { +id: string, +time: number, }; type AccountDeletionRawUpdateInfo = { ...SharedRawUpdateInfo, ...AccountDeletionData, +type: 0, }; type ThreadRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadData, +type: 1, }; type ThreadReadStatusRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadReadStatusData, +type: 2, }; type ThreadDeletionRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadDeletionData, +type: 3, }; type ThreadJoinRawUpdateInfo = { ...SharedRawUpdateInfo, ...ThreadJoinData, +type: 4, }; type BadDeviceTokenRawUpdateInfo = { ...SharedRawUpdateInfo, ...BadDeviceTokenData, +type: 5, }; type EntryRawUpdateInfo = { ...SharedRawUpdateInfo, ...EntryData, +type: 6, }; type CurrentUserRawUpdateInfo = { ...SharedRawUpdateInfo, ...CurrentUserData, +type: 7, }; type UserRawUpdateInfo = { ...SharedRawUpdateInfo, ...UserData, +type: 8, }; export type RawUpdateInfo = | AccountDeletionRawUpdateInfo | ThreadRawUpdateInfo | ThreadReadStatusRawUpdateInfo | ThreadDeletionRawUpdateInfo | ThreadJoinRawUpdateInfo | BadDeviceTokenRawUpdateInfo | EntryRawUpdateInfo | CurrentUserRawUpdateInfo | UserRawUpdateInfo; type AccountDeletionUpdateInfo = { +type: 0, +id: string, +time: number, +deletedUserID: string, }; export const accountDeletionUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.DELETE_ACCOUNT), id: t.String, time: t.Number, deletedUserID: t.String, }); type ThreadUpdateInfo = { +type: 1, +id: string, +time: number, +threadInfo: RawThreadInfo, }; export const threadUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_THREAD), id: t.String, time: t.Number, threadInfo: rawThreadInfoValidator, }); type ThreadReadStatusUpdateInfo = { +type: 2, +id: string, +time: number, +threadID: string, +unread: boolean, }; export const threadReadStatusUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_THREAD_READ_STATUS), id: t.String, time: t.Number, threadID: tID, unread: t.Boolean, }); type ThreadDeletionUpdateInfo = { +type: 3, +id: string, +time: number, +threadID: string, }; export const threadDeletionUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.DELETE_THREAD), id: t.String, time: t.Number, threadID: tID, }); type ThreadJoinUpdateInfo = { +type: 4, +id: string, +time: number, +threadInfo: RawThreadInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatus: MessageTruncationStatus, +rawEntryInfos: $ReadOnlyArray, }; export const threadJoinUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.JOIN_THREAD), id: t.String, time: t.Number, threadInfo: rawThreadInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatus: messageTruncationStatusValidator, rawEntryInfos: t.list(rawEntryInfoValidator), }); type BadDeviceTokenUpdateInfo = { +type: 5, +id: string, +time: number, +deviceToken: string, }; export const badDeviceTokenUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.BAD_DEVICE_TOKEN), id: t.String, time: t.Number, deviceToken: t.String, }); type EntryUpdateInfo = { +type: 6, +id: string, +time: number, +entryInfo: RawEntryInfo, }; export const entryUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_ENTRY), id: t.String, time: t.Number, entryInfo: rawEntryInfoValidator, }); type CurrentUserUpdateInfo = { +type: 7, +id: string, +time: number, +currentUserInfo: LoggedInUserInfo, }; type UserUpdateInfo = { +type: 8, +id: string, +time: number, // Updated UserInfo is already contained within the UpdatesResultWithUserInfos +updatedUserID: string, }; export const userUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_USER), id: t.String, time: t.Number, updatedUserID: t.String, }); export type ClientUpdateInfo = | AccountDeletionUpdateInfo | ThreadUpdateInfo | ThreadReadStatusUpdateInfo | ThreadDeletionUpdateInfo | ThreadJoinUpdateInfo | BadDeviceTokenUpdateInfo | EntryUpdateInfo | CurrentUserUpdateInfo | UserUpdateInfo; type ServerCurrentUserUpdateInfo = { +type: 7, +id: string, +time: number, +currentUserInfo: LoggedInUserInfo | OldLoggedInUserInfo, }; export const serverCurrentUserUpdateInfoValidator: TInterface = tShape({ type: tNumber(updateTypes.UPDATE_CURRENT_USER), id: t.String, time: t.Number, currentUserInfo: t.union([ loggedInUserInfoValidator, oldLoggedInUserInfoValidator, ]), }); export type ServerUpdateInfo = | AccountDeletionUpdateInfo | ThreadUpdateInfo | ThreadReadStatusUpdateInfo | ThreadDeletionUpdateInfo | ThreadJoinUpdateInfo | BadDeviceTokenUpdateInfo | EntryUpdateInfo | ServerCurrentUserUpdateInfo | UserUpdateInfo; export const serverUpdateInfoValidator: TUnion = t.union([ accountDeletionUpdateInfoValidator, threadUpdateInfoValidator, threadReadStatusUpdateInfoValidator, threadDeletionUpdateInfoValidator, threadJoinUpdateInfoValidator, badDeviceTokenUpdateInfoValidator, entryUpdateInfoValidator, serverCurrentUserUpdateInfoValidator, userUpdateInfoValidator, ]); export type ServerUpdatesResult = { +currentAsOf: number, +newUpdates: $ReadOnlyArray, }; export type ServerUpdatesResultWithUserInfos = { +updatesResult: ServerUpdatesResult, +userInfos: $ReadOnlyArray, }; export type ClientUpdatesResult = { +currentAsOf: number, +newUpdates: $ReadOnlyArray, }; export type ClientUpdatesResultWithUserInfos = { +updatesResult: ClientUpdatesResult, +userInfos: $ReadOnlyArray, }; export type CreateUpdatesResult = { +viewerUpdates: $ReadOnlyArray, +userInfos: UserInfos, }; export type ServerCreateUpdatesResponse = { +viewerUpdates: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; +export const serverCreateUpdatesResponseValidator: TInterface = + tShape({ + viewerUpdates: t.list(serverUpdateInfoValidator), + userInfos: t.list(userInfoValidator), + }); + export type ClientCreateUpdatesResponse = { +viewerUpdates: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export const processUpdatesActionType = 'PROCESS_UPDATES'; diff --git a/lib/types/user-types.js b/lib/types/user-types.js index 837ccca9d..909c4a73b 100644 --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -1,115 +1,128 @@ // @flow import t, { type TInterface } from 'tcomb'; import { type DefaultNotificationPayload, defaultNotificationPayloadValidator, } from './account-types.js'; import { type ClientAvatar, clientAvatarValidator } from './avatar-types.js'; import { type UserRelationshipStatus, userRelationshipStatusValidator, } from './relationship-types.js'; import type { UserInconsistencyReportCreationRequest } from './report-types.js'; import { tBool, tShape } from '../utils/validation-utils.js'; export type GlobalUserInfo = { +id: string, +username: ?string, +avatar?: ?ClientAvatar, }; export type GlobalAccountUserInfo = { +id: string, +username: string, +avatar?: ?ClientAvatar, }; +export const globalAccountUserInfoValidator: TInterface = + tShape({ + id: t.String, + username: t.String, + avatar: t.maybe(clientAvatarValidator), + }); export type UserInfo = { +id: string, +username: ?string, +relationshipStatus?: UserRelationshipStatus, +avatar?: ?ClientAvatar, }; export const userInfoValidator: TInterface = tShape({ id: t.String, username: t.maybe(t.String), relationshipStatus: t.maybe(userRelationshipStatusValidator), avatar: t.maybe(clientAvatarValidator), }); export type UserInfos = { +[id: string]: UserInfo }; export type AccountUserInfo = { +id: string, +username: string, +relationshipStatus?: UserRelationshipStatus, +avatar?: ?ClientAvatar, }; +export const accountUserInfoValidator: TInterface = + tShape({ + id: t.String, + username: t.String, + relationshipStatus: t.maybe(userRelationshipStatusValidator), + avatar: t.maybe(clientAvatarValidator), + }); export type UserStore = { +userInfos: UserInfos, +inconsistencyReports: $ReadOnlyArray, }; export type RelativeUserInfo = { +id: string, +username: ?string, +isViewer: boolean, +avatar?: ?ClientAvatar, }; export type OldLoggedInUserInfo = { +id: string, +username: string, +email: string, +emailVerified: boolean, }; export const oldLoggedInUserInfoValidator: TInterface = tShape({ id: t.String, username: t.String, email: t.String, emailVerified: t.Boolean, }); export type LoggedInUserInfo = { +id: string, +username: string, +settings?: DefaultNotificationPayload, +avatar?: ?ClientAvatar, }; export const loggedInUserInfoValidator: TInterface = tShape({ id: t.String, username: t.String, settings: t.maybe(defaultNotificationPayloadValidator), avatar: t.maybe(clientAvatarValidator), }); export type LoggedOutUserInfo = { +id: string, +anonymous: true, }; export const loggedOutUserInfoValidator: TInterface = tShape({ id: t.String, anonymous: tBool(true) }); export type OldCurrentUserInfo = OldLoggedInUserInfo | LoggedOutUserInfo; export type CurrentUserInfo = LoggedInUserInfo | LoggedOutUserInfo; export type PasswordUpdate = { +updatedFields: { +password?: ?string, }, +currentPassword: string, }; export type UserListItem = { +id: string, +username: string, +disabled?: boolean, +notice?: string, +alertText?: string, +alertTitle?: string, +avatar?: ?ClientAvatar, }; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js index a9d8e5300..9b6e8b654 100644 --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -1,125 +1,126 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import type { TStructProps, TIrreducible, TRefinement, TEnums, TInterface, TUnion, TType, } from 'tcomb'; import { validEmailRegex, oldValidUsernameRegex, validHexColorRegex, } from '../shared/account-utils.js'; import type { PlatformDetails } from '../types/device-types'; import type { MediaMessageServerDBContent, PhotoMessageServerDBContent, VideoMessageServerDBContent, } from '../types/messages/media'; function tBool(value: boolean): TIrreducible { return t.irreducible('literal bool', x => x === value); } function tString(value: string): TIrreducible { return t.irreducible('literal string', x => x === value); } function tNumber(value: number): TIrreducible { return t.irreducible('literal number', x => x === value); } function tShape(spec: TStructProps): TInterface { return t.interface(spec, { strict: true }); } type TRegex = TRefinement; function tRegex(regex: RegExp): TRegex { return t.refinement(t.String, val => regex.test(val)); } function tNumEnum(nums: $ReadOnlyArray): TRefinement { return t.refinement(t.Number, (input: number) => { for (const num of nums) { if (input === num) { return true; } } return false; }); } - +const tNull: TIrreducible = t.irreducible('null', x => x === null); const tDate: TRegex = tRegex(/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/); const tColor: TRegex = tRegex(validHexColorRegex); // we don't include # char const tPlatform: TEnums = t.enums.of([ 'ios', 'android', 'web', 'windows', 'macos', ]); const tDeviceType: TEnums = t.enums.of(['ios', 'android']); const tPlatformDetails: TInterface = tShape({ platform: tPlatform, codeVersion: t.maybe(t.Number), stateVersion: t.maybe(t.Number), }); const tPassword: TRefinement = t.refinement( t.String, (password: string) => !!password, ); const tCookie: TRegex = tRegex(/^(user|anonymous)=[0-9]+:[0-9a-f]+$/); const tEmail: TRegex = tRegex(validEmailRegex); const tOldValidUsername: TRegex = tRegex(oldValidUsernameRegex); const tID: TRefinement = t.refinement(t.String, (id: string) => !!id); const tMediaMessagePhoto: TInterface = tShape({ type: tString('photo'), uploadID: t.String, }); const tMediaMessageVideo: TInterface = tShape({ type: tString('video'), uploadID: t.String, thumbnailUploadID: t.String, }); const tMediaMessageMedia: TUnion = t.union([ tMediaMessagePhoto, tMediaMessageVideo, ]); function assertWithValidator(data: mixed, validator: TType): T { invariant(validator.is(data), "data isn't of type T"); return (data: any); } export { tBool, tString, tNumber, tShape, tRegex, tNumEnum, + tNull, tDate, tColor, tPlatform, tDeviceType, tPlatformDetails, tPassword, tCookie, tEmail, tOldValidUsername, tID, tMediaMessagePhoto, tMediaMessageVideo, tMediaMessageMedia, assertWithValidator, };