diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -178,6 +178,8 @@ updatePasswordRequestInputValidator, updateUserAvatarResponderValidator, updateUserSettingsInputValidator, + claimUsernameResponder, + claimUsernameResponseValidator, } from './responders/user-responders.js'; import { codeVerificationResponder, @@ -533,6 +535,12 @@ logInResponseValidator, [], ), + claim_username: createJSONResponder( + claimUsernameResponder, + ignoredArgumentValidator, + claimUsernameResponseValidator, + [], + ), update_user_avatar: createJSONResponder( updateUserAvatarResponder, updateUserAvatarRequestValidator, diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -23,6 +23,7 @@ UpdatePasswordRequest, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, + ClaimUsernameResponse, } from 'lib/types/account-types.js'; import { userSettingsTypes, @@ -36,6 +37,7 @@ type UpdateUserAvatarRequest, } from 'lib/types/avatar-types.js'; import type { + ReservedUsernameMessage, IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; @@ -113,6 +115,7 @@ fetchKnownUserInfos, fetchLoggedInUserInfo, fetchUserIDForEthereumAddress, + fetchUsername, } from '../fetchers/user-fetchers.js'; import { createNewAnonymousCookie, @@ -129,6 +132,7 @@ updateUserSettings, updateUserAvatar, } from '../updaters/account-updaters.js'; +import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; import { getOlmUtility } from '../utils/olm-utils.js'; @@ -730,6 +734,36 @@ return await updateUserAvatar(viewer, request); } +export const claimUsernameResponseValidator: TInterface = + tShape({ + message: t.String, + signature: t.String, + }); + +async function claimUsernameResponder( + viewer: Viewer, +): Promise { + const [username, accountInfo] = await Promise.all([ + fetchUsername(viewer.userID), + fetchOlmAccount('content'), + ]); + + if (!username) { + throw new ServerError('invalid_credentials'); + } + + const issuedAt = new Date().toISOString(); + const reservedUsernameMessage: ReservedUsernameMessage = { + statement: 'This user is the owner of the following username', + payload: username, + issuedAt, + }; + const message = JSON.stringify(reservedUsernameMessage); + const signature = accountInfo.account.sign(message); + + return { message, signature }; +} + export { userSubscriptionUpdateResponder, passwordUpdateResponder, @@ -744,4 +778,5 @@ updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, + claimUsernameResponder, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -9,6 +9,7 @@ RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, + ClaimUsernameResponse, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, @@ -62,6 +63,27 @@ return { currentUserInfo, preRequestUserState }; }; +const claimUsername = + ( + callServerEndpoint: CallServerEndpoint, + ): (() => Promise) => + async () => { + let response = null; + try { + response = await Promise.race([ + callServerEndpoint('claim_username', {}), + (async () => { + await sleep(500); + throw new Error('claim_username took more than 500ms'); + })(), + ]); + } catch (error) { + console.error('Error while claiming username:', error); + return null; + } + return response; + }; + const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', @@ -325,6 +347,7 @@ export { changeUserPasswordActionTypes, changeUserPassword, + claimUsername, deleteAccount, deleteAccountActionTypes, getSessionPublicKeys, diff --git a/lib/types/account-types.js b/lib/types/account-types.js --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -203,3 +203,8 @@ tShape({ default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), }); + +export type ClaimUsernameResponse = { + +message: string, + +signature: string, +}; diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -59,6 +59,11 @@ +statement: 'Remove the following username from reserved list', +payload: string, +issuedAt: string, + } + | { + +statement: 'This user is the owner of the following username', + +payload: string, + +issuedAt: string, }; export const olmEncryptedMessageTypes = Object.freeze({ diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -93,6 +93,7 @@ VERIFY_INVITE_LINK: 'verify_invite_link', SIWE_NONCE: 'siwe_nonce', SIWE_AUTH: 'siwe_auth', + CLAIM_USERNAME: 'claim_username', UPDATE_USER_AVATAR: 'update_user_avatar', UPLOAD_MEDIA_METADATA: 'upload_media_metadata', SEARCH_MESSAGES: 'search_messages', diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -6,6 +6,7 @@ LogInResult, RegisterResult, DefaultNotificationPayload, + ClaimUsernameResponse, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, @@ -184,6 +185,22 @@ +payload: LogOutResult, +loadingInfo: LoadingInfo, } + | { + +type: 'CLAIM_USERNAME_STARTED', + +payload?: void, + +loadingInfo: LoadingInfo, + } + | { + +type: 'CLAIM_USERNAME_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, + } + | { + +type: 'CLAIM_USERNAME_SUCCESS', + +payload: ClaimUsernameResponse, + +loadingInfo: LoadingInfo, + } | { +type: 'DELETE_ACCOUNT_STARTED', +payload?: void,