Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions backend/migrations/003_add_nonces.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add login_nonce column to profiles table
-- The nonce starts at 1 and increments with each successful login
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS login_nonce BIGINT NOT NULL DEFAULT 1;
2 changes: 0 additions & 2 deletions backend/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
pub mod create_profile;
pub mod get_all_profiles;
pub mod get_profile;
pub mod update_profile;
10 changes: 10 additions & 0 deletions backend/src/application/dtos/auth_dtos.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyMessageRequest {
pub address: String,
pub nonce: String,
pub message: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VerifyMessageResponse {
pub success: bool,
pub address: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct NonceResponse {
pub nonce: i64,
pub address: String,
}
1 change: 1 addition & 0 deletions backend/src/application/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod commands;
pub mod dtos;
pub mod queries;
19 changes: 19 additions & 0 deletions backend/src/application/queries/get_login_nonce.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::sync::Arc;

use crate::domain::{repositories::ProfileRepository, value_objects::WalletAddress};

pub async fn get_login_nonce(
profile_repository: Arc<dyn ProfileRepository>,
address: String,
) -> Result<i64, String> {
let wallet_address = WalletAddress(address);

match profile_repository
.get_login_nonce_by_wallet_address(&wallet_address)
.await
{
Ok(Some(nonce)) => Ok(nonce),
Ok(None) => Ok(1), // Return default nonce for new addresses
Err(e) => Err(format!("Error fetching nonce: {}", e)),
}
}
3 changes: 3 additions & 0 deletions backend/src/application/queries/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod get_all_profiles;
pub mod get_login_nonce;
pub mod get_profile;
2 changes: 2 additions & 0 deletions backend/src/domain/entities/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct Profile {
pub description: Option<String>,
pub avatar_url: Option<String>,
pub github_login: Option<String>,
pub login_nonce: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Expand All @@ -23,6 +24,7 @@ impl Profile {
description: None,
avatar_url: None,
github_login: None,
login_nonce: 1,
created_at: now,
updated_at: now,
}
Expand Down
8 changes: 8 additions & 0 deletions backend/src/domain/repositories/profile_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ pub trait ProfileRepository: Send + Sync {
&self,
github_login: &str,
) -> Result<Option<Profile>, Box<dyn std::error::Error + Send + Sync>>;
async fn get_login_nonce_by_wallet_address(
&self,
address: &WalletAddress,
) -> Result<Option<i64>, Box<dyn std::error::Error>>;
async fn increment_login_nonce(
&self,
address: &WalletAddress,
) -> Result<(), Box<dyn std::error::Error>>;
}
2 changes: 1 addition & 1 deletion backend/src/domain/services/auth_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::domain::value_objects::wallet_address::WalletAddress;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthChallenge {
pub nonce: String,
pub nonce: i64,
pub address: String,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ impl ProfileRepository for PostgresProfileRepository {
description: r.description,
avatar_url: r.avatar_url,
github_login: r.github_login,
login_nonce: 0, // Not needed for regular profile queries
created_at: r.created_at.unwrap(),
updated_at: r.updated_at.unwrap(),
}))
Expand All @@ -64,6 +65,7 @@ impl ProfileRepository for PostgresProfileRepository {
description: r.description,
avatar_url: r.avatar_url,
github_login: r.github_login,
login_nonce: 0, // Not needed for regular profile queries
created_at: r.created_at.unwrap(),
updated_at: r.updated_at.unwrap(),
})
Expand All @@ -73,14 +75,15 @@ impl ProfileRepository for PostgresProfileRepository {
async fn create(&self, profile: &Profile) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query!(
r#"
INSERT INTO profiles (address, name, description, avatar_url, github_login, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
INSERT INTO profiles (address, name, description, avatar_url, github_login, login_nonce, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
profile.address.as_str(),
profile.name,
profile.description,
profile.avatar_url,
profile.github_login,
profile.login_nonce,
profile.created_at,
profile.updated_at
)
Expand Down Expand Up @@ -149,8 +152,47 @@ impl ProfileRepository for PostgresProfileRepository {
description: r.description,
avatar_url: r.avatar_url,
github_login: r.github_login,
login_nonce: 0, // Not needed for regular profile queries
created_at: r.created_at.unwrap(),
updated_at: r.updated_at.unwrap(),
}))
}

async fn get_login_nonce_by_wallet_address(
&self,
address: &WalletAddress,
) -> Result<Option<i64>, Box<dyn std::error::Error>> {
let row = sqlx::query!(
r#"
SELECT login_nonce
FROM profiles
WHERE address = $1
"#,
address.as_str()
)
.fetch_optional(&self.pool)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;

Ok(row.map(|r| r.login_nonce))
}

async fn increment_login_nonce(
&self,
address: &WalletAddress,
) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query!(
r#"
UPDATE profiles
SET login_nonce = login_nonce + 1, updated_at = NOW()
WHERE address = $1
"#,
address.as_str()
)
.execute(&self.pool)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;

Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ use async_trait::async_trait;
use ethers::core::utils::hash_message;
use ethers::types::{Address, Signature};
use std::str::FromStr;
use std::sync::Arc;

