Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js
index 6984b5781..e6cec824b 100644
--- a/keyserver/src/endpoints.js
+++ b/keyserver/src/endpoints.js
@@ -1,247 +1,252 @@
// @flow
import { baseLegalPolicies } from 'lib/facts/policies.js';
import type { Endpoint } from 'lib/types/endpoints.js';
import {
updateActivityResponder,
threadSetUnreadStatusResponder,
} from './responders/activity-responders.js';
import { deviceTokenUpdateResponder } from './responders/device-responders.js';
import {
entryFetchResponder,
entryRevisionFetchResponder,
entryCreationResponder,
entryUpdateResponder,
entryDeletionResponder,
entryRestorationResponder,
calendarQueryUpdateResponder,
} from './responders/entry-responders.js';
import type { JSONResponder } from './responders/handlers.js';
import { getSessionPublicKeysResponder } from './responders/keys-responders.js';
import { messageReportCreationResponder } from './responders/message-report-responder.js';
import {
textMessageCreationResponder,
messageFetchResponder,
multimediaMessageCreationResponder,
reactionMessageCreationResponder,
} from './responders/message-responders.js';
import { updateRelationshipsResponder } from './responders/relationship-responders.js';
import {
reportCreationResponder,
reportMultiCreationResponder,
errorReportFetchInfosResponder,
} from './responders/report-responders.js';
import { userSearchResponder } from './responders/search-responders.js';
import { siweNonceResponder } from './responders/siwe-nonce-responders.js';
import {
threadDeletionResponder,
roleUpdateResponder,
memberRemovalResponder,
threadLeaveResponder,
threadUpdateResponder,
threadCreationResponder,
threadFetchMediaResponder,
threadJoinResponder,
} from './responders/thread-responders.js';
import {
userSubscriptionUpdateResponder,
passwordUpdateResponder,
sendVerificationEmailResponder,
sendPasswordResetEmailResponder,
logOutResponder,
accountDeletionResponder,
accountCreationResponder,
logInResponder,
siweAuthResponder,
oldPasswordUpdateResponder,
updateUserSettingsResponder,
policyAcknowledgmentResponder,
+ updateUserAvatarResponder,
} from './responders/user-responders.js';
import { codeVerificationResponder } from './responders/verification-responders.js';
import { uploadDeletionResponder } from './uploads/uploads.js';
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_reaction_message: {
responder: reactionMessageCreationResponder,
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_thread: {
responder: threadDeletionResponder,
requiredPolicies: baseLegalPolicies,
},
delete_upload: {
responder: uploadDeletionResponder,
requiredPolicies: baseLegalPolicies,
},
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_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: [],
},
policy_acknowledgment: {
responder: policyAcknowledgmentResponder,
requiredPolicies: [],
},
remove_members: {
responder: memberRemovalResponder,
requiredPolicies: baseLegalPolicies,
},
restore_entry: {
responder: entryRestorationResponder,
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,
},
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,
},
siwe_nonce: {
responder: siweNonceResponder,
requiredPolicies: [],
},
siwe_auth: {
responder: siweAuthResponder,
requiredPolicies: [],
},
+ update_user_avatar: {
+ responder: updateUserAvatarResponder,
+ requiredPolicies: baseLegalPolicies,
+ },
};
export { jsonEndpoints };
diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js
index 230a0f1a6..e8697cf23 100644
--- a/keyserver/src/responders/user-responders.js
+++ b/keyserver/src/responders/user-responders.js
@@ -1,686 +1,697 @@
// @flow
import type { Utility as OlmUtility } from '@matrix-org/olm';
import invariant from 'invariant';
import { getRustAPI } from 'rust-node-addon';
import { ErrorTypes, SiweMessage } from 'siwe';
import t from 'tcomb';
import bcrypt from 'twin-bcrypt';
import { baseLegalPolicies, policies } 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 { UpdateUserAvatarRequest } from 'lib/types/avatar-types.js';
import type {
IdentityKeysBlob,
SignedIdentityKeysBlob,
} from 'lib/types/crypto-types.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import { defaultNumberPerThread } from 'lib/types/message-types.js';
import type {
SIWEAuthRequest,
SIWEMessage,
SIWESocialProof,
} from 'lib/types/siwe-types.js';
import type {
SubscriptionUpdateRequest,
SubscriptionUpdateResponse,
} from 'lib/types/subscription-types.js';
import type { PasswordUpdate } 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,
} from 'lib/utils/validation-utils.js';
import {
entryQueryInputValidator,
newEntryQueryInputValidator,
normalizeCalendarQuery,
verifyCalendarQueryThreadIDs,
} from './entry-responders.js';
import { handleAsyncPromise } from './handlers.js';
import {
createAccount,
processSIWEAccountCreation,
} from '../creators/account-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 type { Viewer } from '../session/viewer.js';
import {
accountUpdater,
checkAndSendVerificationEmail,
checkAndSendPasswordResetEmail,
updatePassword,
updateUserSettings,
} 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 } from '../utils/validation-utils.js';
const subscriptionUpdateRequestInputValidator = tShape({
threadID: t.String,
updatedFields: tShape({
pushNotifs: t.maybe(t.Boolean),
home: t.maybe(t.Boolean),
}),
});
async function userSubscriptionUpdateResponder(
viewer: Viewer,
input: any,
): Promise<SubscriptionUpdateResponse> {
const request: SubscriptionUpdateRequest = input;
await validateInput(viewer, subscriptionUpdateRequestInputValidator, request);
const threadSubscription = await userSubscriptionUpdater(viewer, request);
return { threadSubscription };
}
const accountUpdateInputValidator = tShape({
updatedFields: tShape({
email: t.maybe(tEmail),
password: t.maybe(tPassword),
}),
currentPassword: tPassword,
});
async function passwordUpdateResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: PasswordUpdate = input;
await validateInput(viewer, accountUpdateInputValidator, request);
await accountUpdater(viewer, request);
}
async function sendVerificationEmailResponder(viewer: Viewer): Promise<void> {
await validateInput(viewer, null, null);
await checkAndSendVerificationEmail(viewer);
}
const resetPasswordRequestInputValidator = tShape({
usernameOrEmail: t.union([tEmail, tOldValidUsername]),
});
async function sendPasswordResetEmailResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: ResetPasswordRequest = input;
await validateInput(viewer, resetPasswordRequestInputValidator, request);
await checkAndSendPasswordResetEmail(request);
}
async function logOutResponder(viewer: Viewer): Promise<LogOutResponse> {
await validateInput(viewer, null, null);
if (viewer.loggedIn) {
const [anonymousViewerData] = await Promise.all([
createNewAnonymousCookie({
platformDetails: viewer.platformDetails,
deviceToken: viewer.deviceToken,
}),
deleteCookie(viewer.cookieID),
]);
viewer.setNewCookie(anonymousViewerData);
}
return {
currentUserInfo: {
id: viewer.id,
anonymous: true,
},
};
}
const deleteAccountRequestInputValidator = tShape({
password: t.maybe(tPassword),
});
async function accountDeletionResponder(
viewer: Viewer,
input: any,
): Promise<LogOutResponse> {
const request: DeleteAccountRequest = input;
await validateInput(viewer, deleteAccountRequestInputValidator, request);
const result = await deleteAccount(viewer, request);
invariant(result, 'deleteAccount should return result if handed request');
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),
});
async function accountCreationResponder(
viewer: Viewer,
input: any,
): Promise<RegisterResponse> {
const request: RegisterRequest = input;
await validateInput(viewer, registerRequestInputValidator, request);
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');
}
}
return await createAccount(viewer, request);
}
type ProcessSuccessfulLoginParams = {
+viewer: Viewer,
+input: any,
+userID: string,
+calendarQuery: ?CalendarQuery,
+socialProof?: ?SIWESocialProof,
+signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
};
async function processSuccessfulLogin(
params: ProcessSuccessfulLoginParams,
): Promise<LogInResponse> {
const {
viewer,
input,
userID,
calendarQuery,
socialProof,
signedIdentityKeysBlob,
} = 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, 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 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),
]);
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(t.String),
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),
});
async function logInResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, logInRequestInputValidator, input);
const request: LogInRequest = input;
let identityKeys: ?IdentityKeysBlob;
const { signedIdentityKeysBlob } = 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, 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, 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();
if (identityKeys && signedIdentityKeysBlob) {
const constIdentityKeys = identityKeys;
handleAsyncPromise(
(async () => {
const rustAPI = await getRustAPI();
try {
await rustAPI.loginUserPake(
id,
constIdentityKeys.primaryIdentityPublicKeys.ed25519,
request.password,
signedIdentityKeysBlob,
);
} catch (e) {
if (e.code === 'InvalidArg' && e.message === 'user not found') {
await rustAPI.registerUser(
id,
constIdentityKeys.primaryIdentityPublicKeys.ed25519,
username,
request.password,
signedIdentityKeysBlob,
);
}
}
})(),
);
}
return await processSuccessfulLogin({
viewer,
input,
userID: id,
calendarQuery,
signedIdentityKeysBlob,
});
}
const siweAuthRequestInputValidator = tShape({
signature: t.String,
message: t.String,
calendarQuery: entryQueryInputValidator,
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
watchedIDs: t.list(t.String),
signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator),
});
async function siweAuthResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, siweAuthRequestInputValidator, input);
const request: SIWEAuthRequest = input;
const {
message,
signature,
deviceTokenUpdateRequest,
platformDetails,
signedIdentityKeysBlob,
} = 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. Try to double-write SIWE account info to the Identity service.
const userIDCopy = userID;
if (identityKeys && signedIdentityKeysBlob) {
const identityKeysCopy = identityKeys;
handleAsyncPromise(
(async () => {
const rustAPI = await getRustAPI();
await rustAPI.loginUserWallet(
userIDCopy,
identityKeysCopy.primaryIdentityPublicKeys.ed25519,
siweMessage.toMessage(),
signature,
signedIdentityKeysBlob,
JSON.stringify(socialProof),
);
})(),
);
}
// 10. Complete login with call to `processSuccessfulLogin(...)`.
return await processSuccessfulLogin({
viewer,
input,
userID,
calendarQuery,
socialProof,
signedIdentityKeysBlob,
});
}
const updatePasswordRequestInputValidator = tShape({
code: t.String,
password: tPassword,
watchedIDs: t.list(t.String),
calendarQuery: t.maybe(entryQueryInputValidator),
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
});
async function oldPasswordUpdateResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, updatePasswordRequestInputValidator, input);
const request: UpdatePasswordRequest = input;
if (request.calendarQuery) {
request.calendarQuery = normalizeCalendarQuery(request.calendarQuery);
}
return await updatePassword(viewer, request);
}
const updateUserSettingsInputValidator = tShape({
name: t.irreducible(
userSettingsTypes.DEFAULT_NOTIFICATIONS,
x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS,
),
data: t.enums.of(notificationTypeValues),
});
async function updateUserSettingsResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: UpdateUserSettingsRequest = input;
await validateInput(viewer, updateUserSettingsInputValidator, request);
return await updateUserSettings(viewer, request);
}
const policyAcknowledgmentRequestInputValidator = tShape({
policy: t.maybe(t.enums.of(policies)),
});
async function policyAcknowledgmentResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: PolicyAcknowledgmentRequest = input;
await validateInput(
viewer,
policyAcknowledgmentRequestInputValidator,
request,
);
await viewerAcknowledgmentUpdater(viewer, request.policy);
}
+async function updateUserAvatarResponder(
+ viewer: Viewer,
+ input: any,
+): Promise<void> {
+ const request: UpdateUserAvatarRequest = input;
+ await validateInput(viewer, updateUserAvatarRequestValidator, request);
+}
+
export {
userSubscriptionUpdateResponder,
passwordUpdateResponder,
sendVerificationEmailResponder,
sendPasswordResetEmailResponder,
logOutResponder,
accountDeletionResponder,
accountCreationResponder,
logInResponder,
siweAuthResponder,
oldPasswordUpdateResponder,
updateUserSettingsResponder,
policyAcknowledgmentResponder,
+ updateUserAvatarResponder,
};
diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js
index 8ab8356e4..d40e88378 100644
--- a/lib/types/endpoints.js
+++ b/lib/types/endpoints.js
@@ -1,115 +1,116 @@
// @flow
export type APIRequest = {
endpoint: Endpoint,
input: Object,
};
export type SocketAPIHandler = (request: APIRequest) => Promise<Object>;
export type Endpoint =
| HTTPOnlyEndpoint
| SocketOnlyEndpoint
| HTTPPreferredEndpoint
| SocketPreferredEndpoint;
// Endpoints that can cause session changes should occur over HTTP, since the
// socket code does not currently support changing sessions. In the future they
// could be made to work for native, but cookie changes on web require HTTP
// since websockets aren't able to Set-Cookie. Note that technically any
// endpoint can cause a sessionChange, and in that case the server will close
// the socket with a specific error code, and the client will proceed via HTTP.
const sessionChangingEndpoints = Object.freeze({
LOG_OUT: 'log_out',
DELETE_ACCOUNT: 'delete_account',
CREATE_ACCOUNT: 'create_account',
LOG_IN: 'log_in',
UPDATE_PASSWORD: 'update_password',
POLICY_ACKNOWLEDGMENT: 'policy_acknowledgment',
});
type SessionChangingEndpoint = $Values<typeof sessionChangingEndpoints>;
// We do uploads over HTTP as well. This is because Websockets use TCP, which
// guarantees ordering. That means that if we start an upload, any messages we
// try to send the server after the upload starts will have to wait until the
// upload ends. To avoid blocking other messages we upload using HTTP
// multipart/form-data.
const uploadEndpoints = Object.freeze({
UPLOAD_MULTIMEDIA: 'upload_multimedia',
});
type UploadEndpoint = $Values<typeof uploadEndpoints>;
type HTTPOnlyEndpoint = SessionChangingEndpoint | UploadEndpoint;
const socketOnlyEndpoints = Object.freeze({
UPDATE_ACTIVITY: 'update_activity',
UPDATE_CALENDAR_QUERY: 'update_calendar_query',
});
type SocketOnlyEndpoint = $Values<typeof socketOnlyEndpoints>;
const socketPreferredEndpoints = Object.freeze({
CREATE_ENTRY: 'create_entry',
CREATE_ERROR_REPORT: 'create_error_report',
CREATE_MESSAGE_REPORT: 'create_message_report',
CREATE_MULTIMEDIA_MESSAGE: 'create_multimedia_message',
CREATE_REACTION_MESSAGE: 'create_reaction_message',
CREATE_TEXT_MESSAGE: 'create_text_message',
CREATE_THREAD: 'create_thread',
DELETE_ENTRY: 'delete_entry',
DELETE_THREAD: 'delete_thread',
DELETE_UPLOAD: 'delete_upload',
FETCH_ENTRIES: 'fetch_entries',
FETCH_ENTRY_REVISIONS: 'fetch_entry_revisions',
FETCH_ERROR_REPORT_INFOS: 'fetch_error_report_infos',
FETCH_MESSAGES: 'fetch_messages',
FETCH_THREAD_MEDIA: 'fetch_thread_media',
GET_SESSION_PUBLIC_KEYS: 'get_session_public_keys',
JOIN_THREAD: 'join_thread',
LEAVE_THREAD: 'leave_thread',
REMOVE_MEMBERS: 'remove_members',
REQUEST_ACCESS: 'request_access',
RESTORE_ENTRY: 'restore_entry',
SEARCH_USERS: 'search_users',
SEND_PASSWORD_RESET_EMAIL: 'send_password_reset_email',
SEND_VERIFICATION_EMAIL: 'send_verification_email',
SET_THREAD_UNREAD_STATUS: 'set_thread_unread_status',
UPDATE_ACCOUNT: 'update_account',
UPDATE_USER_SETTINGS: 'update_user_settings',
UPDATE_DEVICE_TOKEN: 'update_device_token',
UPDATE_ENTRY: 'update_entry',
UPDATE_RELATIONSHIPS: 'update_relationships',
UPDATE_ROLE: 'update_role',
UPDATE_THREAD: 'update_thread',
UPDATE_USER_SUBSCRIPTION: 'update_user_subscription',
VERIFY_CODE: 'verify_code',
SIWE_NONCE: 'siwe_nonce',
SIWE_AUTH: 'siwe_auth',
+ UPDATE_USER_AVATAR: 'update_user_avatar',
});
type SocketPreferredEndpoint = $Values<typeof socketPreferredEndpoints>;
const httpPreferredEndpoints = Object.freeze({
CREATE_REPORT: 'create_report',
CREATE_REPORTS: 'create_reports',
});
type HTTPPreferredEndpoint = $Values<typeof httpPreferredEndpoints>;
const socketPreferredEndpointSet = new Set([
...Object.values(socketOnlyEndpoints),
...Object.values(socketPreferredEndpoints),
]);
export function endpointIsSocketPreferred(endpoint: Endpoint): boolean {
return socketPreferredEndpointSet.has(endpoint);
}
const socketSafeEndpointSet = new Set([
...Object.values(socketOnlyEndpoints),
...Object.values(socketPreferredEndpoints),
...Object.values(httpPreferredEndpoints),
]);
export function endpointIsSocketSafe(endpoint: Endpoint): boolean {
return socketSafeEndpointSet.has(endpoint);
}
const socketOnlyEndpointSet = new Set(Object.values(socketOnlyEndpoints));
export function endpointIsSocketOnly(endpoint: Endpoint): boolean {
return socketOnlyEndpointSet.has(endpoint);
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:59 AM (8 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690185
Default Alt Text
(33 KB)

Event Timeline