Allow passwordless registration
This commit is contained in:
parent
6ddfb5b52d
commit
227e3d3729
7 changed files with 43 additions and 13 deletions
|
@ -20,6 +20,7 @@ paths:
|
||||||
- password
|
- password
|
||||||
- ethereum
|
- ethereum
|
||||||
- eip4361
|
- eip4361
|
||||||
|
example: eip4361
|
||||||
username:
|
username:
|
||||||
description: User name (required if grant type is "password").
|
description: User name (required if grant type is "password").
|
||||||
type: string
|
type: string
|
||||||
|
@ -75,21 +76,23 @@ paths:
|
||||||
description: The desired username for the account.
|
description: The desired username for the account.
|
||||||
type: string
|
type: string
|
||||||
password:
|
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
|
type: string
|
||||||
|
example: null
|
||||||
message:
|
message:
|
||||||
description: EIP-4361 message
|
description: EIP-4361 message
|
||||||
type: string
|
type: string
|
||||||
|
example: "example.com wants you to sign in with your Ethereum account:"
|
||||||
signature:
|
signature:
|
||||||
description: EIP-4361 signature (required if message is present)
|
description: EIP-4361 signature (required if message is present)
|
||||||
type: string
|
type: string
|
||||||
|
example: 0x905...
|
||||||
invite_code:
|
invite_code:
|
||||||
description: Invite code
|
description: Invite code
|
||||||
type: string
|
type: string
|
||||||
example: 9b288bfa7dc75fff53e98aa4d76e77d5
|
example: 9b288bfa7dc75fff53e98aa4d76e77d5
|
||||||
required:
|
required:
|
||||||
- username
|
- username
|
||||||
- password
|
|
||||||
responses:
|
responses:
|
||||||
201:
|
201:
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE user_account ALTER COLUMN password_hash DROP NOT NULL;
|
|
@ -23,7 +23,7 @@ CREATE TABLE user_invite_code (
|
||||||
CREATE TABLE user_account (
|
CREATE TABLE user_account (
|
||||||
id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE,
|
id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||||
wallet_address VARCHAR(100) UNIQUE,
|
wallet_address VARCHAR(100) UNIQUE,
|
||||||
password_hash VARCHAR(200) NOT NULL,
|
password_hash VARCHAR(200),
|
||||||
private_key TEXT NOT NULL,
|
private_key TEXT NOT NULL,
|
||||||
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET 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()
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||||
|
|
|
@ -104,7 +104,7 @@ impl Account {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct AccountCreateData {
|
pub struct AccountCreateData {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: Option<String>,
|
||||||
|
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
pub signature: Option<String>,
|
pub signature: Option<String>,
|
||||||
|
@ -117,6 +117,9 @@ impl AccountCreateData {
|
||||||
pub fn clean(&self) -> Result<(), ValidationError> {
|
pub fn clean(&self) -> Result<(), ValidationError> {
|
||||||
validate_username(&self.username)?;
|
validate_username(&self.username)?;
|
||||||
validate_local_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,6 +215,19 @@ mod tests {
|
||||||
|
|
||||||
const INSTANCE_URL: &str = "https://example.com";
|
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]
|
#[test]
|
||||||
fn test_create_account_from_profile() {
|
fn test_create_account_from_profile() {
|
||||||
let profile = DbActorProfile {
|
let profile = DbActorProfile {
|
||||||
|
|
|
@ -72,6 +72,14 @@ pub async fn create_account(
|
||||||
return Err(ValidationError("invalid invite code").into());
|
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 wallet_address = if let Some(message) = account_data.message.as_ref() {
|
||||||
let signature = account_data.signature.as_ref()
|
let signature = account_data.signature.as_ref()
|
||||||
.ok_or(ValidationError("signature is required"))?;
|
.ok_or(ValidationError("signature is required"))?;
|
||||||
|
@ -85,8 +93,10 @@ pub async fn create_account(
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
assert!(password_hash.is_some() || wallet_address.is_some());
|
||||||
|
|
||||||
if let Some(blockchain_config) = config.blockchain.as_ref() {
|
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()
|
let wallet_address = wallet_address.as_ref()
|
||||||
.ok_or(ValidationError("wallet address is required"))?;
|
.ok_or(ValidationError("wallet address is required"))?;
|
||||||
let is_allowed = is_allowed_user(blockchain_config, wallet_address).await
|
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());
|
return Err(ValidationError("not allowed to sign up").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Hash password and generate private key
|
// Generate RSA private key for actor
|
||||||
let password_hash = hash_password(&account_data.password)
|
|
||||||
.map_err(|_| HttpError::InternalError)?;
|
|
||||||
let private_key = match web::block(generate_private_key).await {
|
let private_key = match web::block(generate_private_key).await {
|
||||||
Ok(private_key) => private_key,
|
Ok(private_key) => private_key,
|
||||||
Err(_) => return Err(HttpError::InternalError),
|
Err(_) => return Err(HttpError::InternalError),
|
||||||
|
|
|
@ -58,9 +58,11 @@ async fn token_view(
|
||||||
if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
|
if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
|
||||||
let password = request_data.password.as_ref()
|
let password = request_data.password.as_ref()
|
||||||
.ok_or(ValidationError("password is required"))?;
|
.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(
|
let password_correct = verify_password(
|
||||||
&user.password_hash,
|
password_hash,
|
||||||
&password,
|
password,
|
||||||
).map_err(|_| HttpError::InternalError)?;
|
).map_err(|_| HttpError::InternalError)?;
|
||||||
if !password_correct {
|
if !password_correct {
|
||||||
return Err(ValidationError("incorrect password").into());
|
return Err(ValidationError("incorrect password").into());
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::models::profiles::types::DbActorProfile;
|
||||||
pub struct DbUser {
|
pub struct DbUser {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub wallet_address: Option<String>,
|
pub wallet_address: Option<String>,
|
||||||
pub password_hash: String,
|
pub password_hash: Option<String>,
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
pub invite_code: Option<String>,
|
pub invite_code: Option<String>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
@ -23,7 +23,7 @@ pub struct DbUser {
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub wallet_address: Option<String>,
|
pub wallet_address: Option<String>,
|
||||||
pub password_hash: String,
|
pub password_hash: Option<String>,
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
pub profile: DbActorProfile,
|
pub profile: DbActorProfile,
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ impl User {
|
||||||
#[cfg_attr(test, derive(Default))]
|
#[cfg_attr(test, derive(Default))]
|
||||||
pub struct UserCreateData {
|
pub struct UserCreateData {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password_hash: String,
|
pub password_hash: Option<String>,
|
||||||
pub private_key_pem: String,
|
pub private_key_pem: String,
|
||||||
pub wallet_address: Option<String>,
|
pub wallet_address: Option<String>,
|
||||||
pub invite_code: Option<String>,
|
pub invite_code: Option<String>,
|
||||||
|
|
Loading…
Reference in a new issue