diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0b0201c..7596db6 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -18,10 +18,19 @@ paths: type: string enum: - password + - ethereum username: + description: User name (required if grant type is "password"). type: string + wallet_address: + description: Ethereum wallet address (required if grant type is "ethereum"). + type: string + example: null password: type: string + required: + - grant_type + - password responses: 200: description: Successful operation @@ -59,7 +68,7 @@ paths: description: The password to be used for login. type: string wallet_address: - description: Ethereum wallet address + description: Ethereum wallet address. type: string example: '0xd8da6bf...' invite_code: @@ -69,7 +78,6 @@ paths: required: - username - password - - wallet_address responses: 201: description: Successful operation @@ -193,7 +201,7 @@ paths: type: string example: '6a5cb313907cd3...' 403: - description: Post does not belong to user or is not public + description: Post does not belong to user or is not public or user's wallet address is not known 404: description: Post not found 418: @@ -295,6 +303,10 @@ components: description: The Webfinger account URI. Equal to username for local actors, or username@domain for remote actors. type: string example: user@example.com + wallet_address: + description: Ethereum wallet address. + type: string + example: '0xd8da6bf...' Status: type: object properties: diff --git a/migrations/V0016__user_account__nullable_wallet_address.sql b/migrations/V0016__user_account__nullable_wallet_address.sql new file mode 100644 index 0000000..27dd4d9 --- /dev/null +++ b/migrations/V0016__user_account__nullable_wallet_address.sql @@ -0,0 +1 @@ +ALTER TABLE user_account ALTER COLUMN wallet_address DROP NOT NULL; diff --git a/migrations/schema.sql b/migrations/schema.sql index 0d35c7b..ae22c40 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -22,7 +22,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 NOT NULL, + wallet_address VARCHAR(100) UNIQUE, password_hash VARCHAR(200) NOT NULL, private_key TEXT NOT NULL, invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL, diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index db60a8f..78fd678 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -87,7 +87,7 @@ impl Account { }; let mut account = Self::from_profile(user.profile, instance_url); account.source = Some(source); - account.wallet_address = Some(user.wallet_address); + account.wallet_address = user.wallet_address; account } } @@ -98,7 +98,7 @@ pub struct AccountCreateData { username: String, password: String, - wallet_address: String, + wallet_address: Option, invite_code: Option, } @@ -204,7 +204,7 @@ mod tests { ..Default::default() }; let user = User { - wallet_address: wallet_address.to_string(), + wallet_address: Some(wallet_address.to_string()), profile, ..Default::default() }; diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 0144046..b0d9d2d 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -58,7 +58,10 @@ pub async fn create_account( } } if config.ethereum_contract.is_some() { - let is_allowed = is_allowed_user(&config, &user_data.wallet_address).await + // Wallet address is required only if ethereum integration is enabled + let wallet_address = user_data.wallet_address.as_ref() + .ok_or(ValidationError("wallet address is required"))?; + let is_allowed = is_allowed_user(&config, wallet_address).await .map_err(|_| HttpError::InternalError)?; if !is_allowed { return Err(ValidationError("not allowed to sign up").into()); diff --git a/src/mastodon_api/oauth/types.rs b/src/mastodon_api/oauth/types.rs index 9bed4bd..f5bb416 100644 --- a/src/mastodon_api/oauth/types.rs +++ b/src/mastodon_api/oauth/types.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct TokenRequest { pub grant_type: String, - pub username: String, // wallet address + pub username: Option, + pub wallet_address: Option, pub password: String, } diff --git a/src/mastodon_api/oauth/views.rs b/src/mastodon_api/oauth/views.rs index 3b91a42..ca96eb5 100644 --- a/src/mastodon_api/oauth/views.rs +++ b/src/mastodon_api/oauth/views.rs @@ -4,7 +4,10 @@ use chrono::{Duration, Utc}; use crate::database::{Pool, get_database_client}; use crate::errors::{HttpError, ValidationError}; use crate::models::oauth::queries::save_oauth_token; -use crate::models::users::queries::get_user_by_wallet_address; +use crate::models::users::queries::{ + get_user_by_name, + get_user_by_wallet_address, +}; use crate::utils::crypto::verify_password; use super::types::{TokenRequest, TokenResponse}; use super::utils::generate_access_token; @@ -18,14 +21,22 @@ async fn token_view( db_pool: web::Data, request_data: web::Json, ) -> Result { - if &request_data.grant_type != "password" { - return Err(ValidationError("unsupported grant type").into()); - } let db_client = &**get_database_client(&db_pool).await?; - let user = get_user_by_wallet_address( - db_client, - &request_data.username, - ).await?; + let user = match request_data.grant_type.as_str() { + "password" => { + let username = request_data.username.as_ref() + .ok_or(ValidationError("username is required"))?; + get_user_by_name(db_client, username).await? + }, + "ethereum" => { + let wallet_address = request_data.wallet_address.as_ref() + .ok_or(ValidationError("wallet address is required"))?; + get_user_by_wallet_address(db_client, wallet_address).await? + }, + _ => { + return Err(ValidationError("unsupported grant type").into()); + }, + }; let password_correct = verify_password( &user.password_hash, &request_data.password, diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index d8104bf..5b06a1f 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -477,6 +477,8 @@ async fn get_signature( let current_user = get_current_user(db_client, auth.token()).await?; let contract_config = config.ethereum_contract.as_ref() .ok_or(HttpError::NotSupported)?; + let wallet_address = current_user.wallet_address + .ok_or(HttpError::PermissionError)?; let post = get_post_by_id(db_client, &status_id).await?; if post.author.id != current_user.id || !post.is_public() { // Users can only tokenize their own public posts @@ -488,7 +490,7 @@ async fn get_signature( let token_uri = get_ipfs_url(&ipfs_cid); let signature = create_mint_signature( contract_config, - ¤t_user.wallet_address, + &wallet_address, &token_uri, ).map_err(|_| HttpError::InternalError)?; Ok(HttpResponse::Ok().json(signature)) diff --git a/src/models/users/types.rs b/src/models/users/types.rs index 39d8693..50e8617 100644 --- a/src/models/users/types.rs +++ b/src/models/users/types.rs @@ -11,7 +11,7 @@ use crate::models::profiles::types::DbActorProfile; #[postgres(name = "user_account")] pub struct DbUser { pub id: Uuid, - pub wallet_address: String, + pub wallet_address: Option, pub password_hash: String, pub private_key: String, pub invite_code: Option, @@ -23,7 +23,7 @@ pub struct DbUser { #[cfg_attr(test, derive(Default))] pub struct User { pub id: Uuid, - pub wallet_address: String, + pub wallet_address: Option, pub password_hash: String, pub private_key: String, pub profile: DbActorProfile, @@ -49,7 +49,7 @@ impl User { pub struct UserCreateData { pub username: String, pub password: String, - pub wallet_address: String, + pub wallet_address: Option, pub invite_code: Option, }