- add: initial database and auth services
This commit is contained in:
20
auth-service/Cargo.toml
Normal file
20
auth-service/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "auth-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tonic = "0.12.3"
|
||||
jsonwebtoken = "9.3.0"
|
||||
argon2 = "0.5.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
dotenv = "0.15"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
|
||||
prost = "0.13.3"
|
||||
prost-types = "0.13.3"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.12.3"
|
||||
16
auth-service/build.rs
Normal file
16
auth-service/build.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
fn main() {
|
||||
// gRPC Server code
|
||||
tonic_build::configure()
|
||||
.build_server(true) // Generate gRPC server code
|
||||
.compile_well_known_types(true)
|
||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.compile_protos(&["../proto/auth.proto"], &["../proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
|
||||
// gRPC Client code
|
||||
tonic_build::configure()
|
||||
.build_server(false) // Generate gRPC client code
|
||||
.compile_well_known_types(true)
|
||||
.compile_protos(&["../proto/database.proto"], &["../proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
}
|
||||
36
auth-service/src/database_client.rs
Normal file
36
auth-service/src/database_client.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use tonic::transport::Channel;
|
||||
use crate::database::{database_service_client::DatabaseServiceClient, GetUserByUsernameRequest, GetUserRequest, GetUserResponse};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DatabaseClient {
|
||||
client: DatabaseServiceClient<Channel>,
|
||||
}
|
||||
|
||||
impl DatabaseClient {
|
||||
pub async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = DatabaseServiceClient::connect(endpoint.to_string()).await?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
pub async fn get_user_by_userid(
|
||||
&mut self,
|
||||
user_id: i32,
|
||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>> {
|
||||
let request = tonic::Request::new(GetUserRequest {
|
||||
user_id,
|
||||
});
|
||||
let response = self.client.get_user(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn get_user_by_username(
|
||||
&mut self,
|
||||
username: &str,
|
||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>> {
|
||||
let request = tonic::Request::new(GetUserByUsernameRequest {
|
||||
username: username.to_string(),
|
||||
});
|
||||
let response = self.client.get_user_by_username(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
}
|
||||
52
auth-service/src/grpc.rs
Normal file
52
auth-service/src/grpc.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use tonic::{Request, Response, Status};
|
||||
use crate::jwt::{generate_token, validate_token};
|
||||
use crate::users::verify_user;
|
||||
use crate::database_client::DatabaseClient;
|
||||
use crate::auth::auth_service_server::{AuthService};
|
||||
use crate::auth::{LoginRequest, LoginResponse, ValidateTokenRequest, ValidateTokenResponse};
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub struct MyAuthService {
|
||||
pub db_client: DatabaseClient,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl AuthService for MyAuthService {
|
||||
async fn login(
|
||||
&self,
|
||||
request: Request<LoginRequest>,
|
||||
) -> Result<Response<LoginResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
|
||||
info!("Login attempt for username: {}", req.username);
|
||||
|
||||
if let Some(user_id) = verify_user(self.db_client.clone(), &req.username, &req.password).await {
|
||||
let token = generate_token(&user_id, vec!["user".to_string()])
|
||||
.map_err(|_| Status::internal("Token generation failed"))?;
|
||||
|
||||
info!("Login successful for username: {}", req.username);
|
||||
Ok(Response::new(LoginResponse { token, user_id }))
|
||||
} else {
|
||||
warn!("Invalid login attempt for username: {}", req.username);
|
||||
Err(Status::unauthenticated("Invalid credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_token(
|
||||
&self,
|
||||
request: Request<ValidateTokenRequest>,
|
||||
) -> Result<Response<ValidateTokenResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
|
||||
match validate_token(&req.token) {
|
||||
Ok(user_id) => Ok(Response::new(ValidateTokenResponse {
|
||||
valid: true,
|
||||
user_id,
|
||||
})),
|
||||
Err(_) => Ok(Response::new(ValidateTokenResponse {
|
||||
valid: false,
|
||||
user_id: "".to_string(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
36
auth-service/src/jwt.rs
Normal file
36
auth-service/src/jwt.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String, // Subject (user ID)
|
||||
roles: Vec<String>, // Roles/permissions
|
||||
exp: usize, // Expiration time
|
||||
}
|
||||
|
||||
pub fn generate_token(user_id: &str, roles: Vec<String>) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
let expiration = chrono::Utc::now()
|
||||
.checked_add_signed(chrono::Duration::days(1))
|
||||
.expect("valid timestamp")
|
||||
.timestamp() as usize;
|
||||
|
||||
let claims = Claims {
|
||||
sub: user_id.to_owned(),
|
||||
roles,
|
||||
exp: expiration,
|
||||
};
|
||||
|
||||
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_ref()))
|
||||
}
|
||||
|
||||
pub fn validate_token(token: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
let token_data = decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret.as_ref()),
|
||||
&Validation::default(),
|
||||
)?;
|
||||
Ok(token_data.claims.sub)
|
||||
}
|
||||
11
auth-service/src/lib.rs
Normal file
11
auth-service/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod grpc;
|
||||
pub mod jwt;
|
||||
pub mod database_client;
|
||||
|
||||
pub mod users;
|
||||
pub mod auth {
|
||||
tonic::include_proto!("auth"); // Path matches the package name in auth.proto
|
||||
}
|
||||
pub mod database {
|
||||
tonic::include_proto!("database"); // Matches package name in database.proto
|
||||
}
|
||||
45
auth-service/src/main.rs
Normal file
45
auth-service/src/main.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use tonic::transport::Server;
|
||||
use auth_service::grpc::MyAuthService;
|
||||
use auth_service::database_client::DatabaseClient;
|
||||
use auth_service::auth::auth_service_server::AuthServiceServer;
|
||||
|
||||
pub mod auth {
|
||||
tonic::include_proto!("auth"); // Path matches the package name in auth.proto
|
||||
}
|
||||
pub mod database {
|
||||
tonic::include_proto!("database"); // Matches package name in database.proto
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Load environment variables from .env
|
||||
dotenv().ok();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_thread_names(true)
|
||||
.with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339())
|
||||
.init();
|
||||
|
||||
// Set the gRPC server address
|
||||
let addr = env::var("AUTH_SERVICE_ADDR").unwrap_or_else(|_| "127.0.0.1:50051".to_string());
|
||||
let db_addr = env::var("DATABASE_SERVICE_ADDR").unwrap_or_else(|_| "http://127.0.0.1:50052".to_string());
|
||||
let database_client = DatabaseClient::connect(&db_addr).await?;
|
||||
|
||||
let addr = addr.parse().expect("Invalid address");
|
||||
let auth_service = MyAuthService {
|
||||
db_client: database_client,
|
||||
};
|
||||
|
||||
println!("Authentication Service running on {}", addr);
|
||||
|
||||
// Start the gRPC server
|
||||
Server::builder()
|
||||
.add_service(AuthServiceServer::new(auth_service))
|
||||
.serve(addr)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
32
auth-service/src/users.rs
Normal file
32
auth-service/src/users.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use crate::database_client::DatabaseClient;
|
||||
|
||||
use argon2::{
|
||||
password_hash::{
|
||||
rand_core::OsRng,
|
||||
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
|
||||
},
|
||||
Argon2
|
||||
};
|
||||
|
||||
pub fn hash_password(password: &str) -> String {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
argon2.hash_password(password.as_ref(), &salt).unwrap().to_string()
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, hash: &str) -> bool {
|
||||
let parsed_hash = PasswordHash::new(&hash).unwrap();
|
||||
Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok()
|
||||
}
|
||||
|
||||
pub async fn verify_user(mut db_client: DatabaseClient,
|
||||
username: &str, password: &str) -> Option<String> {
|
||||
// Placeholder: Replace with a gRPC call to the Database Service
|
||||
let user = db_client.get_user_by_username(username).await.ok()?;
|
||||
|
||||
if verify_password(password, &user.hashed_password) {
|
||||
Some(user.user_id.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
60
auth-service/tests/integration.rs
Normal file
60
auth-service/tests/integration.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use dotenv::dotenv;
|
||||
use tonic::Request;
|
||||
use auth_service::auth::auth_service_server::AuthService;
|
||||
use auth_service::auth::{LoginRequest, LoginResponse, ValidateTokenRequest, ValidateTokenResponse};
|
||||
use auth_service::database_client::DatabaseClient;
|
||||
use auth_service::grpc::MyAuthService;
|
||||
use auth_service::jwt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_login() {
|
||||
dotenv().ok();
|
||||
|
||||
// Mock dependencies or use the actual Database Service
|
||||
let db_client = DatabaseClient::connect("http://127.0.0.1:50052").await.unwrap();
|
||||
|
||||
let auth_service = MyAuthService {
|
||||
db_client,
|
||||
};
|
||||
|
||||
// Create a test LoginRequest
|
||||
let request = Request::new(LoginRequest {
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
});
|
||||
|
||||
// Call the login method
|
||||
let response = auth_service.login(request).await.unwrap().into_inner();
|
||||
|
||||
// Verify the response
|
||||
assert!(!response.token.is_empty());
|
||||
assert_eq!(response.user_id, "9"); // Replace with the expected user ID
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_validate_token() {
|
||||
dotenv().ok();
|
||||
|
||||
let db_client = DatabaseClient::connect("http://127.0.0.1:50052").await.unwrap();
|
||||
|
||||
let auth_service = MyAuthService {
|
||||
db_client,
|
||||
};
|
||||
|
||||
// Generate a token for testing
|
||||
let token = jwt::generate_token("123", Vec::from(["".to_string()])).unwrap();
|
||||
|
||||
// Create a ValidateTokenRequest
|
||||
let request = Request::new(ValidateTokenRequest { token });
|
||||
|
||||
// Call the validate_token method
|
||||
let response = auth_service.validate_token(request).await.unwrap().into_inner();
|
||||
|
||||
// Verify the response
|
||||
assert!(response.valid);
|
||||
assert_eq!(response.user_id, "123");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user