use crate::domain::repositories::ProfileRepository;
use crate::domain::services::auth_service::{AuthChallenge, AuthResult, AuthService};
use crate::domain::value_objects::WalletAddress;

pub struct EthereumAddressVerificationService {}

impl EthereumAddressVerificationService {
pub fn new() -> Self {
Self {}
}
pub struct EthereumAddressVerificationService {
profile_repository: Arc<dyn ProfileRepository>,
}

impl Default for EthereumAddressVerificationService {
fn default() -> Self {
Self::new()
impl EthereumAddressVerificationService {
pub fn new(profile_repository: Arc<dyn ProfileRepository>) -> Self {
Self { profile_repository }
}
}

Expand All @@ -27,10 +25,14 @@ impl AuthService for EthereumAddressVerificationService {
challenge: &AuthChallenge,
signature: &str,
) -> Result<Option<AuthResult>, Box<dyn std::error::Error>> {
const EXPECTED_MSG: &str = "LOGIN_NONCE"; // or whatever constant string you are signing
// Create the message with the nonce
let message = format!(
"Sign this message to authenticate with The Guild.\n\nNonce: {}",
challenge.nonce
);

// EIP-191 prefix + keccak256
let msg_hash = hash_message(EXPECTED_MSG);
let msg_hash = hash_message(message);

// Parse signature and expected address
let sig = Signature::from_str(signature)?;
Expand All @@ -40,9 +42,13 @@ impl AuthService for EthereumAddressVerificationService {
let recovered = sig.recover(msg_hash)?;

if recovered == expected {
Ok(Some(AuthResult {
wallet_address: WalletAddress(challenge.address.clone()),
}))
// Increment the nonce after successful verification
let wallet_address = WalletAddress(challenge.address.clone());
self.profile_repository
.increment_login_nonce(&wallet_address)
.await?;

Ok(Some(AuthResult { wallet_address }))
} else {
Ok(None)
}
Expand Down
13 changes: 8 additions & 5 deletions backend/src/presentation/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,24 @@ use tower_http::{
};

use super::handlers::{
create_profile_handler, delete_profile_handler, get_all_profiles_handler, get_profile_handler,
update_profile_handler,
create_profile_handler, delete_profile_handler, get_all_profiles_handler, get_nonce_handler,
get_profile_handler, update_profile_handler,
};

use super::middlewares::{eth_auth_layer, test_auth_layer};

pub async fn create_app(pool: sqlx::PgPool) -> Router {
let auth_service = EthereumAddressVerificationService::new();
let profile_repository = PostgresProfileRepository::new(pool);
let profile_repository = Arc::from(PostgresProfileRepository::new(pool));
let auth_service = EthereumAddressVerificationService::new(profile_repository.clone());

let state: AppState = AppState {
profile_repository: Arc::from(profile_repository),
profile_repository,
auth_service: Arc::from(auth_service),
};

let protected_routes = Router::new()
.route("/profiles", post(create_profile_handler))
.route("/profiles/", post(create_profile_handler))
.route("/profiles/:address", put(update_profile_handler))
.route("/profiles/:address", delete(delete_profile_handler))
.with_state(state.clone());
Expand All @@ -50,6 +51,7 @@ pub async fn create_app(pool: sqlx::PgPool) -> Router {
let public_routes = Router::new()
.route("/profiles/:address", get(get_profile_handler))
.route("/profiles", get(get_all_profiles_handler))
.route("/auth/nonce/:address", get(get_nonce_handler))
.with_state(state.clone());

Router::new()
Expand Down Expand Up @@ -86,6 +88,7 @@ pub fn test_api(state: AppState) -> Router {
let public_routes = Router::new()
.route("/profiles/:address", get(get_profile_handler))
.route("/profiles", get(get_all_profiles_handler))
.route("/auth/nonce/:address", get(get_nonce_handler))
.with_state(state.clone());

Router::new()
Expand Down
19 changes: 15 additions & 4 deletions backend/src/presentation/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ use axum::{

use crate::{
application::{
commands::{
create_profile::create_profile, get_all_profiles::get_all_profiles,
get_profile::get_profile, update_profile::update_profile,
commands::{create_profile::create_profile, update_profile::update_profile},
dtos::{CreateProfileRequest, NonceResponse, ProfileResponse, UpdateProfileRequest},
queries::{
get_all_profiles::get_all_profiles, get_login_nonce::get_login_nonce,
get_profile::get_profile,
},
dtos::{CreateProfileRequest, ProfileResponse, UpdateProfileRequest},
},
domain::value_objects::WalletAddress,
};
Expand Down Expand Up @@ -76,3 +77,13 @@ pub async fn delete_profile_handler(
.unwrap();
StatusCode::ACCEPTED
}

pub async fn get_nonce_handler(
State(state): State<AppState>,
Path(address): Path<String>,
) -> impl IntoResponse {
match get_login_nonce(state.profile_repository, address.clone()).await {
Ok(nonce) => Json(NonceResponse { nonce, address }).into_response(),
Err(e) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e}))).into_response(),
}
}
Loading
Loading