use axum::{ extract::State, http::StatusCode, response::Json, routing::post, Router, }; use bcrypt::verify; use chrono::{Duration, Utc}; use jsonwebtoken::{encode, EncodingKey, Header}; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres}; use std::env; use std::sync::Arc; use tracing::{info, warn}; // 应用状态 #[derive(Clone)] struct AppState { db: Pool, jwt_secret: String, } // 登录请求 #[derive(Deserialize)] struct LoginRequest { email: String, password: String, } // 登录响应 #[derive(Serialize)] struct LoginResponse { success: bool, token: Option, message: String, } // JWT Claims #[derive(Serialize, Deserialize)] struct Claims { sub: String, exp: usize, iat: usize, } #[tokio::main] async fn main() { // 初始化日志 tracing_subscriber::fmt::init(); info!("Starting user-login-email service..."); // 数据库连接 let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = sqlx::postgres::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); info!("Database connected"); // JWT 密钥 let jwt_secret = env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret".to_string()); let state = Arc::new(AppState { db: pool, jwt_secret, }); // 路由 let app = Router::new() .route("/login", post(login_handler)) .route("/health", axum::routing::get(health_handler)) .with_state(state); let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "8080".to_string()); let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) .await .unwrap(); info!("User-login-email service listening on port {}", port); axum::serve(listener, app).await.unwrap(); } // 登录处理 async fn login_handler( State(state): State>, Json(payload): Json, ) -> (StatusCode, Json) { info!("Login attempt for email: {}", payload.email); // 查询用户邮箱与密码 let user: Option<(uuid::Uuid, String)> = sqlx::query_as( "SELECT e.user_id, p.password \ FROM user_login_email e \ JOIN user_login_password p ON e.user_id = p.user_id \ WHERE e.email = $1 AND e.deleted = FALSE AND p.deleted = FALSE" ) .bind(&payload.email) .fetch_optional(&state.db) .await .unwrap_or(None); match user { Some((user_id, password_hash)) => { // 验证密码 tracing::debug!("Verifying password: input_len={}, hash_len={}", payload.password.len(), password_hash.len()); match verify(&payload.password, &password_hash) { Ok(true) => { info!("Email {} logged in successfully", payload.email); // 生成 JWT let token = generate_token(&user_id.to_string(), &state.jwt_secret); ( StatusCode::OK, Json(LoginResponse { success: true, token: Some(token), message: "Login successful".to_string(), }), ) } Ok(false) => { warn!("Invalid password for email {}", payload.email); ( StatusCode::UNAUTHORIZED, Json(LoginResponse { success: false, token: None, message: "Invalid credentials".to_string(), }), ) } Err(e) => { warn!("Password verification error: {:?}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(LoginResponse { success: false, token: None, message: "Internal error".to_string(), }), ) } } } None => { warn!("Email not found: {}", payload.email); ( StatusCode::UNAUTHORIZED, Json(LoginResponse { success: false, token: None, message: "Invalid credentials".to_string(), }), ) } } } // 健康检查 async fn health_handler() -> &'static str { "OK" } // 生成 JWT Token fn generate_token(sub: &str, secret: &str) -> String { let now = Utc::now(); let exp = now + Duration::days(7); let claims = Claims { sub: sub.to_string(), iat: now.timestamp() as usize, exp: exp.timestamp() as usize, }; encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()), ) .unwrap() }