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
4 changes: 3 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ DATABASE_URL=postgres://guild_user:guild_password@localhost:5432/guild_genesis
# Optional: allow SQLx offline mode when building
# SQLX_OFFLINE=true

# Other env vars your app uses...
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRATION=86400
50 changes: 44 additions & 6 deletions backend/Cargo.lock

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

1 change: 1 addition & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ serde_json = "1.0"
siwe = "0.6"
ethers = { version = "2.0", features = ["rustls"] }
sha3 = "0.10"
jsonwebtoken = "9.3"

# Pin problematic dependencies to avoid edition 2024
base64ct = "1.7.3"
Expand Down
6 changes: 6 additions & 0 deletions backend/src/application/commands/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use crate::infrastructure::jwt::JwtManager;

pub async fn login(address: String) -> Result<String, String> {
let jwt_manager = JwtManager::new();
jwt_manager.generate_token(&address)
}
1 change: 1 addition & 0 deletions backend/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod create_profile;
pub mod login;
pub mod update_profile;
6 changes: 6 additions & 0 deletions backend/src/application/dtos/auth_dtos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ pub struct NonceResponse {
pub nonce: i64,
pub address: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AuthTokenResponse {
pub token: String,
pub address: String,
}
57 changes: 57 additions & 0 deletions backend/src/infrastructure/jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::env;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JwtClaims {
pub address: String,
pub exp: usize,
}

pub struct JwtManager {
secret: String,
expiration: usize,
}

impl JwtManager {
pub fn new() -> Self {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let expiration: usize = env::var("JWT_EXPIRATION")
.unwrap_or_else(|_| "86400".to_string())
.parse()
.expect("JWT_EXPIRATION must be a valid number (seconds)");

JwtManager { secret, expiration }
}

pub fn generate_token(&self, address: &str) -> Result<String, String> {
let now = chrono::Utc::now().timestamp() as usize;
let claims = JwtClaims {
address: address.to_string(),
exp: now + self.expiration,
};

encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(self.secret.as_bytes()),
)
.map_err(|e| format!("Failed to generate token: {}", e))
}

pub fn validate_token(&self, token: &str) -> Result<JwtClaims, String> {
decode::<JwtClaims>(
token,
&DecodingKey::from_secret(self.secret.as_bytes()),
&Validation::default(),
)
.map(|data| data.claims)
.map_err(|e| format!("Invalid token: {}", e))
}
}

impl Default for JwtManager {
fn default() -> Self {
Self::new()
}
}
1 change: 1 addition & 0 deletions backend/src/infrastructure/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod jwt;
pub mod repositories;
pub mod services;
4 changes: 3 additions & 1 deletion backend/src/presentation/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use tower_http::{

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

use super::middlewares::{eth_auth_layer, test_auth_layer};
Expand All @@ -40,6 +40,7 @@ pub async fn create_app(pool: sqlx::PgPool) -> Router {
.route("/profiles/", post(create_profile_handler))
.route("/profiles/:address", put(update_profile_handler))
.route("/profiles/:address", delete(delete_profile_handler))
.route("/auth/login", post(login_handler))
.with_state(state.clone());

let protected_with_auth = if std::env::var("TEST_MODE").is_ok() {
Expand Down Expand Up @@ -82,6 +83,7 @@ pub fn test_api(state: AppState) -> Router {
.route("/profiles", post(create_profile_handler))
.route("/profiles/:address", put(update_profile_handler))
.route("/profiles/:address", delete(delete_profile_handler))
.route("/auth/login", post(login_handler))
.with_state(state.clone())
.layer(from_fn(test_auth_layer));

Expand Down
20 changes: 18 additions & 2 deletions backend/src/presentation/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ use axum::{

use crate::{
application::{
commands::{create_profile::create_profile, update_profile::update_profile},
dtos::{CreateProfileRequest, NonceResponse, ProfileResponse, UpdateProfileRequest},
commands::{create_profile::create_profile, login::login, update_profile::update_profile},
dtos::{
AuthTokenResponse, CreateProfileRequest, NonceResponse, ProfileResponse,
UpdateProfileRequest,
},
queries::{
get_all_profiles::get_all_profiles, get_login_nonce::get_login_nonce,
get_profile::get_profile,
Expand Down Expand Up @@ -87,3 +90,16 @@ pub async fn get_nonce_handler(
Err(e) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e}))).into_response(),
}
}

pub async fn login_handler(
Extension(VerifiedWallet(address)): Extension<VerifiedWallet>,
) -> impl IntoResponse {
match login(address.clone()).await {
Ok(token) => (StatusCode::OK, Json(AuthTokenResponse { token, address })).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e})),
)
.into_response(),
}
}
15 changes: 15 additions & 0 deletions backend/src/presentation/middlewares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use axum::{
};

use crate::domain::services::auth_service::AuthChallenge;
use crate::infrastructure::jwt::JwtManager;

use super::api::AppState;

Expand All @@ -31,6 +32,20 @@ pub async fn eth_auth_layer(
return Ok(next.run(req).await);
}

// Try JWT token first
if let Some(auth_header) = headers.get("authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if let Some(token) = auth_str.strip_prefix("Bearer ") {
let jwt_manager = JwtManager::new();
if let Ok(claims) = jwt_manager.validate_token(token) {
req.extensions_mut().insert(VerifiedWallet(claims.address));
return Ok(next.run(req).await);
}
}
}
}

// Fall back to signature verification
let address = headers
.get("x-eth-address")
.and_then(|v| v.to_str().ok())
Expand Down
Loading