diff --git a/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs b/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs --- a/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs @@ -49,6 +49,7 @@ device_key_upload: Some(device_key_upload), farcaster_id: None, initial_device_list: "".to_string(), + farcaster_dc_token: None, }; // Finish OPAQUE registration and send final registration request diff --git a/services/commtest/src/identity/device.rs b/services/commtest/src/identity/device.rs --- a/services/commtest/src/identity/device.rs +++ b/services/commtest/src/identity/device.rs @@ -93,6 +93,7 @@ }), farcaster_id: None, initial_device_list: initial_device_list.unwrap_or_default(), + farcaster_dc_token: None, }; let mut identity_client = get_unauthenticated_client( diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -77,6 +77,7 @@ pub user_id: Option, pub farcaster_id: Option, pub initial_device_list: Option, + pub farcaster_dcs_token: Option, } #[derive(Clone, Serialize, Deserialize)] @@ -157,6 +158,7 @@ None, message.username.clone(), message.farcaster_id.clone(), + message.farcaster_dc_token.clone(), )?; self .check_device_id_taken( @@ -215,6 +217,7 @@ Some(user_id), original_username, None, + None, )?; self .check_device_id_taken( @@ -603,6 +606,7 @@ platform_metadata, login_time, message.farcaster_id, + message.farcaster_dcs_token, None, ) .await?; @@ -688,6 +692,7 @@ platform_metadata, login_time, message.farcaster_id, + message.farcaster_dcs_token, initial_device_list, ) .await?; @@ -1439,6 +1444,7 @@ user_id: Option, username: String, farcaster_id: Option, + farcaster_dcs_token: Option, ) -> Result { Ok(UserRegistrationInfo { username, @@ -1448,6 +1454,7 @@ user_id, farcaster_id, initial_device_list: message.get_and_verify_initial_device_list()?, + farcaster_dcs_token, }) } diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -18,6 +18,8 @@ pub const USERS_TABLE_DEVICELIST_TIMESTAMP_ATTRIBUTE_NAME: &str = "deviceListTimestamp"; pub const USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME: &str = "farcasterID"; +pub const USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME: &str = + "farcasterDCsToken"; pub const USERS_TABLE_USERNAME_LOWER_ATTRIBUTE_NAME: &str = "usernameLower"; pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index"; pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index"; @@ -205,6 +207,7 @@ pub const NONCE_EXPIRED: &str = "nonce_expired"; pub const FID_TAKEN: &str = "fid_taken"; pub const CANNOT_LINK_FID: &str = "cannot_link_fid"; + pub const CANNOT_LINK_FARCASTER_DCS: &str = "cannot_link_farcaster_dcs"; pub const INVALID_PLATFORM_METADATA: &str = "invalid_platform_metadata"; pub const MISSING_CREDENTIALS: &str = "missing_credentials"; pub const BAD_CREDENTIALS: &str = "bad_credentials"; diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs --- a/services/identity/src/database.rs +++ b/services/identity/src/database.rs @@ -48,6 +48,7 @@ RESERVED_USERNAMES_TABLE_USERNAME_LOWER_INDEX, RESERVED_USERNAMES_TABLE_USER_ID_ATTRIBUTE, USERS_TABLE, USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME, + USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME, USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME, USERS_TABLE_PARTITION_KEY, USERS_TABLE_REGISTRATION_ATTRIBUTE, USERS_TABLE_SOCIAL_PROOF_ATTRIBUTE_NAME, USERS_TABLE_USERNAME_ATTRIBUTE, USERS_TABLE_USERNAME_LOWER_ATTRIBUTE_NAME, @@ -168,6 +169,7 @@ None, registration_state.user_id, registration_state.farcaster_id, + registration_state.farcaster_dcs_token, ) .await?; @@ -218,6 +220,7 @@ platform_metadata: PlatformMetadata, access_token_creation_time: DateTime, farcaster_id: Option, + farcaster_dcs_token: Option, initial_device_list: Option, ) -> Result { let wallet_identity = EthereumIdentity { @@ -230,6 +233,7 @@ Some(wallet_identity), user_id, farcaster_id, + farcaster_dcs_token, ) .await?; @@ -276,6 +280,7 @@ wallet_identity: Option, user_id: Option, farcaster_id: Option, + farcaster_dcs_token: Option, ) -> Result { let user_id = user_id.unwrap_or_else(generate_uuid); let mut user = HashMap::from([( @@ -317,6 +322,13 @@ ); } + if let Some(token) = farcaster_dcs_token { + user.insert( + USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(token), + ); + } + let put_user = Put::builder() .table_name(USERS_TABLE) .set_item(Some(user)) diff --git a/services/identity/src/database/farcaster.rs b/services/identity/src/database/farcaster.rs --- a/services/identity/src/database/farcaster.rs +++ b/services/identity/src/database/farcaster.rs @@ -9,6 +9,7 @@ use crate::constants::error_types; use crate::constants::USERS_TABLE; +use crate::constants::USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME; use crate::constants::USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME; use crate::constants::USERS_TABLE_FARCASTER_ID_INDEX; use crate::constants::USERS_TABLE_PARTITION_KEY; @@ -107,12 +108,46 @@ Ok(()) } - pub async fn remove_farcaster_id( + pub async fn add_farcaster_dcs_token( &self, user_id: String, + farcaster_dcs_token: String, ) -> Result<(), Error> { - let update_expression = - format!("REMOVE {}", USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME); + let update_expression = format!( + "SET {0} = :val", + USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME, + ); + + self + .client + .update_item() + .table_name(USERS_TABLE) + .key(USERS_TABLE_PARTITION_KEY, AttributeValue::S(user_id)) + .update_expression(update_expression) + .expression_attribute_values( + ":val", + AttributeValue::S(farcaster_dcs_token.clone()), + ) + .return_values(ReturnValue::UpdatedNew) + .send() + .await + .map_err(|e| { + error!( + errorType = error_types::FARCASTER_DB_LOG, + "DDB client failed to add Farcaster DCs token: {:?}", e + ); + Error::AwsSdk(e.into()) + })?; + + Ok(()) + } + + pub async fn unlink_farcaster(&self, user_id: String) -> Result<(), Error> { + let update_expression = format!( + "REMOVE {}, {}", + USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME, + USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME + ); self .client diff --git a/services/identity/src/ddb_utils.rs b/services/identity/src/ddb_utils.rs --- a/services/identity/src/ddb_utils.rs +++ b/services/identity/src/ddb_utils.rs @@ -8,6 +8,7 @@ use crate::{ constants::{ + USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME, USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME, USERS_TABLE_SOCIAL_PROOF_ATTRIBUTE_NAME, USERS_TABLE_USERNAME_ATTRIBUTE, USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE, @@ -166,6 +167,7 @@ pub struct DBIdentity { pub identifier: Identifier, pub farcaster_id: Option, + pub farcaster_dcs_token: Option, } pub enum Identifier { @@ -195,12 +197,16 @@ let farcaster_id = value.take_attr(USERS_TABLE_FARCASTER_ID_ATTRIBUTE_NAME)?; + let farcaster_dcs_token = + value.take_attr(USERS_TABLE_FARCASTER_DCS_TOKEN_ATTRIBUTE_NAME)?; + let username_result = value.take_attr(USERS_TABLE_USERNAME_ATTRIBUTE); if let Ok(username) = username_result { return Ok(DBIdentity { identifier: Identifier::Username(username), farcaster_id, + farcaster_dcs_token, }); } @@ -218,6 +224,7 @@ social_proof, }), farcaster_id, + farcaster_dcs_token, }) } else { Err(Self::Error::MalformedItem) diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -26,11 +26,11 @@ DeletePasswordUserFinishRequest, DeletePasswordUserStartRequest, DeletePasswordUserStartResponse, GetDeviceListRequest, GetDeviceListResponse, InboundKeyInfo, InboundKeysForUserRequest, InboundKeysForUserResponse, - KeyserverKeysResponse, LinkFarcasterAccountRequest, OutboundKeyInfo, - OutboundKeysForUserRequest, OutboundKeysForUserResponse, - PeersDeviceListsRequest, PeersDeviceListsResponse, - PrimaryDeviceLogoutRequest, PrivilegedDeleteUsersRequest, - PrivilegedResetUserPasswordFinishRequest, + KeyserverKeysResponse, LinkFarcasterAccountRequest, + LinkFarcasterDCsAccountRequest, OutboundKeyInfo, OutboundKeysForUserRequest, + OutboundKeysForUserResponse, PeersDeviceListsRequest, + PeersDeviceListsResponse, PrimaryDeviceLogoutRequest, + PrivilegedDeleteUsersRequest, PrivilegedResetUserPasswordFinishRequest, PrivilegedResetUserPasswordStartRequest, PrivilegedResetUserPasswordStartResponse, RefreshUserPrekeysRequest, UpdateDeviceListRequest, UpdateUserPasswordFinishRequest, @@ -1124,6 +1124,43 @@ Ok(Response::new(response)) } + #[tracing::instrument(skip_all)] + async fn link_farcaster_d_cs_account( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let (user_id, _) = get_user_and_device_id(&request)?; + let message = request.into_inner(); + + info!( + user_id = redact_sensitive_data(&user_id), + "Attempting to link Farcaster DCs account" + ); + + let get_farcaster_users_response = self + .db_client + .get_farcaster_users(vec![message.farcaster_id.clone()]) + .await?; + + if get_farcaster_users_response.len() != 1 { + error!( + errorType = error_types::GRPC_SERVICES_LOG, + "Farcaster ID missing" + ); + return Err(Status::failed_precondition( + tonic_status_messages::CANNOT_LINK_FARCASTER_DCS, + )); + } + + self + .db_client + .add_farcaster_dcs_token(user_id, message.farcaster_dc_token) + .await?; + + let response = Empty {}; + Ok(Response::new(response)) + } + #[tracing::instrument(skip_all)] async fn unlink_farcaster_account( &self, @@ -1136,7 +1173,7 @@ "Attempting to unlink Farcaster account." ); - self.db_client.remove_farcaster_id(user_id).await?; + self.db_client.unlink_farcaster(user_id).await?; let response = Empty {}; Ok(Response::new(response)) diff --git a/shared/protos/identity_auth.proto b/shared/protos/identity_auth.proto --- a/shared/protos/identity_auth.proto +++ b/shared/protos/identity_auth.proto @@ -97,6 +97,10 @@ // Called by an existing user to unlink their Farcaster account rpc UnlinkFarcasterAccount(identity.unauth.Empty) returns (identity.unauth.Empty) {} + // Called by an existing user with a linked Farcaster account to link + // their DCs + rpc LinkFarcasterDCsAccount(LinkFarcasterDCsAccountRequest) returns + (identity.unauth.Empty) {} /* Miscellaneous actions */ @@ -362,6 +366,11 @@ string farcaster_id = 1; } +message LinkFarcasterDCsAccountRequest { + string farcaster_dc_token = 1; + string farcaster_id = 2; +} + // FindUserIdentities message UserIdentitiesRequest { diff --git a/shared/protos/identity_unauth.proto b/shared/protos/identity_unauth.proto --- a/shared/protos/identity_unauth.proto +++ b/shared/protos/identity_unauth.proto @@ -133,6 +133,7 @@ // } // It's an empty string for older clients which don't sign device lists yet. string initial_device_list = 5; + optional string farcaster_dc_token = 6; } message ReservedRegistrationStartRequest { @@ -231,6 +232,7 @@ // It's an empty string when used outside `RegisterWalletUser` RPC // and for older clients which don't sign device lists yet. string initial_device_list = 5; + optional string farcaster_dcs_token = 6; } // Primary backup restore