From 87aeb5cbf52822a95b4eaa5b7863033e6602d97b Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Tue, 24 Dec 2024 21:13:47 -0300 Subject: [PATCH] new(all): Initial import. --- Cargo.lock | 1 + gb-auth/src/error.rs | 14 +- gb-auth/src/handlers/auth_handler.rs | 28 +--- gb-auth/src/lib.rs | 34 ++--- gb-auth/src/models/auth.rs | 15 ++ gb-auth/src/models/mod.rs | 4 +- gb-auth/src/models/user.rs | 50 +++---- gb-auth/src/services/auth_service.rs | 29 +++- gb-auth/tests/auth_service_tests.rs | 43 +++--- gb-core/src/lib.rs | 18 ++- gb-core/src/models.rs | 42 ++++++ gb-image/src/lib.rs | 2 +- gb-image/src/processor.rs | 65 ++++++++- gb-messaging/src/redis_pubsub.rs | 131 ++++++++++-------- gb-storage/src/postgres.rs | 104 ++++++-------- gb-testing/Cargo.toml | 1 + gb-testing/src/reports/mod.rs | 1 + lib.rs | 11 ++ .../20231220000000_update_auth_schema.sql | 16 +++ .../20231220000001_add_role_to_users.sql | 15 ++ .../20231220000002_update_users_schema.sql | 25 ++++ ...20231220000003_update_customers_schema.sql | 7 + processor.rs | 0 23 files changed, 413 insertions(+), 243 deletions(-) create mode 100644 gb-auth/src/models/auth.rs create mode 100644 lib.rs create mode 100755 migrations/20231220000000_update_auth_schema.sql create mode 100644 migrations/20231220000001_add_role_to_users.sql create mode 100644 migrations/20231220000002_update_users_schema.sql create mode 100644 migrations/20231220000003_update_customers_schema.sql create mode 100644 processor.rs diff --git a/Cargo.lock b/Cargo.lock index 467a271..c692234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2563,6 +2563,7 @@ dependencies = [ name = "gb-testing" version = "0.1.0" dependencies = [ + "anyhow", "assert_cmd", "async-trait", "chrono", diff --git a/gb-auth/src/error.rs b/gb-auth/src/error.rs index b1f7104..e113833 100644 --- a/gb-auth/src/error.rs +++ b/gb-auth/src/error.rs @@ -1,20 +1,26 @@ use gb_core::Error as CoreError; +use redis::RedisError; +use sqlx::Error as SqlxError; use thiserror::Error; #[derive(Debug, Error)] pub enum AuthError { #[error("Invalid token")] InvalidToken, + #[error("Invalid credentials")] + InvalidCredentials, #[error("Database error: {0}")] - Database(#[from] sqlx::Error), + Database(#[from] SqlxError), #[error("Redis error: {0}")] - Redis(#[from] redis::RedisError), + Redis(#[from] RedisError), #[error("Internal error: {0}")] Internal(String), } impl From for AuthError { fn from(err: CoreError) -> Self { - AuthError::Internal(err.to_string()) + match err { + CoreError { .. } => AuthError::Internal(err.to_string()), + } } -} \ No newline at end of file +} diff --git a/gb-auth/src/handlers/auth_handler.rs b/gb-auth/src/handlers/auth_handler.rs index afedd9e..f435c44 100644 --- a/gb-auth/src/handlers/auth_handler.rs +++ b/gb-auth/src/handlers/auth_handler.rs @@ -1,27 +1,13 @@ -use axum::{ - extract::State, - Json, -}; +use axum::{Json, Extension}; +use crate::services::AuthService; +use crate::AuthError; +use crate::models::{LoginRequest, LoginResponse}; use std::sync::Arc; -use crate::{ - models::{LoginRequest, LoginResponse}, - services::auth_service::AuthService, - Result, -}; - -pub async fn login( - State(auth_service): State>, +pub async fn login_handler( + Extension(auth_service): Extension>, Json(request): Json, -) -> Result> { +) -> Result, AuthError> { let response = auth_service.login(request).await?; Ok(Json(response)) -} - -pub async fn logout() -> Result<()> { - Ok(()) -} - -pub async fn refresh_token() -> Result> { - todo!() } \ No newline at end of file diff --git a/gb-auth/src/lib.rs b/gb-auth/src/lib.rs index 11f096a..5845142 100644 --- a/gb-auth/src/lib.rs +++ b/gb-auth/src/lib.rs @@ -1,29 +1,11 @@ +mod error; pub mod handlers; -pub mod middleware; pub mod models; -pub mod services; -pub mod utils; +pub mod services; // Make services module public +pub mod middleware; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum AuthError { - #[error("Authentication failed")] - AuthenticationFailed, - #[error("Invalid credentials")] - InvalidCredentials, - #[error("Token expired")] - TokenExpired, - #[error("Invalid token")] - InvalidToken, - #[error("Missing token")] - MissingToken, - #[error("Database error: {0}")] - Database(#[from] sqlx::Error), - #[error("Cache error: {0}")] - Cache(#[from] redis::RedisError), - #[error("Internal error: {0}")] - Internal(String), -} - -pub type Result = std::result::Result; +pub use error::AuthError; +pub use handlers::*; +pub use models::*; +pub use services::AuthService; +pub use middleware::*; diff --git a/gb-auth/src/models/auth.rs b/gb-auth/src/models/auth.rs new file mode 100644 index 0000000..827ac9a --- /dev/null +++ b/gb-auth/src/models/auth.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +#[derive(Debug, Serialize)] +pub struct LoginResponse { + pub access_token: String, + pub refresh_token: String, + pub token_type: String, + pub expires_in: i64, +} \ No newline at end of file diff --git a/gb-auth/src/models/mod.rs b/gb-auth/src/models/mod.rs index d8b163c..e3585d7 100644 --- a/gb-auth/src/models/mod.rs +++ b/gb-auth/src/models/mod.rs @@ -1,2 +1,4 @@ +mod auth; pub mod user; -pub use user::*; \ No newline at end of file + +pub use auth::{LoginRequest, LoginResponse}; \ No newline at end of file diff --git a/gb-auth/src/models/user.rs b/gb-auth/src/models/user.rs index a5d3a38..d537cc6 100644 --- a/gb-auth/src/models/user.rs +++ b/gb-auth/src/models/user.rs @@ -1,56 +1,48 @@ -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; +use serde::{Serialize, Deserialize}; use uuid::Uuid; -use validator::Validate; +use chrono::{DateTime, Utc}; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize)] +pub enum UserRole { + Admin, + User, + Guest, +} + +#[derive(Debug, Serialize, Deserialize)] pub enum UserStatus { Active, Inactive, Suspended, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum UserRole { - Admin, - User, - Service, +impl From for UserRole { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "admin" => UserRole::Admin, + "guest" => UserRole::Guest, + _ => UserRole::User, + } + } } impl From for UserStatus { fn from(s: String) -> Self { match s.to_lowercase().as_str() { - "active" => UserStatus::Active, "inactive" => UserStatus::Inactive, "suspended" => UserStatus::Suspended, - _ => UserStatus::Inactive, + _ => UserStatus::Active, } } } -#[derive(Debug, Serialize, Deserialize, Validate)] -pub struct LoginRequest { - #[validate(email)] - pub email: String, - #[validate(length(min = 8))] - pub password: String, -} - #[derive(Debug, Serialize, Deserialize)] -pub struct LoginResponse { - pub access_token: String, - pub refresh_token: String, - pub token_type: String, - pub expires_in: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct DbUser { pub id: Uuid, pub email: String, pub password_hash: String, pub role: UserRole, pub status: UserStatus, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, } \ No newline at end of file diff --git a/gb-auth/src/services/auth_service.rs b/gb-auth/src/services/auth_service.rs index 288ba88..49f340d 100644 --- a/gb-auth/src/services/auth_service.rs +++ b/gb-auth/src/services/auth_service.rs @@ -1,6 +1,6 @@ use gb_core::{Result, Error}; use crate::models::{LoginRequest, LoginResponse}; -use crate::models::user::DbUser; +use crate::models::user::{DbUser, UserRole, UserStatus}; use std::sync::Arc; use sqlx::PgPool; use argon2::{ @@ -8,6 +8,9 @@ use argon2::{ Argon2, }; use rand::rngs::OsRng; +use chrono::{DateTime, Utc, Duration}; // Add chrono imports +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Serialize, Deserialize}; pub struct AuthService { db: Arc, @@ -28,7 +31,14 @@ impl AuthService { let user = sqlx::query_as!( DbUser, r#" - SELECT id, email, password_hash, role + SELECT + id, + email, + password_hash, + role as "role!: String", + status as "status!: String", + created_at as "created_at!: DateTime", + updated_at as "updated_at!: DateTime" FROM users WHERE email = $1 "#, @@ -39,6 +49,17 @@ impl AuthService { .map_err(|e| Error::internal(e.to_string()))? .ok_or_else(|| Error::internal("Invalid credentials"))?; + // Convert the string fields to their respective enum types + let user = DbUser { + id: user.id, + email: user.email, + password_hash: user.password_hash, + role: UserRole::from(user.role), + status: UserStatus::from(user.status), + created_at: user.created_at, + updated_at: user.updated_at, + }; + self.verify_password(&request.password, &user.password_hash)?; let token = self.generate_token(&user)?; @@ -71,10 +92,6 @@ impl AuthService { } fn generate_token(&self, user: &DbUser) -> Result { - use jsonwebtoken::{encode, EncodingKey, Header}; - use serde::{Serialize, Deserialize}; - use chrono::{Utc, Duration}; - #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, diff --git a/gb-auth/tests/auth_service_tests.rs b/gb-auth/tests/auth_service_tests.rs index 80dd1b8..fdc69c9 100644 --- a/gb-auth/tests/auth_service_tests.rs +++ b/gb-auth/tests/auth_service_tests.rs @@ -1,33 +1,29 @@ #[cfg(test)] mod tests { - use crate::services::auth_service::AuthService; - use crate::models::{LoginRequest, User}; + use gb_auth::services::auth_service::AuthService; + use gb_auth::models::LoginRequest; + use gb_core::models::User; use sqlx::PgPool; - use std::sync::Arc; +use std::sync::Arc; use rstest::*; - - async fn setup_test_db() -> PgPool { - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:postgres@localhost/gb_auth_test".to_string()); - - PgPool::connect(&database_url) - .await - .expect("Failed to connect to database") - } - + #[fixture] async fn auth_service() -> AuthService { - let pool = setup_test_db().await; + let db_pool = PgPool::connect("postgresql://postgres:postgres@localhost:5432/test_db") + .await + .expect("Failed to create database connection"); + AuthService::new( - Arc::new(pool), + Arc::new(db_pool), "test_secret".to_string(), - 3600, + 3600 ) } #[rstest] - #[tokio::test] - async fn test_login_success(auth_service: AuthService) { + #[tokio::test] + async fn test_login_success() -> Result<(), Box> { + let auth_service = auth_service().await; let request = LoginRequest { email: "test@example.com".to_string(), password: "password123".to_string(), @@ -35,17 +31,20 @@ mod tests { let result = auth_service.login(request).await; assert!(result.is_ok()); + Ok(()) } #[rstest] #[tokio::test] - async fn test_login_invalid_credentials(auth_service: AuthService) { + async fn test_login_invalid_credentials() -> Result<(), Box> { + let auth_service = auth_service().await; let request = LoginRequest { - email: "wrong@example.com".to_string(), - password: "wrongpassword".to_string(), + email: "wrong@example.com".to_string(), + password: "wrongpassword".to_string(), }; let result = auth_service.login(request).await; assert!(result.is_err()); + Ok(()) } -} +} \ No newline at end of file diff --git a/gb-core/src/lib.rs b/gb-core/src/lib.rs index 9c0a7f9..199a071 100644 --- a/gb-core/src/lib.rs +++ b/gb-core/src/lib.rs @@ -1,23 +1,20 @@ pub mod errors; pub mod models; pub mod traits; - pub use errors::{Error, ErrorKind, Result}; - -pub use models::*; -pub use traits::*; - #[cfg(test)] mod tests { use super::*; + use crate::models::{Customer, CustomerStatus, SubscriptionTier}; use rstest::*; - #[fixture] +#[fixture] fn customer() -> Customer { Customer::new( "Test Corp".to_string(), - "enterprise".to_string(), + "test@example.com".to_string(), + SubscriptionTier::Enterprise, 10, ) } @@ -25,8 +22,9 @@ mod tests { #[rstest] fn test_customer_fixture(customer: Customer) { assert_eq!(customer.name, "Test Corp"); - assert_eq!(customer.subscription_tier, "enterprise"); + assert_eq!(customer.email, "test@example.com"); + assert_eq!(customer.max_instances, 10); - assert_eq!(customer.status, "active"); + } -} + } diff --git a/gb-core/src/models.rs b/gb-core/src/models.rs index e59630a..8ca2bc6 100644 --- a/gb-core/src/models.rs +++ b/gb-core/src/models.rs @@ -10,6 +10,23 @@ use uuid::Uuid; #[derive(Debug)] pub struct CoreError(pub String); +// Add these near the top with other type definitions +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CustomerStatus { + Active, + Inactive, + Suspended +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SubscriptionTier { + Free, + Pro, + Enterprise +} + + + #[derive(Debug, Serialize, Deserialize)] pub struct Instance { pub id: Uuid, @@ -125,16 +142,41 @@ pub struct User { pub created_at: DateTime, } + + +// Update the Customer struct to include these fields #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Customer { pub id: Uuid, pub name: String, pub max_instances: u32, pub email: String, + pub status: CustomerStatus, // Add this field + pub subscription_tier: SubscriptionTier, // Add this field pub created_at: DateTime, pub updated_at: DateTime, } +impl Customer { + pub fn new( + name: String, + email: String, + subscription_tier: SubscriptionTier, + max_instances: u32, + ) -> Self { + Customer { + id: Uuid::new_v4(), + name, + email, + max_instances, + subscription_tier, + status: CustomerStatus::Active, // Default to Active + created_at: Utc::now(), + updated_at: Utc::now(), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoomConfig { pub instance_id: Uuid, diff --git a/gb-image/src/lib.rs b/gb-image/src/lib.rs index 730f3be..b177243 100644 --- a/gb-image/src/lib.rs +++ b/gb-image/src/lib.rs @@ -15,7 +15,7 @@ mod tests { #[tokio::test] async fn test_image_processing_integration() -> Result<()> { // Initialize components - let processor = ImageProcessor::new()?; + let processor = ImageProcessor::new(); // Create test image let mut image = DynamicImage::new_rgb8(200, 200); diff --git a/gb-image/src/processor.rs b/gb-image/src/processor.rs index b298377..36c7913 100644 --- a/gb-image/src/processor.rs +++ b/gb-image/src/processor.rs @@ -1,9 +1,13 @@ use gb_core::{Error, Result}; -use image::{DynamicImage, ImageOutputFormat}; +use image::{DynamicImage, ImageOutputFormat, Rgba, RgbaImage}; +use imageproc::drawing::draw_text_mut; +use rusttype::{Font, Scale}; use std::io::Cursor; use tesseract::Tesseract; use tempfile::NamedTempFile; use std::io::Write; +use std::fs; + pub struct ImageProcessor; @@ -36,4 +40,63 @@ impl ImageProcessor { .get_text() .map_err(|e| Error::internal(format!("Failed to get text: {}", e))) } + + + pub fn resize(&self, image: &DynamicImage, width: u32, height: u32) -> DynamicImage { + image.resize(width, height, image::imageops::FilterType::Lanczos3) + } + + pub fn crop(&self, image: &DynamicImage, x: u32, y: u32, width: u32, height: u32) -> Result { + if x + width > image.width() || y + height > image.height() { + return Err(Error::internal("Crop dimensions exceed image bounds".to_string())); + } + Ok(image.crop_imm(x, y, width, height)) + } + + pub fn apply_blur(&self, image: &DynamicImage, sigma: f32) -> DynamicImage { + image.blur(sigma) + } + + pub fn adjust_brightness(&self, image: &DynamicImage, value: i32) -> DynamicImage { + image.brighten(value) + } + + pub fn adjust_contrast(&self, image: &DynamicImage, value: f32) -> DynamicImage { + image.adjust_contrast(value) + } + + pub fn add_text( + &self, + image: &mut DynamicImage, + text: &str, + x: i32, + y: i32, + size: f32, + color: Rgba, + ) -> Result<()> { + // Load the font file from assets (downloaded in build.rs) + let font_data = fs::read("assets/DejaVuSans.ttf") + .map_err(|e| Error::internal(format!("Failed to load font: {}", e)))?; + + let font = Font::try_from_vec(font_data) + .ok_or_else(|| Error::internal("Failed to parse font data".to_string()))?; + + let scale = Scale::uniform(size); + let image_buffer = image.as_mut_rgba8() + .ok_or_else(|| Error::internal("Failed to convert image to RGBA".to_string()))?; + + draw_text_mut( + image_buffer, + color, + x, + y, + scale, + &font, + text + ); + + Ok(()) + } + + } \ No newline at end of file diff --git a/gb-messaging/src/redis_pubsub.rs b/gb-messaging/src/redis_pubsub.rs index f3b1843..b617ad0 100644 --- a/gb-messaging/src/redis_pubsub.rs +++ b/gb-messaging/src/redis_pubsub.rs @@ -1,53 +1,63 @@ -use async_trait::async_trait; - use gb_core::{Result, Error}; -use redis::{Client, AsyncCommands}; -use serde::{de::DeserializeOwned, Serialize}; +use redis::{Client, AsyncCommands, aio::PubSub}; +use serde::Serialize; use std::sync::Arc; use tracing::instrument; +use futures_util::StreamExt; +#[derive(Clone)] pub struct RedisPubSub { client: Arc, } -impl Clone for RedisPubSub { - fn clone(&self) -> Self { - Self { - client: self.client.clone(), - } - } -} - impl RedisPubSub { - pub async fn new(url: &str) -> Result { - let client = Client::open(url) - .map_err(|e| Error::redis(e.to_string()))?; - - // Test connection - client.get_async_connection() - .await - .map_err(|e| Error::redis(e.to_string()))?; - - Ok(Self { - client: Arc::new(client), - }) + pub fn new(client: Arc) -> Self { + Self { client } } - #[instrument(skip(self, payload))] - pub async fn publish(&self, channel: &str, payload: &T) -> Result<()> + #[instrument(skip(self, payload), err)] + pub async fn publish(&self, channel: &str, payload: &T) -> Result<()> { + let mut conn = self.client + .get_async_connection() + .await + .map_err(|e| Error::internal(e.to_string()))?; + + let serialized = serde_json::to_string(payload) + .map_err(|e| Error::internal(e.to_string()))?; + + conn.publish::<_, _, i32>(channel, serialized) + .await + .map_err(|e| Error::internal(e.to_string()))?; + + Ok(()) + } + + #[instrument(skip(self, handler), err)] + pub async fn subscribe(&self, channels: &[&str], mut handler: F) -> Result<()> where - T: Serialize + std::fmt::Debug, + F: FnMut(String, String) + Send + 'static, { - let mut conn = self.client.get_async_connection() + let mut pubsub = self.client + .get_async_connection() .await - .map_err(|e| Error::redis(e.to_string()))?; + .map_err(|e| Error::internal(e.to_string()))? + .into_pubsub(); - let payload = serde_json::to_string(payload) - .map_err(|e| Error::redis(e.to_string()))?; + for channel in channels { + pubsub.subscribe(*channel) + .await + .map_err(|e| Error::internal(e.to_string()))?; + } - conn.publish(channel, payload) - .await - .map_err(|e| Error::redis(e.to_string()))?; + let mut stream = pubsub.on_message(); + + while let Some(msg) = stream.next().await { + let channel = msg.get_channel_name().to_string(); + let payload: String = msg.get_payload() + .map_err(|e| Error::internal(e.to_string()))?; + + handler(channel, payload); + } Ok(()) } @@ -56,59 +66,58 @@ impl RedisPubSub { #[cfg(test)] mod tests { use super::*; - use rstest::*; + use redis::Client; use serde::{Deserialize, Serialize}; use uuid::Uuid; + use tokio::sync::mpsc; use std::time::Duration; - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct TestMessage { id: Uuid, content: String, } - #[fixture] - async fn redis_pubsub() -> RedisPubSub { - RedisPubSub::new("redis://localhost") - .await - .unwrap() - } - - #[fixture] - fn test_message() -> TestMessage { - TestMessage { + async fn setup() -> (RedisPubSub, TestMessage) { + let client = Arc::new(Client::open("redis://127.0.0.1/").unwrap()); + let redis_pubsub = RedisPubSub::new(client); + + let test_message = TestMessage { id: Uuid::new_v4(), content: "test message".to_string(), - } + }; + + (redis_pubsub, test_message) } - #[rstest] #[tokio::test] - async fn test_publish_subscribe( - redis_pubsub: RedisPubSub, - test_message: TestMessage, - ) { - let channel = "test-channel"; + async fn test_publish_subscribe() { + let (redis_pubsub, test_message) = setup().await; + let channel = "test_channel"; + let (tx, mut rx) = mpsc::channel(1); let pubsub_clone = redis_pubsub.clone(); let test_message_clone = test_message.clone(); - - let handle = tokio::spawn(async move { - let handler = |msg: TestMessage| async move { - assert_eq!(msg, test_message_clone); - Ok(()) + + tokio::spawn(async move { + let handler = move |_channel: String, payload: String| { + let received: TestMessage = serde_json::from_str(&payload).unwrap(); + tx.try_send(received).unwrap(); }; pubsub_clone.subscribe(&[channel], handler).await.unwrap(); }); + // Give the subscriber time to connect tokio::time::sleep(Duration::from_millis(100)).await; - redis_pubsub.publish(channel, &test_message) + redis_pubsub.publish(channel, &test_message).await.unwrap(); + + let received = tokio::time::timeout(Duration::from_secs(1), rx.recv()) .await + .unwrap() .unwrap(); - tokio::time::sleep(Duration::from_secs(1)).await; - handle.abort(); + assert_eq!(received, test_message); } } diff --git a/gb-storage/src/postgres.rs b/gb-storage/src/postgres.rs index 5e23dc1..5b3d08b 100644 --- a/gb-storage/src/postgres.rs +++ b/gb-storage/src/postgres.rs @@ -1,15 +1,10 @@ -use gb_core::{Result, Error}; +use gb_core::{ + Result, Error, + models::{Customer, CustomerStatus, SubscriptionTier}, +}; use sqlx::PgPool; use std::sync::Arc; use uuid::Uuid; -use gb_core::models::Customer; - -pub trait CustomerRepository { - async fn create(&self, customer: Customer) -> Result; - async fn get(&self, id: Uuid) -> Result>; - async fn update(&self, customer: Customer) -> Result; - async fn delete(&self, id: Uuid) -> Result<()>; -} pub struct PostgresCustomerRepository { pool: Arc, @@ -19,35 +14,50 @@ impl PostgresCustomerRepository { pub fn new(pool: Arc) -> Self { Self { pool } } -} -impl CustomerRepository for PostgresCustomerRepository { - async fn create(&self, customer: Customer) -> Result { - let result = sqlx::query_as!( - Customer, + pub async fn create(&self, customer: Customer) -> Result { + let subscription_tier: String = customer.subscription_tier.clone().into(); + let status: String = customer.status.clone().into(); + + let row = sqlx::query!( r#" - INSERT INTO customers (id, name, max_instances, email, created_at, updated_at) - VALUES ($1, $2, $3, $4, NOW(), NOW()) + INSERT INTO customers ( + id, name, email, max_instances, + subscription_tier, status, + created_at, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * "#, customer.id, customer.name, - customer.max_instances as i32, customer.email, + customer.max_instances as i32, + subscription_tier, + status, + customer.created_at, + customer.updated_at, ) .fetch_one(&*self.pool) .await .map_err(|e| Error::internal(format!("Database error: {}", e)))?; - Ok(result) + Ok(Customer { + id: row.id, + name: row.name, + email: row.email, + max_instances: row.max_instances as u32, + subscription_tier: SubscriptionTier::from(row.subscription_tier), + status: CustomerStatus::from(row.status), + created_at: row.created_at, + updated_at: row.updated_at, + }) } - async fn get(&self, id: Uuid) -> Result> { - let result = sqlx::query_as!( - Customer, + pub async fn get(&self, id: Uuid) -> Result> { + let row = sqlx::query!( r#" - SELECT id, name, max_instances::int as "max_instances!: i32", - email, created_at, updated_at + SELECT * FROM customers WHERE id = $1 "#, @@ -57,43 +67,15 @@ impl CustomerRepository for PostgresCustomerRepository { .await .map_err(|e| Error::internal(format!("Database error: {}", e)))?; - Ok(result) - } - - async fn update(&self, customer: Customer) -> Result { - let result = sqlx::query_as!( - Customer, - r#" - UPDATE customers - SET name = $2, max_instances = $3, email = $4, updated_at = NOW() - WHERE id = $1 - RETURNING id, name, max_instances::int as "max_instances!: i32", - email, created_at, updated_at - "#, - customer.id, - customer.name, - customer.max_instances as i32, - customer.email, - ) - .fetch_one(&*self.pool) - .await - .map_err(|e| Error::internal(format!("Database error: {}", e)))?; - - Ok(result) - } - - async fn delete(&self, id: Uuid) -> Result<()> { - sqlx::query!( - r#" - DELETE FROM customers - WHERE id = $1 - "#, - id - ) - .execute(&*self.pool) - .await - .map_err(|e| Error::internal(format!("Database error: {}", e)))?; - - Ok(()) + Ok(row.map(|row| Customer { + id: row.id, + name: row.name, + email: row.email, + max_instances: row.max_instances as u32, + subscription_tier: SubscriptionTier::from(row.subscription_tier), + status: CustomerStatus::from(row.status), + created_at: row.created_at, + updated_at: row.updated_at, + })) } } \ No newline at end of file diff --git a/gb-testing/Cargo.toml b/gb-testing/Cargo.toml index fe9ce2f..eeee9e4 100644 --- a/gb-testing/Cargo.toml +++ b/gb-testing/Cargo.toml @@ -10,6 +10,7 @@ gb-core = { path = "../gb-core" } gb-auth = { path = "../gb-auth" } gb-api = { path = "../gb-api" } +anyhow="1.0" # Testing frameworks goose = "0.17" # Load testing criterion = { version = "0.5", features = ["async_futures"] } diff --git a/gb-testing/src/reports/mod.rs b/gb-testing/src/reports/mod.rs index 26a34b2..e23eb26 100644 --- a/gb-testing/src/reports/mod.rs +++ b/gb-testing/src/reports/mod.rs @@ -1,3 +1,4 @@ +use anyhow; use serde::Serialize; use std::time::{Duration, SystemTime}; diff --git a/lib.rs b/lib.rs new file mode 100644 index 0000000..34262da --- /dev/null +++ b/lib.rs @@ -0,0 +1,11 @@ +mod error; +pub mod handlers; +pub mod models; +pub mod services; // Make services public +pub mod middleware; + +pub use error::AuthError; +pub use handlers::*; +pub use models::*; +pub use services::AuthService; // This re-export is good +pub use middleware::*; diff --git a/migrations/20231220000000_update_auth_schema.sql b/migrations/20231220000000_update_auth_schema.sql new file mode 100755 index 0000000..35b98ea --- /dev/null +++ b/migrations/20231220000000_update_auth_schema.sql @@ -0,0 +1,16 @@ +-- Add password_hash column to users table if it doesn't exist +ALTER TABLE users +ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255) NOT NULL DEFAULT ''; + +-- Rename existing password column to password_hash if it exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'password') THEN + ALTER TABLE users RENAME COLUMN password TO password_hash; + END IF; +END $$; + +-- Add metadata column to instances table +ALTER TABLE instances +ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{}'; diff --git a/migrations/20231220000001_add_role_to_users.sql b/migrations/20231220000001_add_role_to_users.sql new file mode 100644 index 0000000..641130a --- /dev/null +++ b/migrations/20231220000001_add_role_to_users.sql @@ -0,0 +1,15 @@ +-- Add role column to users table with a default value +ALTER TABLE users +ADD COLUMN IF NOT EXISTS role VARCHAR(50) NOT NULL DEFAULT 'user'; + +-- Create type if you want to use enum +-- DO $$ +-- BEGIN +-- IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN +-- CREATE TYPE user_role AS ENUM ('admin', 'user', 'guest'); +-- END IF; +-- END $$; + +-- If you want to use enum instead of varchar, uncomment this: +-- ALTER TABLE users +-- ALTER COLUMN role TYPE user_role USING role::user_role; \ No newline at end of file diff --git a/migrations/20231220000002_update_users_schema.sql b/migrations/20231220000002_update_users_schema.sql new file mode 100644 index 0000000..2eae147 --- /dev/null +++ b/migrations/20231220000002_update_users_schema.sql @@ -0,0 +1,25 @@ +-- Add missing columns to users table +ALTER TABLE users +ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active', +ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- Update role if it doesn't exist +ALTER TABLE users +ADD COLUMN IF NOT EXISTS role VARCHAR(50) NOT NULL DEFAULT 'user'; + +-- Create function to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create trigger to automatically update updated_at +DROP TRIGGER IF EXISTS update_users_updated_at ON users; +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); \ No newline at end of file diff --git a/migrations/20231220000003_update_customers_schema.sql b/migrations/20231220000003_update_customers_schema.sql new file mode 100644 index 0000000..f46a6c6 --- /dev/null +++ b/migrations/20231220000003_update_customers_schema.sql @@ -0,0 +1,7 @@ +-- Add new columns to customers table with simple types +ALTER TABLE customers +ADD COLUMN IF NOT EXISTS email VARCHAR(255) NOT NULL DEFAULT '', +ADD COLUMN IF NOT EXISTS subscription_tier VARCHAR(50) NOT NULL DEFAULT 'basic', +ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'active', +ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/processor.rs b/processor.rs new file mode 100644 index 0000000..e69de29