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 @@ -956,6 +956,29 @@ await callSingleKeyserverEndpoint('update_account', passwordUpdate); }; +const changeIdentityUserPasswordActionTypes = Object.freeze({ + started: 'CHANGE_IDENTITY_USER_PASSWORD_STARTED', + success: 'CHANGE_IDENTITY_USER_PASSWORD_SUCCESS', + failed: 'CHANGE_IDENTITY_USER_PASSWORD_FAILED', +}); + +function useChangeIdentityUserPassword(): ( + oldPassword: string, + newPassword: string, +) => Promise { + const client = React.useContext(IdentityClientContext); + const identityClient = client?.identityClient; + return React.useCallback( + (oldPassword, newPassword) => { + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + return identityClient.changePassword(oldPassword, newPassword); + }, + [identityClient], + ); +} + const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', @@ -1158,6 +1181,8 @@ export { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, + changeIdentityUserPasswordActionTypes, + useChangeIdentityUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -196,6 +196,7 @@ +unlinkFarcasterAccount: () => Promise; +findUserIdentities: (userIDs: $ReadOnlyArray) => Promise; +versionSupported: () => Promise; + +changePassword: (oldPassword: string, newPassword: string) => Promise; } export type IdentityServiceAuthLayer = { diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -699,6 +699,20 @@ versionSupported: () => { return commRustModule.versionSupported(); }, + changePassword: async (oldPassword: string, newPassword: string) => { + const { + deviceID, + userID, + accessToken: token, + } = await getAuthMetadata(); + return await commRustModule.updatePassword( + userID, + deviceID, + token, + oldPassword, + newPassword, + ); + }, }), [getAuthMetadata], ); diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,6 +1,6 @@ // @flow -import { Login } from '@commapp/opaque-ke-wasm'; +import { Login, Registration } from '@commapp/opaque-ke-wasm'; import identityServiceConfig from 'lib/facts/identity-service.js'; import type { @@ -680,6 +680,73 @@ throw e; } }; + + changePassword: (oldPassword: string, newPassword: string) => Promise = + async (oldPassword, newPassword) => { + const client = this.authClient; + if (!client) { + throw new Error('Identity service client is not initialized'); + } + await initOpaque(this.overridedOpaqueFilepath); + + const opaqueLogin = new Login(); + const loginStartRequestBytes = opaqueLogin.start(oldPassword); + + const opaqueRegistration = new Registration(); + const registrationStartRequestBytes = + opaqueRegistration.start(newPassword); + + const updatePasswordStartRequest = + new IdentityAuthStructs.UpdateUserPasswordStartRequest(); + updatePasswordStartRequest.setOpaqueLoginRequest(loginStartRequestBytes); + updatePasswordStartRequest.setOpaqueRegistrationRequest( + registrationStartRequestBytes, + ); + + let updatePasswordStartResponse; + try { + updatePasswordStartResponse = await client.updateUserPasswordStart( + updatePasswordStartRequest, + ); + } catch (e) { + console.log('Error calling updateUserPasswordStart:', e); + throw new Error( + `updateUserPasswordStart RPC failed: ${ + getMessageForException(e) ?? 'unknown' + }`, + ); + } + + const loginFinishRequestBytes = opaqueLogin.finish( + updatePasswordStartResponse.getOpaqueLoginResponse_asU8(), + ); + + const registrationFinishRequestBytes = opaqueRegistration.finish( + newPassword, + updatePasswordStartResponse.getOpaqueRegistrationResponse_asU8(), + ); + + const updatePasswordFinishRequest = + new IdentityAuthStructs.UpdateUserPasswordFinishRequest(); + updatePasswordFinishRequest.setSessionId( + updatePasswordStartResponse.getSessionId(), + ); + updatePasswordFinishRequest.setOpaqueLoginUpload(loginFinishRequestBytes); + updatePasswordFinishRequest.setOpaqueRegistrationUpload( + registrationFinishRequestBytes, + ); + + try { + await client.updateUserPasswordFinish(updatePasswordFinishRequest); + } catch (e) { + console.log('Error calling updateUserPasswordFinish:', e); + throw new Error( + `updateUserPasswordFinish RPC failed: ${ + getMessageForException(e) ?? 'unknown' + }`, + ); + } + }; } function authNewDeviceKeyUpload( diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -140,6 +140,7 @@ unlinkFarcasterAccount: proxyMethodToWorker('unlinkFarcasterAccount'), findUserIdentities: proxyMethodToWorker('findUserIdentities'), versionSupported: proxyMethodToWorker('versionSupported'), + changePassword: proxyMethodToWorker('changePassword'), }; }, [proxyMethodToWorker]);