diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 29ec2d8..bf51c59 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -20,6 +20,7 @@ paths: - password - ethereum - eip4361 + example: eip4361 username: description: User name (required if grant type is "password"). type: string @@ -75,21 +76,23 @@ paths: description: The desired username for the account. type: string password: - description: The password to be used for login. + description: The password to be used for login. Either password or EIP-4361 message must be provided. type: string + example: null message: description: EIP-4361 message type: string + example: "example.com wants you to sign in with your Ethereum account:" signature: description: EIP-4361 signature (required if message is present) type: string + example: 0x905... invite_code: description: Invite code type: string example: 9b288bfa7dc75fff53e98aa4d76e77d5 required: - username - - password responses: 201: description: Successful operation diff --git a/migrations/V0021__user_account__nullable_password_hash.sql b/migrations/V0021__user_account__nullable_password_hash.sql new file mode 100644 index 0000000..0016218 --- /dev/null +++ b/migrations/V0021__user_account__nullable_password_hash.sql @@ -0,0 +1 @@ +ALTER TABLE user_account ALTER COLUMN password_hash DROP NOT NULL; diff --git a/migrations/schema.sql b/migrations/schema.sql index e72d130..c58f93a 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -23,7 +23,7 @@ CREATE TABLE user_invite_code ( CREATE TABLE user_account ( id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE, wallet_address VARCHAR(100) UNIQUE, - password_hash VARCHAR(200) NOT NULL, + password_hash VARCHAR(200), private_key TEXT NOT NULL, invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index 3e0767a..e67d880 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -104,7 +104,7 @@ impl Account { #[derive(Deserialize)] pub struct AccountCreateData { pub username: String, - pub password: String, + pub password: Option, pub message: Option, pub signature: Option, @@ -117,6 +117,9 @@ impl AccountCreateData { pub fn clean(&self) -> Result<(), ValidationError> { validate_username(&self.username)?; validate_local_username(&self.username)?; + if self.password.is_none() && self.message.is_none() { + return Err(ValidationError("password or EIP-4361 message is required")); + }; Ok(()) } } @@ -212,6 +215,19 @@ mod tests { const INSTANCE_URL: &str = "https://example.com"; + #[test] + fn test_validate_account_create_data() { + let account_data = AccountCreateData { + username: "test".to_string(), + password: None, + message: None, + signature: Some("test".to_string()), + invite_code: None, + }; + let error = account_data.clean().unwrap_err(); + assert_eq!(error.to_string(), "password or EIP-4361 message is required"); + } + #[test] fn test_create_account_from_profile() { let profile = DbActorProfile { diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 613e3d0..89e1137 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -72,6 +72,14 @@ pub async fn create_account( return Err(ValidationError("invalid invite code").into()); } } + + let password_hash = if let Some(password) = account_data.password.as_ref() { + let password_hash = hash_password(password) + .map_err(|_| HttpError::InternalError)?; + Some(password_hash) + } else { + None + }; let wallet_address = if let Some(message) = account_data.message.as_ref() { let signature = account_data.signature.as_ref() .ok_or(ValidationError("signature is required"))?; @@ -85,8 +93,10 @@ pub async fn create_account( } else { None }; + assert!(password_hash.is_some() || wallet_address.is_some()); + if let Some(blockchain_config) = config.blockchain.as_ref() { - // Wallet address is required only if blockchain integration is enabled + // Wallet address is required if blockchain integration is enabled let wallet_address = wallet_address.as_ref() .ok_or(ValidationError("wallet address is required"))?; let is_allowed = is_allowed_user(blockchain_config, wallet_address).await @@ -95,9 +105,7 @@ pub async fn create_account( return Err(ValidationError("not allowed to sign up").into()); } } - // Hash password and generate private key - let password_hash = hash_password(&account_data.password) - .map_err(|_| HttpError::InternalError)?; + // Generate RSA private key for actor let private_key = match web::block(generate_private_key).await { Ok(private_key) => private_key, Err(_) => return Err(HttpError::InternalError), diff --git a/src/mastodon_api/oauth/views.rs b/src/mastodon_api/oauth/views.rs index 30f1a27..f494449 100644 --- a/src/mastodon_api/oauth/views.rs +++ b/src/mastodon_api/oauth/views.rs @@ -58,9 +58,11 @@ async fn token_view( if request_data.grant_type == "password" || request_data.grant_type == "ethereum" { let password = request_data.password.as_ref() .ok_or(ValidationError("password is required"))?; + let password_hash = user.password_hash.as_ref() + .ok_or(ValidationError("password auth is disabled"))?; let password_correct = verify_password( - &user.password_hash, - &password, + password_hash, + password, ).map_err(|_| HttpError::InternalError)?; if !password_correct { return Err(ValidationError("incorrect password").into()); diff --git a/src/models/users/types.rs b/src/models/users/types.rs index b3471eb..a5b2f5c 100644 --- a/src/models/users/types.rs +++ b/src/models/users/types.rs @@ -11,7 +11,7 @@ use crate::models::profiles::types::DbActorProfile; pub struct DbUser { pub id: Uuid, pub wallet_address: Option, - pub password_hash: String, + pub password_hash: Option, pub private_key: String, pub invite_code: Option, pub created_at: DateTime, @@ -23,7 +23,7 @@ pub struct DbUser { pub struct User { pub id: Uuid, pub wallet_address: Option, - pub password_hash: String, + pub password_hash: Option, pub private_key: String, pub profile: DbActorProfile, } @@ -47,7 +47,7 @@ impl User { #[cfg_attr(test, derive(Default))] pub struct UserCreateData { pub username: String, - pub password_hash: String, + pub password_hash: Option, pub private_key_pem: String, pub wallet_address: Option, pub invite_code: Option,