new(all): Initial import.

This commit is contained in:
Rodrigo Rodriguez 2024-12-24 21:13:47 -03:00
parent 28cc734340
commit 87aeb5cbf5
23 changed files with 413 additions and 243 deletions

1
Cargo.lock generated
View file

@ -2563,6 +2563,7 @@ dependencies = [
name = "gb-testing"
version = "0.1.0"
dependencies = [
"anyhow",
"assert_cmd",
"async-trait",
"chrono",

View file

@ -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<CoreError> for AuthError {
fn from(err: CoreError) -> Self {
AuthError::Internal(err.to_string())
match err {
CoreError { .. } => AuthError::Internal(err.to_string()),
}
}
}
}

View file

@ -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<Arc<AuthService>>,
pub async fn login_handler(
Extension(auth_service): Extension<Arc<AuthService>>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>> {
) -> Result<Json<LoginResponse>, AuthError> {
let response = auth_service.login(request).await?;
Ok(Json(response))
}
pub async fn logout() -> Result<()> {
Ok(())
}
pub async fn refresh_token() -> Result<Json<LoginResponse>> {
todo!()
}

View file

@ -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<T> = std::result::Result<T, AuthError>;
pub use error::AuthError;
pub use handlers::*;
pub use models::*;
pub use services::AuthService;
pub use middleware::*;

View file

@ -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,
}

View file

@ -1,2 +1,4 @@
mod auth;
pub mod user;
pub use user::*;
pub use auth::{LoginRequest, LoginResponse};

View file

@ -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<String> for UserRole {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"admin" => UserRole::Admin,
"guest" => UserRole::Guest,
_ => UserRole::User,
}
}
}
impl From<String> 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<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}

View file

@ -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<PgPool>,
@ -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<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
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<String> {
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Serialize, Deserialize};
use chrono::{Utc, Duration};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,

View file

@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}
}
}

View file

@ -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");
}
}
}

View file

@ -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<Utc>,
}
// 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<Utc>,
pub updated_at: DateTime<Utc>,
}
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,

View file

@ -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);

View file

@ -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<DynamicImage> {
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<u8>,
) -> 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(())
}
}

View file

@ -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<Client>,
}
impl Clone for RedisPubSub {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
}
}
}
impl RedisPubSub {
pub async fn new(url: &str) -> Result<Self> {
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<Client>) -> Self {
Self { client }
}
#[instrument(skip(self, payload))]
pub async fn publish<T>(&self, channel: &str, payload: &T) -> Result<()>
#[instrument(skip(self, payload), err)]
pub async fn publish<T: Serialize>(&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<F>(&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);
}
}

View file

@ -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<Customer>;
async fn get(&self, id: Uuid) -> Result<Option<Customer>>;
async fn update(&self, customer: Customer) -> Result<Customer>;
async fn delete(&self, id: Uuid) -> Result<()>;
}
pub struct PostgresCustomerRepository {
pool: Arc<PgPool>,
@ -19,35 +14,50 @@ impl PostgresCustomerRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
}
impl CustomerRepository for PostgresCustomerRepository {
async fn create(&self, customer: Customer) -> Result<Customer> {
let result = sqlx::query_as!(
Customer,
pub async fn create(&self, customer: Customer) -> Result<Customer> {
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<Option<Customer>> {
let result = sqlx::query_as!(
Customer,
pub async fn get(&self, id: Uuid) -> Result<Option<Customer>> {
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<Customer> {
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,
}))
}
}

View file

@ -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"] }

View file

@ -1,3 +1,4 @@
use anyhow;
use serde::Serialize;
use std::time::{Duration, SystemTime};

11
lib.rs Normal file
View file

@ -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::*;

View file

@ -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 '{}';

View file

@ -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;

View file

@ -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();

View file

@ -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;

0
processor.rs Normal file
View file