From 3ff22c9a5bb8645a9ac704c7f4c1072c13640f24190e259c26e0eef98459d443 Mon Sep 17 00:00:00 2001 From: raven <7156279+RavenX8@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:45:16 -0500 Subject: [PATCH] - add: initial database and auth services --- .gitignore | 6 +- auth-service/Cargo.toml | 20 ++++ auth-service/build.rs | 16 +++ auth-service/src/database_client.rs | 36 +++++++ auth-service/src/grpc.rs | 52 ++++++++++ auth-service/src/jwt.rs | 36 +++++++ auth-service/src/lib.rs | 11 +++ auth-service/src/main.rs | 45 +++++++++ auth-service/src/users.rs | 32 ++++++ auth-service/tests/integration.rs | 60 ++++++++++++ database-service/Cargo.toml | 21 ++++ database-service/build.rs | 8 ++ database-service/src/db.rs | 24 +++++ database-service/src/grpc.rs | 63 ++++++++++++ database-service/src/lib.rs | 8 ++ database-service/src/main.rs | 48 +++++++++ database-service/src/redis_cache.rs | 60 ++++++++++++ database-service/src/users.rs | 123 ++++++++++++++++++++++++ database-service/tests/get_user.rs | 30 ++++++ database-service/tests/grpc_get_user.rs | 25 +++++ database-service/tests/integration.rs | 14 +++ database-service/tests/redis_cache.rs | 19 ++++ proto/auth.proto | 27 ++++++ proto/database.proto | 34 +++++++ 24 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 auth-service/Cargo.toml create mode 100644 auth-service/build.rs create mode 100644 auth-service/src/database_client.rs create mode 100644 auth-service/src/grpc.rs create mode 100644 auth-service/src/jwt.rs create mode 100644 auth-service/src/lib.rs create mode 100644 auth-service/src/main.rs create mode 100644 auth-service/src/users.rs create mode 100644 auth-service/tests/integration.rs create mode 100644 database-service/Cargo.toml create mode 100644 database-service/build.rs create mode 100644 database-service/src/db.rs create mode 100644 database-service/src/grpc.rs create mode 100644 database-service/src/lib.rs create mode 100644 database-service/src/main.rs create mode 100644 database-service/src/redis_cache.rs create mode 100644 database-service/src/users.rs create mode 100644 database-service/tests/get_user.rs create mode 100644 database-service/tests/grpc_get_user.rs create mode 100644 database-service/tests/integration.rs create mode 100644 database-service/tests/redis_cache.rs create mode 100644 proto/auth.proto create mode 100644 proto/database.proto diff --git a/.gitignore b/.gitignore index 3fb8173..f027000 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,13 @@ debug/ target/ +auth-service/.env +database-service/.env + # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock +auth-service/Cargo.lock +database-service/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/auth-service/Cargo.toml b/auth-service/Cargo.toml new file mode 100644 index 0000000..93204dc --- /dev/null +++ b/auth-service/Cargo.toml @@ -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" diff --git a/auth-service/build.rs b/auth-service/build.rs new file mode 100644 index 0000000..51b9cc7 --- /dev/null +++ b/auth-service/build.rs @@ -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)); +} diff --git a/auth-service/src/database_client.rs b/auth-service/src/database_client.rs new file mode 100644 index 0000000..0457a03 --- /dev/null +++ b/auth-service/src/database_client.rs @@ -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, +} + +impl DatabaseClient { + pub async fn connect(endpoint: &str) -> Result> { + let client = DatabaseServiceClient::connect(endpoint.to_string()).await?; + Ok(Self { client }) + } + + pub async fn get_user_by_userid( + &mut self, + user_id: i32, + ) -> Result> { + 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> { + let request = tonic::Request::new(GetUserByUsernameRequest { + username: username.to_string(), + }); + let response = self.client.get_user_by_username(request).await?; + Ok(response.into_inner()) + } +} diff --git a/auth-service/src/grpc.rs b/auth-service/src/grpc.rs new file mode 100644 index 0000000..027d14a --- /dev/null +++ b/auth-service/src/grpc.rs @@ -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, + ) -> Result, 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, + ) -> Result, 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(), + })), + } + } +} diff --git a/auth-service/src/jwt.rs b/auth-service/src/jwt.rs new file mode 100644 index 0000000..f9c94b1 --- /dev/null +++ b/auth-service/src/jwt.rs @@ -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, // Roles/permissions + exp: usize, // Expiration time +} + +pub fn generate_token(user_id: &str, roles: Vec) -> Result { + 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 { + let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set"); + let token_data = decode::( + token, + &DecodingKey::from_secret(secret.as_ref()), + &Validation::default(), + )?; + Ok(token_data.claims.sub) +} diff --git a/auth-service/src/lib.rs b/auth-service/src/lib.rs new file mode 100644 index 0000000..6986e7a --- /dev/null +++ b/auth-service/src/lib.rs @@ -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 +} \ No newline at end of file diff --git a/auth-service/src/main.rs b/auth-service/src/main.rs new file mode 100644 index 0000000..1108389 --- /dev/null +++ b/auth-service/src/main.rs @@ -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> { + // 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(()) +} diff --git a/auth-service/src/users.rs b/auth-service/src/users.rs new file mode 100644 index 0000000..6ddf418 --- /dev/null +++ b/auth-service/src/users.rs @@ -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 { + // 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 + } +} diff --git a/auth-service/tests/integration.rs b/auth-service/tests/integration.rs new file mode 100644 index 0000000..262299b --- /dev/null +++ b/auth-service/tests/integration.rs @@ -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"); + } +} \ No newline at end of file diff --git a/database-service/Cargo.toml b/database-service/Cargo.toml new file mode 100644 index 0000000..6b45257 --- /dev/null +++ b/database-service/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "database-service" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +tonic-build = "0.12.3" + +[dependencies] +tokio = { version = "1.41.1", features = ["full"] } +sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls", "chrono"] } +tonic = "0.12.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" +redis = "0.27.5" +deadpool-redis = "0.18.0" +serde_json = "1.0.133" diff --git a/database-service/build.rs b/database-service/build.rs new file mode 100644 index 0000000..faddf25 --- /dev/null +++ b/database-service/build.rs @@ -0,0 +1,8 @@ +fn main() { + tonic_build::configure() + .build_server(true) + .compile_well_known_types(true) + .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") + .compile_protos(&["../proto/database.proto"], &["../proto"]) + .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); +} diff --git a/database-service/src/db.rs b/database-service/src/db.rs new file mode 100644 index 0000000..69b247a --- /dev/null +++ b/database-service/src/db.rs @@ -0,0 +1,24 @@ +use sqlx::PgPool; +use std::sync::Arc; +use crate::redis_cache::RedisCache; +use crate::users::UsersService; + +pub struct Database { + pub users_service: UsersService, // User-specific functionality +} + +impl Database { + pub async fn new(pool: PgPool, cache: Arc) -> Self { + let users_service = UsersService { pool, cache }; + + Self { users_service } + } + + pub async fn health_check(&self) -> bool { + // Simple query to check database health + sqlx::query("SELECT 1") + .execute(&self.users_service.pool) + .await + .is_ok() + } +} diff --git a/database-service/src/grpc.rs b/database-service/src/grpc.rs new file mode 100644 index 0000000..0675036 --- /dev/null +++ b/database-service/src/grpc.rs @@ -0,0 +1,63 @@ +use crate::db::Database; +use crate::database::{CreateUserRequest, CreateUserResponse, GetUserRequest, GetUserByUsernameRequest, GetUserResponse}; +use tonic::{Request, Response, Status}; + +use crate::database::database_service_server::{DatabaseService}; +use tracing::{debug, error}; + +pub struct MyDatabaseService { + pub db: Database, // Use the Database struct from users.rs +} + +#[tonic::async_trait] +impl DatabaseService for MyDatabaseService { + async fn get_user( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + let user = self.db.users_service.get_user_by_id(req.user_id) + .await + .map_err(|_| Status::not_found("User not found"))?; + + Ok(Response::new(GetUserResponse { + user_id: user.id, + username: user.username, + email: user.email, + hashed_password: user.hashed_password, + })) + } + + async fn get_user_by_username( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + let user = self.db.users_service.get_user_by_username(&req.username) + .await + .map_err(|_| Status::not_found("User not found"))?; + + Ok(Response::new(GetUserResponse { + user_id: user.id, + username: user.username, + email: user.email, + hashed_password: user.hashed_password, + })) + } + + async fn create_user( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + let user_id = self.db.users_service.create_user(&req.username, &req.email, &req.hashed_password) + .await + .map_err(|_| Status::internal("Failed to create user"))?; + + // Return the newly created user ID + Ok(Response::new(CreateUserResponse { user_id: user_id })) + } +} \ No newline at end of file diff --git a/database-service/src/lib.rs b/database-service/src/lib.rs new file mode 100644 index 0000000..fb1b43a --- /dev/null +++ b/database-service/src/lib.rs @@ -0,0 +1,8 @@ +pub mod users; +pub mod redis_cache; +pub mod db; +pub mod grpc; + +pub mod database { + tonic::include_proto!("database"); +} \ No newline at end of file diff --git a/database-service/src/main.rs b/database-service/src/main.rs new file mode 100644 index 0000000..ef621df --- /dev/null +++ b/database-service/src/main.rs @@ -0,0 +1,48 @@ +use dotenv::dotenv; +use std::env; +use tonic::transport::Server; +use database_service::db::Database; +use database_service::redis_cache::RedisCache; +use database::database_service_server::DatabaseServiceServer; +use std::sync::Arc; +use database_service::database; +use database_service::grpc::MyDatabaseService; +use sqlx::postgres::PgPoolOptions; + + +#[tokio::main] +async fn main() -> Result<(), Box> { + 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(); + + let addr = env::var("DATABASE_SERVICE_ADDR").unwrap_or_else(|_| "127.0.0.1:50052".to_string()); + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); + + let addr = addr.parse().expect("Invalid address"); + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Failed to create PostgreSQL connection pool"); + + let redis_cache = RedisCache::new(&redis_url); + + let cache = Arc::new(redis_cache); // Share the cache instance between tasks + let database_service = MyDatabaseService { + db: Database::new(pool, cache).await, + }; + + // Pass `shared_cache` into services as needed + println!("Database Service running on {}", addr); + Server::builder() + .add_service(DatabaseServiceServer::new(database_service)) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/database-service/src/redis_cache.rs b/database-service/src/redis_cache.rs new file mode 100644 index 0000000..6016e5c --- /dev/null +++ b/database-service/src/redis_cache.rs @@ -0,0 +1,60 @@ +use deadpool_redis::{Config, Pool, Runtime}; +use redis::{AsyncCommands, RedisError}; +use serde::{de::DeserializeOwned, Serialize}; + +pub struct RedisCache { + pub pool: Pool, +} + +impl RedisCache { + pub fn new(redis_url: &str) -> Self { + let cfg = Config::from_url(redis_url); + let pool = cfg.create_pool(Some(Runtime::Tokio1)).expect("Failed to create Redis pool"); + RedisCache { pool } + } + + pub async fn set( + &self, + key: &String, + value: &T, + ttl: u64, + ) -> Result<(), redis::RedisError> { + let mut conn = self.pool.get().await + .map_err(|err| { + redis::RedisError::from(( + redis::ErrorKind::IoError, + "Failed to get Redis connection", + format!("{:?}", err), + )) + })?; + let serialized_value = serde_json::to_string(value) + .map_err(|err| RedisError::from(( + redis::ErrorKind::IoError, + "Serialization error", + format!("Serialization error: {}", err), + )))?; + conn.set_ex(key, serialized_value, ttl).await + } + + pub async fn get(&self, key: &String) -> Result, redis::RedisError> { + let mut conn = self.pool.get().await + .map_err(|err| { + redis::RedisError::from(( + redis::ErrorKind::IoError, + "Failed to get Redis connection", + format!("{:?}", err), + )) + })?; + if let Some(serialized_value) = conn.get::<_, Option>(key).await? { + let deserialized_value = serde_json::from_str(&serialized_value) + .map_err(|err| RedisError::from(( + redis::ErrorKind::IoError, + "Deserialization error", + format!("Deserialization error: {}", err), + )))?; + Ok(Some(deserialized_value)) + } else { + Ok(None) + } + } +} diff --git a/database-service/src/users.rs b/database-service/src/users.rs new file mode 100644 index 0000000..0e910ef --- /dev/null +++ b/database-service/src/users.rs @@ -0,0 +1,123 @@ +use sqlx::Error; +use sqlx::PgPool; +use serde::{Serialize, Deserialize}; +use std::sync::Arc; +use crate::redis_cache::RedisCache; +use tracing::{debug, error}; + + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: i32, + pub username: String, + pub email: String, + pub hashed_password: String, +} + +pub struct UsersService { + pub pool: PgPool, + pub cache: Arc, // Shared Redis cache +} + + +impl UsersService { + pub async fn create_user( + &self, + username: &str, + email: &str, + hashed_password: &str, + ) -> Result { + let result = sqlx::query!( + r#" + INSERT INTO users (username, email, hashed_password) + VALUES ($1, $2, $3) + RETURNING id + "#, + username, + email, + hashed_password + ) + .fetch_one(&self.pool) + .await?; + + Ok(result.id) + } + + pub async fn get_user_by_id(&self, user_id: i32) -> Result { + // Check Redis cache first + if let Ok(Some(cached_user)) = self.cache.get::(&format!("user:{}", user_id)).await { + return Ok(cached_user); + } + + // Fetch from PostgreSQL + let user = sqlx::query_as!( + User, + "SELECT id, username, email, hashed_password FROM users WHERE id = $1", + user_id + ) + .fetch_one(&self.pool) + .await?; + + // Store result in Redis + self.cache + .set(&format!("user:{}", user_id), &user, 3600) + .await + .unwrap_or_else(|err| eprintln!("Failed to cache user: {:?}", err)); + + Ok(user) + } + + pub async fn get_user_by_username(&self, username: &str) -> Result { + // Check Redis cache first + if let Ok(Some(cached_user)) = self.cache.get::(&format!("user_by_username:{}", username)).await { + return Ok(cached_user); + } + + // Fetch from PostgreSQL + let user = sqlx::query_as!( + User, + "SELECT id, username, email, hashed_password FROM users WHERE username = $1", + username + ) + .fetch_one(&self.pool) + .await?; + + // Store result in Redis + self.cache + .set(&format!("user_by_username:{}", username), &user, 3600) + .await + .unwrap_or_else(|err| eprintln!("Failed to cache user: {:?}", err)); + + Ok(user) + } + + pub async fn update_user_email(&self, user_id: i32, new_email: &str) -> Result<(), Error> { + sqlx::query!( + r#" + UPDATE users + SET email = $1, updated_at = CURRENT_TIMESTAMP + WHERE id = $2 + "#, + new_email, + user_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_user(&self, user_id: i32) -> Result<(), Error> { + sqlx::query!( + r#" + DELETE FROM users + WHERE id = $1 + "#, + user_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } +} diff --git a/database-service/tests/get_user.rs b/database-service/tests/get_user.rs new file mode 100644 index 0000000..68dc834 --- /dev/null +++ b/database-service/tests/get_user.rs @@ -0,0 +1,30 @@ +use sqlx::{PgPool, Executor}; +use tokio; + +#[tokio::test] +async fn test_get_user() { + // Set up a temporary in-memory PostgreSQL database + let pool = PgPool::connect("postgres://user:password@localhost/test_database").await.unwrap(); + + // Create the test table + pool.execute( + r#" + CREATE TABLE users ( + user_id TEXT PRIMARY KEY, + username TEXT NOT NULL, + email TEXT NOT NULL, + hashed_password TEXT NOT NULL + ); + INSERT INTO users (user_id, username, email, hashed_password) + VALUES ('123', 'test_user', 'test@example.com', 'hashed_password_example'); + "#, + ) + .await + .unwrap(); + + // Test the `get_user` function + let user = get_user(&pool, "123").await.unwrap(); + assert_eq!(user.user_id, "123"); + assert_eq!(user.username, "test_user"); + assert_eq!(user.email, "test@example.com"); +} diff --git a/database-service/tests/grpc_get_user.rs b/database-service/tests/grpc_get_user.rs new file mode 100644 index 0000000..8a04183 --- /dev/null +++ b/database-service/tests/grpc_get_user.rs @@ -0,0 +1,25 @@ +use tonic::{Request, Response}; +use database_service::database::database_service_server::DatabaseService; +use database_service::database::GetUserRequest; +use database_service::MyDatabaseService; + +#[tokio::test] +async fn test_grpc_get_user() { + let pool = setup_test_pool().await; // Set up your test pool + let cache = setup_test_cache().await; // Set up mock Redis cache + + let service = MyDatabaseService { pool, cache }; + + // Create a mock gRPC request + let request = Request::new(GetUserRequest { + user_id: 123, + }); + + // Call the service + let response = service.get_user(request).await.unwrap().into_inner(); + + // Validate the response + assert_eq!(response.user_id, 123); + assert_eq!(response.username, "test_user"); + assert_eq!(response.email, "test@example.com"); +} diff --git a/database-service/tests/integration.rs b/database-service/tests/integration.rs new file mode 100644 index 0000000..f791280 --- /dev/null +++ b/database-service/tests/integration.rs @@ -0,0 +1,14 @@ +#[cfg(test)] +mod tests { + use super::*; + use dotenv::dotenv; + use database_service::db::Database; + + #[tokio::test] + async fn test_health_check() { + dotenv().ok(); + let database_url = std::env::var("DATABASE_URL").unwrap(); + let db = Database::new(&database_url).await; + assert!(db.health_check().await); + } +} \ No newline at end of file diff --git a/database-service/tests/redis_cache.rs b/database-service/tests/redis_cache.rs new file mode 100644 index 0000000..0c8b235 --- /dev/null +++ b/database-service/tests/redis_cache.rs @@ -0,0 +1,19 @@ +use deadpool_redis::{Config, Pool, Runtime}; +use redis::AsyncCommands; +use database_service::redis_cache::RedisCache; + +#[tokio::test] +async fn test_redis_cache() { + let redis_url = "redis://127.0.0.1:6379"; + let cache = RedisCache::new(redis_url); + + let key = &"test_key".to_string(); + let value = "test_value"; + + // Test setting a value + cache.set(key, &value, 10).await.unwrap(); + + // Test getting the value + let cached_value: Option = cache.get(key).await.unwrap(); + assert_eq!(cached_value, Some("test_value".to_string())); +} diff --git a/proto/auth.proto b/proto/auth.proto new file mode 100644 index 0000000..4e7126c --- /dev/null +++ b/proto/auth.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package auth; + +service AuthService { + rpc Login(LoginRequest) returns (LoginResponse); + rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse); +} + +message LoginRequest { + string username = 1; + string password = 2; +} + +message LoginResponse { + string token = 1; + string user_id = 2; +} + +message ValidateTokenRequest { + string token = 1; +} + +message ValidateTokenResponse { + bool valid = 1; + string user_id = 2; +} diff --git a/proto/database.proto b/proto/database.proto new file mode 100644 index 0000000..0a43f55 --- /dev/null +++ b/proto/database.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package database; + +service DatabaseService { + rpc GetUser(GetUserRequest) returns (GetUserResponse); + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); + rpc GetUserByUsername(GetUserByUsernameRequest) returns (GetUserResponse); +} + +message GetUserRequest { + int32 user_id = 1; +} + +message GetUserByUsernameRequest { + string username = 1; +} + +message GetUserResponse { + int32 user_id = 1; + string username = 2; + string email = 3; + string hashed_password = 4; +} + +message CreateUserRequest { + string username = 1; + string email = 2; + string hashed_password = 3; +} + +message CreateUserResponse { + int32 user_id = 1; +}