diff --git a/Cargo.lock b/Cargo.lock index 4dc7a4e..80cbfe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,19 +434,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" -[[package]] -name = "bcrypt" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" -dependencies = [ - "base64", - "blowfish", - "getrandom 0.2.16", - "subtle", - "zeroize", -] - [[package]] name = "bigdecimal" version = "0.4.9" @@ -515,16 +502,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blowfish" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" -dependencies = [ - "byteorder", - "cipher", -] - [[package]] name = "borsh" version = "1.5.7" @@ -660,16 +637,6 @@ dependencies = [ "stacker", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -1942,12 +1909,10 @@ version = "0.2.0" dependencies = [ "async-trait", "axum", - "axum-extra", "base64", "chrono", "imphnen-libs", "imphnen-utils", - "jsonwebtoken", "lettre", "reqwest", "sea-orm", @@ -2082,15 +2047,11 @@ version = "0.2.0" dependencies = [ "async-trait", "axum", - "axum-extra", - "bcrypt", "chrono", "image", + "imphnen-libs", "imphnen-utils", - "jsonwebtoken", - "oauth2", "qrcode", - "reqwest", "serde", "serde_json", "sqlx", @@ -2154,15 +2115,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "interpolate_name" version = "0.2.4" diff --git a/imphnen-gateway/src/lib.rs b/imphnen-gateway/src/lib.rs index 0ea742b..028f08a 100644 --- a/imphnen-gateway/src/lib.rs +++ b/imphnen-gateway/src/lib.rs @@ -17,7 +17,8 @@ use imphnen_dimentorin::{ }; use imphnen_gacha::gacha_router; use imphnen_hackathon::{hackathon_router, HackathonConfig}; -use imphnen_qr::{qr_router, QrConfig}; +use imphnen_qr::qr_router; +use imphnen_libs::{MinioConfig, create_minio_service_from_config}; use imphnen_iam::{ auth_public_routes, permissions_protected_routes, @@ -46,7 +47,11 @@ pub async fn gateway_service( let db = state.postgres_connection.conn.clone(); let state_arc = Arc::new(state.clone()); let hackathon_config = Arc::new(HackathonConfig::from_env()); - let qr_config = Arc::new(QrConfig::from_env()); + let minio = Arc::new( + create_minio_service_from_config(MinioConfig::from_env().expect("MinIO config required")) + .await + .expect("Failed to create MinIO service"), + ); let qr_pool = Arc::new( sqlx::PgPool::connect( &std::env::var("QR_DATABASE_URL").expect("QR_DATABASE_URL must be set"), @@ -76,8 +81,8 @@ pub async fn gateway_service( Router::new() .route("/", get(Redirect::to("/docs"))) .nest("/v1", public_routes.merge(protected_routes)) - .nest("/v1/hackathon", hackathon_router(db.clone(), hackathon_config)) - .nest("/v1/qr", qr_router(qr_pool, qr_config)) + .nest("/v1/hackathon", hackathon_router(db.clone(), hackathon_config, minio)) + .nest("/v1/qr", qr_router(qr_pool)) .merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router())) .layer(cors_middleware()) .layer(from_fn(security_headers_middleware)) diff --git a/imphnen-hackathon/Cargo.toml b/imphnen-hackathon/Cargo.toml index 325d5c7..5699f95 100644 --- a/imphnen-hackathon/Cargo.toml +++ b/imphnen-hackathon/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" imphnen-utils.workspace = true imphnen-libs.workspace = true axum.workspace = true -axum-extra.workspace = true sea-orm.workspace = true sqlx.workspace = true async-trait.workspace = true @@ -20,6 +19,5 @@ tokio.workspace = true reqwest.workspace = true lettre.workspace = true base64.workspace = true -jsonwebtoken.workspace = true tracing.workspace = true thiserror.workspace = true diff --git a/imphnen-hackathon/src/admin/routes.rs b/imphnen-hackathon/src/admin/routes.rs index 7b22292..588dae3 100644 --- a/imphnen-hackathon/src/admin/routes.rs +++ b/imphnen-hackathon/src/admin/routes.rs @@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use imphnen_utils::{errors::AppError, response_format::{ApiSuccess, ApiMessage}}; use crate::middleware::{admin_only::admin_only, hackathon_auth::hackathon_auth_middleware}; -use crate::common::hackathon_jwt::HackathonJwtService; #[derive(Deserialize)] struct PageQuery { @@ -173,7 +172,7 @@ async fn admin_list_winners( Ok(ApiSuccess(rows).into_response()) } -pub fn hackathon_admin_routes(pool: Arc, jwt: Arc) -> Router { +pub fn hackathon_admin_routes(pool: Arc) -> Router { Router::new() .route("/admin/users", get(admin_list_users)) .route("/admin/users/:user_id", get(admin_get_user).delete(admin_delete_user)) @@ -185,7 +184,6 @@ pub fn hackathon_admin_routes(pool: Arc, jwt: Arc) .route("/admin/winners/:team_id", delete(admin_remove_winner)) .layer(Extension(pool.clone())) .layer(from_fn(admin_only)) - .layer(Extension(jwt.clone())) .layer(Extension(pool)) .layer(from_fn(hackathon_auth_middleware)) } diff --git a/imphnen-hackathon/src/auth/application/auth_service.rs b/imphnen-hackathon/src/auth/application/auth_service.rs deleted file mode 100644 index bd64c68..0000000 --- a/imphnen-hackathon/src/auth/application/auth_service.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::sync::Arc; -use uuid::Uuid; -use chrono::{Utc, TimeZone}; -use sqlx::PgPool; -use async_trait::async_trait; -use imphnen_utils::errors::AppError; -use crate::common::hackathon_jwt::HackathonJwtService; -use crate::common::supabase_client::SupabaseClient; -use crate::config::HackathonConfig; -use super::super::domain::service::{HackathonAuthService, AuthTokens, HackathonUserData}; - -fn is_registration_closed() -> bool { - let deadline = Utc.with_ymd_and_hms(2025, 11, 30, 16, 29, 0).unwrap(); - Utc::now() >= deadline -} - -pub struct HackathonAuthServiceImpl { - pool: Arc, - jwt: Arc, - supabase: Arc, - config: Arc, -} - -impl HackathonAuthServiceImpl { - pub fn new(pool: Arc, jwt: Arc, supabase: Arc, config: Arc) -> Self { - Self { pool, jwt, supabase, config } - } - - async fn get_user_by_id(&self, user_id: Uuid) -> Result { - sqlx::query_as::<_, HackathonUserData>( - "SELECT id, email, fullname, avatar, phone_number, location, bio, skills, is_active, created_at, updated_at FROM hackathon_users WHERE id = $1" - ) - .bind(user_id) - .fetch_optional(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))? - .ok_or_else(|| AppError::NotFoundError("User not found".to_string())) - } - - async fn get_or_create_active_user(&self, user_id: Uuid, email: &str, fullname: &str) -> Result { - let now = Utc::now(); - sqlx::query_as::<_, HackathonUserData>( - "INSERT INTO hackathon_users (id, email, fullname, is_active, created_at, updated_at) - VALUES ($1, LOWER($2), $3, true, $4, $5) - ON CONFLICT (email) DO UPDATE SET is_active = true, updated_at = NOW() - RETURNING id, email, fullname, avatar, phone_number, location, bio, skills, is_active, created_at, updated_at" - ) - .bind(user_id) - .bind(email) - .bind(fullname) - .bind(now) - .bind(now) - .fetch_one(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string())) - } - - async fn get_or_create_github_user(&self, email: &str, fullname: &str, avatar: Option<&str>) -> Result { - let existing: Option = sqlx::query_as::<_, HackathonUserData>( - "SELECT id, email, fullname, avatar, phone_number, location, bio, skills, is_active, created_at, updated_at FROM hackathon_users WHERE LOWER(email) = LOWER($1)" - ) - .bind(email) - .fetch_optional(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - - if let Some(user) = existing { - return Ok(user); - } - - if is_registration_closed() { - return Err(AppError::BadRequestError("Registration is closed.".to_string())); - } - - let now = Utc::now(); - let user_id = Uuid::new_v4(); - sqlx::query_as::<_, HackathonUserData>( - "INSERT INTO hackathon_users (id, email, fullname, avatar, is_active, created_at, updated_at) - VALUES ($1, LOWER($2), $3, $4, true, $5, $6) - ON CONFLICT (email) DO UPDATE SET avatar = COALESCE(hackathon_users.avatar, EXCLUDED.avatar), is_active = true, updated_at = NOW() - RETURNING id, email, fullname, avatar, phone_number, location, bio, skills, is_active, created_at, updated_at" - ) - .bind(user_id) - .bind(email) - .bind(fullname) - .bind(avatar) - .bind(now) - .bind(now) - .fetch_one(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string())) - } -} - -#[async_trait] -impl HackathonAuthService for HackathonAuthServiceImpl { - async fn signup(&self, email: String, password: String, fullname: String) -> Result<(), AppError> { - if is_registration_closed() { - return Err(AppError::BadRequestError("Registration is closed.".to_string())); - } - let data = self.supabase.signup(&email, &password, &fullname, &self.config.frontend_url).await?; - let user_id_str = data["user"]["id"].as_str() - .or_else(|| data["id"].as_str()) - .ok_or_else(|| AppError::InternalServerError("Missing user ID in signup response".to_string()))?; - let user_uuid = Uuid::parse_str(user_id_str) - .map_err(|_| AppError::InternalServerError("Invalid user ID format".to_string()))?; - let now = Utc::now(); - sqlx::query( - "INSERT INTO hackathon_users (id, email, fullname, is_active, created_at, updated_at) VALUES ($1, LOWER($2), $3, false, $4, $5) ON CONFLICT (email) DO NOTHING" - ) - .bind(user_uuid) - .bind(email) - .bind(fullname) - .bind(now) - .bind(now) - .execute(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - Ok(()) - } - - async fn login(&self, email: String, password: String) -> Result<(AuthTokens, HackathonUserData), AppError> { - let data = self.supabase.login(&email, &password).await?; - let email_confirmed = data["user"]["email_confirmed_at"].as_str().map(|s| !s.is_empty()).unwrap_or(false); - if !email_confirmed { - return Err(AppError::AuthenticationError("Please confirm your email before logging in.".to_string())); - } - let user_id_str = data["user"]["id"].as_str() - .ok_or_else(|| AppError::InternalServerError("Missing user ID".to_string()))?; - let user_id = Uuid::parse_str(user_id_str).map_err(|_| AppError::InternalServerError("Invalid user ID".to_string()))?; - let fullname = data["user"]["user_metadata"]["fullname"].as_str() - .or_else(|| data["user"]["user_metadata"]["full_name"].as_str()) - .unwrap_or(&email).to_string(); - let user = self.get_or_create_active_user(user_id, &email, &fullname).await?; - let tokens = AuthTokens { - access_token: self.jwt.generate_token(user.id)?, - refresh_token: self.jwt.generate_refresh_token(user.id)?, - }; - Ok((tokens, user)) - } - - async fn github_auth(&self, code: String) -> Result<(AuthTokens, HackathonUserData), AppError> { - let http = reqwest::Client::new(); - let token_data: serde_json::Value = http.post("https://github.com/login/oauth/access_token") - .header("Accept", "application/json") - .form(&[("client_id", &self.config.github_client_id), ("client_secret", &self.config.github_client_secret), ("code", &code)]) - .send().await.map_err(|e| AppError::InternalServerError(e.to_string()))? - .json().await.map_err(|e| AppError::InternalServerError(e.to_string()))?; - if token_data.get("error").is_some() { - return Err(AppError::BadRequestError("GitHub OAuth error".to_string())); - } - let access_token = token_data["access_token"].as_str() - .ok_or_else(|| AppError::InternalServerError("Missing access token from GitHub".to_string()))?; - let github_user: serde_json::Value = http.get("https://api.github.com/user") - .header("Authorization", format!("Bearer {}", access_token)) - .header("User-Agent", "imphnen-hackathon-api") - .send().await.map_err(|e| AppError::InternalServerError(e.to_string()))? - .json().await.map_err(|e| AppError::InternalServerError(e.to_string()))?; - let github_id = github_user["id"].as_i64() - .ok_or_else(|| AppError::InternalServerError("Missing GitHub user ID".to_string()))?; - let username = github_user["login"].as_str().unwrap_or("user"); - let email = match github_user["email"].as_str().filter(|e| !e.is_empty()) { - Some(e) => e.to_string(), - None => { - let emails: Vec = http.get("https://api.github.com/user/emails") - .header("Authorization", format!("Bearer {}", access_token)) - .header("User-Agent", "imphnen-hackathon-api") - .send().await.map_err(|e| AppError::InternalServerError(e.to_string()))? - .json().await.unwrap_or_default(); - emails.iter().find(|e| e["primary"].as_bool().unwrap_or(false)) - .or_else(|| emails.iter().find(|e| e["verified"].as_bool().unwrap_or(false))) - .and_then(|e| e["email"].as_str()).map(|s| s.to_string()) - .unwrap_or_else(|| format!("{}+{}@users.noreply.github.com", github_id, username)) - } - }; - let fullname = github_user["name"].as_str().or_else(|| github_user["login"].as_str()).unwrap_or("GitHub User").to_string(); - let avatar = github_user["avatar_url"].as_str(); - let user = self.get_or_create_github_user(&email, &fullname, avatar).await?; - let tokens = AuthTokens { - access_token: self.jwt.generate_token(user.id)?, - refresh_token: self.jwt.generate_refresh_token(user.id)?, - }; - Ok((tokens, user)) - } - - async fn get_session(&self, user_id: Uuid) -> Result { - self.get_user_by_id(user_id).await - } - - async fn forgot_password(&self, email: String) -> Result<(), AppError> { - self.supabase.recover_password(&email, &self.config.frontend_url).await - } - - async fn reset_password(&self, access_token: String, new_password: String) -> Result<(), AppError> { - if new_password.len() < 6 { - return Err(AppError::BadRequestError("Password must be at least 6 characters long".to_string())); - } - self.supabase.update_password(&access_token, &new_password).await - } -} diff --git a/imphnen-hackathon/src/auth/application/mod.rs b/imphnen-hackathon/src/auth/application/mod.rs deleted file mode 100644 index 3fe88a6..0000000 --- a/imphnen-hackathon/src/auth/application/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod auth_service; diff --git a/imphnen-hackathon/src/auth/domain/mod.rs b/imphnen-hackathon/src/auth/domain/mod.rs deleted file mode 100644 index 1f278a4..0000000 --- a/imphnen-hackathon/src/auth/domain/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod service; diff --git a/imphnen-hackathon/src/auth/domain/service.rs b/imphnen-hackathon/src/auth/domain/service.rs deleted file mode 100644 index 57ebb9c..0000000 --- a/imphnen-hackathon/src/auth/domain/service.rs +++ /dev/null @@ -1,34 +0,0 @@ -use async_trait::async_trait; -use imphnen_utils::errors::AppError; -use uuid::Uuid; - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct AuthTokens { - pub access_token: String, - pub refresh_token: String, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema, sqlx::FromRow)] -pub struct HackathonUserData { - pub id: Uuid, - pub email: String, - pub fullname: String, - pub avatar: Option, - pub phone_number: Option, - pub location: Option, - pub bio: Option, - pub skills: Option>, - pub is_active: Option, - pub created_at: Option>, - pub updated_at: Option>, -} - -#[async_trait] -pub trait HackathonAuthService: Send + Sync { - async fn signup(&self, email: String, password: String, fullname: String) -> Result<(), AppError>; - async fn login(&self, email: String, password: String) -> Result<(AuthTokens, HackathonUserData), AppError>; - async fn github_auth(&self, code: String) -> Result<(AuthTokens, HackathonUserData), AppError>; - async fn get_session(&self, user_id: Uuid) -> Result; - async fn forgot_password(&self, email: String) -> Result<(), AppError>; - async fn reset_password(&self, access_token: String, new_password: String) -> Result<(), AppError>; -} diff --git a/imphnen-hackathon/src/auth/infrastructure/http/dto.rs b/imphnen-hackathon/src/auth/infrastructure/http/dto.rs deleted file mode 100644 index 0542666..0000000 --- a/imphnen-hackathon/src/auth/infrastructure/http/dto.rs +++ /dev/null @@ -1,38 +0,0 @@ -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct SignupRequest { - pub email: String, - pub password: String, - pub fullname: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct LoginRequest { - pub email: String, - pub password: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct GitHubAuthRequest { - pub code: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct ForgotPasswordRequest { - pub email: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct ResetPasswordRequest { - pub access_token: String, - pub new_password: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct AuthResponse { - pub access_token: String, - pub refresh_token: String, - pub user: crate::auth::domain::service::HackathonUserData, -} diff --git a/imphnen-hackathon/src/auth/infrastructure/http/handlers.rs b/imphnen-hackathon/src/auth/infrastructure/http/handlers.rs deleted file mode 100644 index 9040668..0000000 --- a/imphnen-hackathon/src/auth/infrastructure/http/handlers.rs +++ /dev/null @@ -1,62 +0,0 @@ -use axum::{Extension, Json, response::IntoResponse}; -use std::sync::Arc; -use imphnen_utils::response_format::{ApiSuccess, ApiMessage}; -use crate::auth::domain::service::HackathonAuthService; -use crate::middleware::hackathon_auth::HackathonAuthUser; -use super::dto::*; - -pub async fn signup_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - service.signup(body.email, body.password, body.fullname).await?; - Ok(ApiMessage::created("Registration successful! Please check your email to activate your account.")) -} - -pub async fn login_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - let (tokens, user) = service.login(body.email, body.password).await?; - Ok(ApiSuccess(AuthResponse { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - user, - }).into_response()) -} - -pub async fn github_auth_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - let (tokens, user) = service.github_auth(body.code).await?; - Ok(ApiSuccess(AuthResponse { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - user, - }).into_response()) -} - -pub async fn get_session_handler( - Extension(service): Extension>, - Extension(auth_user): Extension, -) -> Result { - let user = service.get_session(auth_user.user_id).await?; - Ok(ApiSuccess(user).into_response()) -} - -pub async fn forgot_password_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - service.forgot_password(body.email).await?; - Ok(ApiMessage::ok("If an account with that email exists, a password reset link has been sent.")) -} - -pub async fn reset_password_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - service.reset_password(body.access_token, body.new_password).await?; - Ok(ApiMessage::ok("Password has been successfully reset.")) -} diff --git a/imphnen-hackathon/src/auth/infrastructure/http/mod.rs b/imphnen-hackathon/src/auth/infrastructure/http/mod.rs deleted file mode 100644 index eee210d..0000000 --- a/imphnen-hackathon/src/auth/infrastructure/http/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod dto; -pub mod handlers; -pub mod routes; diff --git a/imphnen-hackathon/src/auth/infrastructure/http/routes.rs b/imphnen-hackathon/src/auth/infrastructure/http/routes.rs deleted file mode 100644 index 4f8618a..0000000 --- a/imphnen-hackathon/src/auth/infrastructure/http/routes.rs +++ /dev/null @@ -1,33 +0,0 @@ -use axum::{middleware::from_fn, routing::{get, post}, Extension, Router}; -use sqlx::PgPool; -use std::sync::Arc; -use crate::auth::application::auth_service::HackathonAuthServiceImpl; -use crate::auth::domain::service::HackathonAuthService; -use crate::common::hackathon_jwt::HackathonJwtService; -use crate::common::supabase_client::SupabaseClient; -use crate::config::HackathonConfig; -use crate::middleware::hackathon_auth::hackathon_auth_middleware; -use super::handlers::*; - -pub fn hackathon_auth_routes(pool: Arc, jwt: Arc, supabase: Arc, config: Arc) -> Router { - let service: Arc = Arc::new( - HackathonAuthServiceImpl::new(pool.clone(), jwt.clone(), supabase, config) - ); - - let public = Router::new() - .route("/auth/signup", post(signup_handler)) - .route("/auth/login", post(login_handler)) - .route("/auth/github", post(github_auth_handler)) - .route("/auth/forgot-password", post(forgot_password_handler)) - .route("/auth/reset-password", post(reset_password_handler)) - .layer(Extension(service.clone())); - - let protected = Router::new() - .route("/auth/session", get(get_session_handler)) - .layer(Extension(service)) - .layer(Extension(jwt.clone())) - .layer(Extension(pool)) - .layer(from_fn(hackathon_auth_middleware)); - - public.merge(protected) -} diff --git a/imphnen-hackathon/src/auth/infrastructure/mod.rs b/imphnen-hackathon/src/auth/infrastructure/mod.rs deleted file mode 100644 index 3883215..0000000 --- a/imphnen-hackathon/src/auth/infrastructure/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod http; diff --git a/imphnen-hackathon/src/auth/mod.rs b/imphnen-hackathon/src/auth/mod.rs deleted file mode 100644 index 36b1245..0000000 --- a/imphnen-hackathon/src/auth/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod domain; -pub mod application; -pub mod infrastructure; - -pub use infrastructure::http::routes::hackathon_auth_routes; diff --git a/imphnen-hackathon/src/chat/infrastructure/http/routes.rs b/imphnen-hackathon/src/chat/infrastructure/http/routes.rs index 6d73963..21b2172 100644 --- a/imphnen-hackathon/src/chat/infrastructure/http/routes.rs +++ b/imphnen-hackathon/src/chat/infrastructure/http/routes.rs @@ -4,11 +4,10 @@ use std::sync::Arc; use crate::chat::application::chat_service::ChatServiceImpl; use crate::chat::domain::service::ChatService; use crate::chat::infrastructure::persistence::PostgresChatRepository; -use crate::common::hackathon_jwt::HackathonJwtService; use crate::middleware::hackathon_auth::hackathon_auth_middleware; use super::handlers::*; -pub fn build_chat_routes(pool: Arc, jwt: Arc) -> Router { +pub fn build_chat_routes(pool: Arc) -> Router { let service: Arc = Arc::new(ChatServiceImpl::new( Arc::new(PostgresChatRepository::new(pool.clone())), )); @@ -16,7 +15,6 @@ pub fn build_chat_routes(pool: Arc, jwt: Arc) -> Ro .route("/chat/teams/:team_id", get(get_team_messages_handler).post(send_message_handler)) .route("/chat/messages/:message_id", delete(delete_message_handler)) .layer(Extension(service)) - .layer(Extension(jwt.clone())) .layer(Extension(pool)) .layer(from_fn(hackathon_auth_middleware)) } diff --git a/imphnen-hackathon/src/common/hackathon_jwt.rs b/imphnen-hackathon/src/common/hackathon_jwt.rs deleted file mode 100644 index 4e1a022..0000000 --- a/imphnen-hackathon/src/common/hackathon_jwt.rs +++ /dev/null @@ -1,59 +0,0 @@ -use chrono::{Duration, Utc}; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use imphnen_utils::errors::AppError; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct HackathonClaims { - pub sub: String, - pub exp: i64, - pub iat: i64, - pub jti: String, - #[serde(default)] - pub token_type: String, -} - -#[derive(Clone)] -pub struct HackathonJwtService { - encoding_key: EncodingKey, - decoding_key: DecodingKey, - expiry_hours: i64, -} - -impl HackathonJwtService { - pub fn new(secret: &str, expiry_hours: i64) -> Self { - Self { - encoding_key: EncodingKey::from_secret(secret.as_bytes()), - decoding_key: DecodingKey::from_secret(secret.as_bytes()), - expiry_hours, - } - } - - pub fn generate_token(&self, user_id: Uuid) -> Result { - self.generate_token_with_type(user_id, "access", self.expiry_hours) - } - - pub fn generate_refresh_token(&self, user_id: Uuid) -> Result { - self.generate_token_with_type(user_id, "refresh", self.expiry_hours * 7) - } - - fn generate_token_with_type(&self, user_id: Uuid, token_type: &str, expiry_hours: i64) -> Result { - let now = Utc::now(); - let claims = HackathonClaims { - sub: user_id.to_string(), - exp: (now + Duration::hours(expiry_hours)).timestamp(), - iat: now.timestamp(), - jti: Uuid::new_v4().to_string(), - token_type: token_type.to_string(), - }; - encode(&Header::default(), &claims, &self.encoding_key) - .map_err(|e| AppError::InternalServerError(e.to_string())) - } - - pub fn verify_token(&self, token: &str) -> Result { - decode::(token, &self.decoding_key, &Validation::default()) - .map(|d| d.claims) - .map_err(|_| AppError::AuthenticationError("Invalid or expired token".to_string())) - } -} diff --git a/imphnen-hackathon/src/common/mod.rs b/imphnen-hackathon/src/common/mod.rs index 1cc2f75..ed94449 100644 --- a/imphnen-hackathon/src/common/mod.rs +++ b/imphnen-hackathon/src/common/mod.rs @@ -1,3 +1 @@ pub mod cities; -pub mod hackathon_jwt; -pub mod supabase_client; diff --git a/imphnen-hackathon/src/common/supabase_client.rs b/imphnen-hackathon/src/common/supabase_client.rs deleted file mode 100644 index 1235e63..0000000 --- a/imphnen-hackathon/src/common/supabase_client.rs +++ /dev/null @@ -1,107 +0,0 @@ -use imphnen_utils::errors::AppError; -use serde_json::{json, Value}; - -pub struct SupabaseClient { - pub base_url: String, - pub anon_key: String, - pub service_role_key: String, - pub storage_bucket: String, - client: reqwest::Client, -} - -impl SupabaseClient { - pub fn new(base_url: String, anon_key: String, service_role_key: String, storage_bucket: String) -> Self { - Self { - base_url, - anon_key, - service_role_key, - storage_bucket, - client: reqwest::Client::new(), - } - } - - pub async fn signup(&self, email: &str, password: &str, fullname: &str, frontend_url: &str) -> Result { - let resp = self.client - .post(format!("{}/auth/v1/signup", self.base_url)) - .header("apikey", &self.anon_key) - .json(&json!({ - "email": email, - "password": password, - "options": { - "data": { "fullname": fullname }, - "emailRedirectTo": format!("{}/auth/callback", frontend_url) - } - })) - .send() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - - if !resp.status().is_success() { - return Err(AppError::BadRequestError("Signup failed. Email may already be registered.".to_string())); - } - resp.json::().await.map_err(|e| AppError::InternalServerError(e.to_string())) - } - - pub async fn login(&self, email: &str, password: &str) -> Result { - let resp = self.client - .post(format!("{}/auth/v1/token?grant_type=password", self.base_url)) - .header("apikey", &self.anon_key) - .json(&json!({ "email": email, "password": password })) - .send() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - - if !resp.status().is_success() { - return Err(AppError::AuthenticationError("Invalid email or password".to_string())); - } - resp.json::().await.map_err(|e| AppError::InternalServerError(e.to_string())) - } - - pub async fn recover_password(&self, email: &str, frontend_url: &str) -> Result<(), AppError> { - let _ = self.client - .post(format!("{}/auth/v1/recover", self.base_url)) - .header("apikey", &self.anon_key) - .json(&json!({ - "email": email, - "redirectTo": format!("{}/auth/callback", frontend_url) - })) - .send() - .await; - Ok(()) - } - - pub async fn update_password(&self, access_token: &str, new_password: &str) -> Result<(), AppError> { - let resp = self.client - .put(format!("{}/auth/v1/user", self.base_url)) - .header("apikey", &self.anon_key) - .header("Authorization", format!("Bearer {}", access_token)) - .json(&json!({ "password": new_password })) - .send() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - - if !resp.status().is_success() { - return Err(AppError::BadRequestError("Password reset failed. Link may have expired.".to_string())); - } - Ok(()) - } - - pub async fn upload_file(&self, path: &str, content_type: &str, data: &[u8]) -> Result { - let resp = self.client - .post(format!("{}/storage/v1/object/{}/{}", self.base_url, self.storage_bucket, path)) - .header("apikey", &self.service_role_key) - .header("Authorization", format!("Bearer {}", self.service_role_key)) - .header("Content-Type", content_type) - .body(data.to_vec()) - .send() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - - if !resp.status().is_success() { - let err = resp.text().await.unwrap_or_default(); - return Err(AppError::InternalServerError(format!("Upload failed: {}", err))); - } - - Ok(format!("{}/storage/v1/object/public/{}/{}", self.base_url, self.storage_bucket, path)) - } -} diff --git a/imphnen-hackathon/src/config.rs b/imphnen-hackathon/src/config.rs index bb121de..bc4fc00 100644 --- a/imphnen-hackathon/src/config.rs +++ b/imphnen-hackathon/src/config.rs @@ -2,42 +2,20 @@ use std::env; #[derive(Debug, Clone)] pub struct HackathonConfig { - pub supabase_url: String, - pub supabase_anon_key: String, - pub supabase_service_role_key: String, - pub jwt_secret: String, - pub jwt_expiry_hours: i64, - pub github_client_id: String, - pub github_client_secret: String, - pub github_redirect_url: String, pub smtp_host: String, pub smtp_user: String, pub smtp_password: String, pub from_email: String, - pub storage_bucket: String, pub frontend_url: String, } impl HackathonConfig { pub fn from_env() -> Self { Self { - supabase_url: env::var("HACKATHON_SUPABASE_URL").unwrap_or_default(), - supabase_anon_key: env::var("HACKATHON_SUPABASE_ANON_KEY").unwrap_or_default(), - supabase_service_role_key: env::var("HACKATHON_SUPABASE_SERVICE_ROLE_KEY").unwrap_or_default(), - jwt_secret: env::var("HACKATHON_JWT_SECRET").expect("HACKATHON_JWT_SECRET must be set"), - jwt_expiry_hours: env::var("HACKATHON_JWT_EXPIRY_HOURS") - .unwrap_or_else(|_| "168".to_string()) - .parse() - .unwrap_or(168), - github_client_id: env::var("HACKATHON_GITHUB_CLIENT_ID").unwrap_or_default(), - github_client_secret: env::var("HACKATHON_GITHUB_CLIENT_SECRET").unwrap_or_default(), - github_redirect_url: env::var("HACKATHON_GITHUB_REDIRECT_URL").unwrap_or_default(), smtp_host: env::var("HACKATHON_SMTP_HOST").unwrap_or_default(), smtp_user: env::var("HACKATHON_SMTP_USER").unwrap_or_default(), smtp_password: env::var("HACKATHON_SMTP_PASSWORD").unwrap_or_default(), from_email: env::var("HACKATHON_FROM_EMAIL").unwrap_or_default(), - storage_bucket: env::var("HACKATHON_STORAGE_BUCKET") - .unwrap_or_else(|_| "hackathon-uploads".to_string()), frontend_url: env::var("HACKATHON_FRONTEND_URL") .unwrap_or_else(|_| "https://hackathon.imphnen.dev".to_string()), } diff --git a/imphnen-hackathon/src/invitations/infrastructure/http/routes.rs b/imphnen-hackathon/src/invitations/infrastructure/http/routes.rs index 0909942..ceff93a 100644 --- a/imphnen-hackathon/src/invitations/infrastructure/http/routes.rs +++ b/imphnen-hackathon/src/invitations/infrastructure/http/routes.rs @@ -4,11 +4,10 @@ use std::sync::Arc; use crate::invitations::application::invitation_service::InvitationServiceImpl; use crate::invitations::domain::service::InvitationService; use crate::invitations::infrastructure::persistence::PostgresInvitationRepository; -use crate::common::hackathon_jwt::HackathonJwtService; use crate::middleware::hackathon_auth::hackathon_auth_middleware; use super::handlers::*; -pub fn build_invitation_routes(pool: Arc, jwt: Arc) -> Router { +pub fn build_invitation_routes(pool: Arc) -> Router { let service: Arc = Arc::new(InvitationServiceImpl::new( Arc::new(PostgresInvitationRepository::new(pool.clone())), )); @@ -17,7 +16,6 @@ pub fn build_invitation_routes(pool: Arc, jwt: Arc) .route("/invitations/:invitation_id/respond", post(respond_to_invitation_handler)) .route("/invitations/teams/:team_id/invite", post(invite_team_member_handler)) .layer(Extension(service)) - .layer(Extension(jwt.clone())) .layer(Extension(pool)) .layer(from_fn(hackathon_auth_middleware)) } diff --git a/imphnen-hackathon/src/join_requests/infrastructure/http/routes.rs b/imphnen-hackathon/src/join_requests/infrastructure/http/routes.rs index fa278a2..69ad6a6 100644 --- a/imphnen-hackathon/src/join_requests/infrastructure/http/routes.rs +++ b/imphnen-hackathon/src/join_requests/infrastructure/http/routes.rs @@ -4,11 +4,10 @@ use std::sync::Arc; use crate::join_requests::application::join_request_service::JoinRequestServiceImpl; use crate::join_requests::domain::service::JoinRequestService; use crate::join_requests::infrastructure::persistence::PostgresJoinRequestRepository; -use crate::common::hackathon_jwt::HackathonJwtService; use crate::middleware::hackathon_auth::hackathon_auth_middleware; use super::handlers::*; -pub fn build_join_request_routes(pool: Arc, jwt: Arc) -> Router { +pub fn build_join_request_routes(pool: Arc) -> Router { let service: Arc = Arc::new(JoinRequestServiceImpl::new( Arc::new(PostgresJoinRequestRepository::new(pool.clone())), )); @@ -18,7 +17,6 @@ pub fn build_join_request_routes(pool: Arc, jwt: Arc) -> Router { +pub fn hackathon_router(db: DatabaseConnection, _config: Arc, minio: Arc) -> Router { let pool = Arc::new(db.get_postgres_connection_pool().clone()); - let jwt = Arc::new(HackathonJwtService::new(&config.jwt_secret, config.jwt_expiry_hours)); - let supabase = Arc::new(SupabaseClient::new( - config.supabase_url.clone(), - config.supabase_anon_key.clone(), - config.supabase_service_role_key.clone(), - config.storage_bucket.clone(), - )); Router::new() - .merge(hackathon_auth_routes(pool.clone(), jwt.clone(), supabase.clone(), config.clone())) - .merge(hackathon_users_routes(pool.clone(), jwt.clone())) - .merge(build_team_routes(pool.clone(), jwt.clone())) - .merge(build_invitation_routes(pool.clone(), jwt.clone())) - .merge(build_join_request_routes(pool.clone(), jwt.clone())) - .merge(build_chat_routes(pool.clone(), jwt.clone())) - .merge(hackathon_submissions_routes(pool.clone(), jwt.clone())) - .merge(hackathon_storage_routes(pool.clone(), jwt.clone(), supabase)) + .merge(hackathon_users_routes(pool.clone())) + .merge(build_team_routes(pool.clone())) + .merge(build_invitation_routes(pool.clone())) + .merge(build_join_request_routes(pool.clone())) + .merge(build_chat_routes(pool.clone())) + .merge(hackathon_submissions_routes(pool.clone())) + .merge(hackathon_storage_routes(pool.clone(), minio)) .merge(hackathon_certificates_routes(pool.clone())) .merge(hackathon_winners_routes(pool.clone())) - .merge(hackathon_admin_routes(pool, jwt)) + .merge(hackathon_admin_routes(pool)) } diff --git a/imphnen-hackathon/src/middleware/hackathon_auth.rs b/imphnen-hackathon/src/middleware/hackathon_auth.rs index 037b8ee..eaa4749 100644 --- a/imphnen-hackathon/src/middleware/hackathon_auth.rs +++ b/imphnen-hackathon/src/middleware/hackathon_auth.rs @@ -4,7 +4,7 @@ use sqlx::PgPool; use std::sync::Arc; use uuid::Uuid; use serde::{Deserialize, Serialize}; -use crate::common::hackathon_jwt::HackathonJwtService; +use imphnen_libs::decode_access_token; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HackathonAuthUser { @@ -13,7 +13,6 @@ pub struct HackathonAuthUser { } pub async fn hackathon_auth_middleware( - axum::Extension(jwt_service): axum::Extension>, axum::Extension(pool): axum::Extension>, mut request: Request, next: Next, @@ -28,20 +27,22 @@ pub async fn hackathon_auth_middleware( (StatusCode::UNAUTHORIZED, "Invalid Authorization header format").into_response() })?; - let claims = jwt_service.verify_token(token).map_err(|_| { + let token_data = decode_access_token(token).map_err(|_| { (StatusCode::UNAUTHORIZED, "Invalid or expired token").into_response() })?; - let user_id = Uuid::parse_str(&claims.sub).map_err(|_| { + let user_id = Uuid::parse_str(&token_data.claims.user_id).map_err(|_| { (StatusCode::UNAUTHORIZED, "Invalid user ID in token").into_response() })?; - let is_admin: bool = sqlx::query_scalar("SELECT COALESCE(is_admin, false) FROM hackathon_users WHERE id = $1") - .bind(user_id) - .fetch_optional(pool.as_ref()) - .await - .unwrap_or(None) - .unwrap_or(false); + let is_admin: bool = sqlx::query_scalar( + "SELECT COALESCE(is_admin, false) FROM hackathon_users WHERE id = $1" + ) + .bind(user_id) + .fetch_optional(pool.as_ref()) + .await + .unwrap_or(None) + .unwrap_or(false); request.extensions_mut().insert(HackathonAuthUser { user_id, is_admin }); Ok(next.run(request).await) diff --git a/imphnen-hackathon/src/storage/routes.rs b/imphnen-hackathon/src/storage/routes.rs index b906614..06498ab 100644 --- a/imphnen-hackathon/src/storage/routes.rs +++ b/imphnen-hackathon/src/storage/routes.rs @@ -4,8 +4,7 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use imphnen_utils::{errors::AppError, response_format::ApiSuccess}; -use crate::common::hackathon_jwt::HackathonJwtService; -use crate::common::supabase_client::SupabaseClient; +use imphnen_libs::MinioService; use crate::middleware::hackathon_auth::{hackathon_auth_middleware, HackathonAuthUser}; use super::service::StorageService; @@ -57,15 +56,14 @@ async fn upload_submission_handler( Ok(ApiSuccess(UploadResponse { url }).into_response()) } -pub fn hackathon_storage_routes(pool: Arc, jwt: Arc, supabase: Arc) -> Router { - let service = Arc::new(StorageService::new(supabase)); +pub fn hackathon_storage_routes(pool: Arc, minio: Arc) -> Router { + let service = Arc::new(StorageService::new(minio)); Router::new() .route("/upload", post(upload_file_handler)) .route("/upload/avatar", post(upload_avatar_handler)) .route("/upload/team", post(upload_team_handler)) .route("/upload/submission", post(upload_submission_handler)) .layer(Extension(service)) - .layer(Extension(jwt.clone())) .layer(Extension(pool)) .layer(from_fn(hackathon_auth_middleware)) } diff --git a/imphnen-hackathon/src/storage/service.rs b/imphnen-hackathon/src/storage/service.rs index bcc15d7..08d532d 100644 --- a/imphnen-hackathon/src/storage/service.rs +++ b/imphnen-hackathon/src/storage/service.rs @@ -1,22 +1,22 @@ use std::sync::Arc; -use base64::Engine; -use chrono::Utc; use uuid::Uuid; +use chrono::Utc; use imphnen_utils::errors::AppError; -use crate::common::supabase_client::SupabaseClient; +use imphnen_libs::MinioService; pub struct StorageService { - supabase: Arc, + minio: Arc, } impl StorageService { - pub fn new(supabase: Arc) -> Self { Self { supabase } } + pub fn new(minio: Arc) -> Self { Self { minio } } pub async fn upload(&self, folder: &str, user_id: Uuid, filename: &str, content_type: &str, data_base64: &str) -> Result { let ext = filename.rsplit('.').next().unwrap_or("bin"); - let path = format!("{}/{}-{}.{}", folder, user_id, Utc::now().timestamp_millis(), ext); - let data = base64::engine::general_purpose::STANDARD.decode(data_base64) - .map_err(|_| AppError::BadRequestError("Invalid base64 data".to_string()))?; - self.supabase.upload_file(&path, content_type, &data).await + let unique_name = format!("{}-{}.{}", user_id, Utc::now().timestamp_millis(), ext); + self.minio + .upload_base64_file(data_base64, content_type, folder, &unique_name) + .await + .map_err(|e| AppError::InternalServerError(e.to_string())) } } diff --git a/imphnen-hackathon/src/submissions/infrastructure/http/routes.rs b/imphnen-hackathon/src/submissions/infrastructure/http/routes.rs index e157122..926124f 100644 --- a/imphnen-hackathon/src/submissions/infrastructure/http/routes.rs +++ b/imphnen-hackathon/src/submissions/infrastructure/http/routes.rs @@ -4,11 +4,10 @@ use std::sync::Arc; use crate::submissions::application::submission_service::SubmissionServiceImpl; use crate::submissions::domain::service::SubmissionService; use crate::submissions::infrastructure::persistence::PostgresSubmissionRepository; -use crate::common::hackathon_jwt::HackathonJwtService; use crate::middleware::hackathon_auth::hackathon_auth_middleware; use super::handlers::*; -pub fn hackathon_submissions_routes(pool: Arc, jwt: Arc) -> Router { +pub fn hackathon_submissions_routes(pool: Arc) -> Router { let service: Arc = Arc::new(SubmissionServiceImpl::new(Arc::new(PostgresSubmissionRepository::new(pool.clone())))); Router::new() .route("/submissions/teams/:team_id", get(get_team_submission_handler).post(create_submission_handler)) @@ -17,7 +16,6 @@ pub fn hackathon_submissions_routes(pool: Arc, jwt: Arc, jwt: Arc) -> Router { +pub fn build_team_routes(pool: Arc) -> Router { let repo = Arc::new(PostgresTeamRepository::new(pool.clone())); let service: Arc = Arc::new(TeamServiceImpl::new(repo)); @@ -25,7 +24,6 @@ pub fn build_team_routes(pool: Arc, jwt: Arc) -> Ro .route("/teams/:team_id/members/:member_id", delete(remove_member_handler)) .layer(Extension(service)) .layer(Extension(pool.clone())) - .layer(Extension(jwt)) .layer(from_fn(hackathon_auth_middleware)); Router::new().merge(public).merge(protected) diff --git a/imphnen-hackathon/src/users/infrastructure/http/routes.rs b/imphnen-hackathon/src/users/infrastructure/http/routes.rs index 6c2beee..6de1e9a 100644 --- a/imphnen-hackathon/src/users/infrastructure/http/routes.rs +++ b/imphnen-hackathon/src/users/infrastructure/http/routes.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use crate::users::application::user_service::HackathonUserServiceImpl; use crate::users::domain::service::HackathonUserService; use crate::users::infrastructure::persistence::PostgresHackathonUserRepository; -use crate::common::hackathon_jwt::HackathonJwtService; use crate::middleware::hackathon_auth::hackathon_auth_middleware; use super::handlers::*; @@ -13,14 +12,13 @@ fn build_service(pool: Arc) -> Arc { Arc::new(HackathonUserServiceImpl::new(repo)) } -pub fn hackathon_users_routes(pool: Arc, jwt: Arc) -> Router { +pub fn hackathon_users_routes(pool: Arc) -> Router { let service = build_service(pool.clone()); Router::new() .route("/users/me", get(get_me_handler).put(update_me_handler)) .route("/users/:user_id", get(get_user_handler)) .route("/users/:user_id/teams", get(get_user_teams_handler)) .layer(Extension(service)) - .layer(Extension(jwt)) .layer(Extension(pool)) .layer(from_fn(hackathon_auth_middleware)) } diff --git a/imphnen-qr/Cargo.toml b/imphnen-qr/Cargo.toml index 7cb4b0d..eb66774 100644 --- a/imphnen-qr/Cargo.toml +++ b/imphnen-qr/Cargo.toml @@ -5,19 +5,15 @@ edition = "2024" [dependencies] imphnen-utils.workspace = true +imphnen-libs.workspace = true axum.workspace = true -axum-extra.workspace = true async-trait.workspace = true serde.workspace = true serde_json.workspace = true tokio.workspace = true -jsonwebtoken.workspace = true -bcrypt.workspace = true chrono.workspace = true uuid.workspace = true sqlx.workspace = true -reqwest.workspace = true -oauth2.workspace = true tracing.workspace = true utoipa.workspace = true image.workspace = true diff --git a/imphnen-qr/src/auth/application/auth_service.rs b/imphnen-qr/src/auth/application/auth_service.rs deleted file mode 100644 index 1284af5..0000000 --- a/imphnen-qr/src/auth/application/auth_service.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::sync::Arc; -use uuid::Uuid; -use sqlx::PgPool; -use async_trait::async_trait; -use imphnen_utils::errors::AppError; -use crate::common::qr_jwt::QrJwtService; -use crate::config::QrConfig; -use super::super::domain::service::{QrAuthService, AuthTokens, QrUserData}; - -pub struct QrAuthServiceImpl { - pool: Arc, - jwt: Arc, - config: Arc, -} - -impl QrAuthServiceImpl { - pub fn new(pool: Arc, jwt: Arc, config: Arc) -> Self { - Self { pool, jwt, config } - } - - async fn find_user_by_id(&self, id: Uuid) -> Result { - sqlx::query_as::<_, QrUserData>( - "SELECT id, email, name, role, provider, created_at, updated_at FROM users WHERE id = $1" - ) - .bind(id) - .fetch_optional(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))? - .ok_or_else(|| AppError::NotFoundError("User not found".to_string())) - } - - async fn find_user_by_email(&self, email: &str) -> Result, AppError> { - sqlx::query_scalar::<_, serde_json::Value>( - "SELECT row_to_json(u) FROM (SELECT id, email, name, role, provider, password FROM users WHERE email = $1) u" - ) - .bind(email) - .fetch_optional(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string())) - } - - fn make_tokens(&self, user_id: Uuid, role: &str) -> Result { - Ok(AuthTokens { - access_token: self.jwt.generate_token(user_id, role)?, - refresh_token: self.jwt.generate_refresh_token(user_id, role)?, - }) - } -} - -#[async_trait] -impl QrAuthService for QrAuthServiceImpl { - async fn register(&self, email: String, password: String, name: String) -> Result<(AuthTokens, QrUserData), AppError> { - let existing = self.find_user_by_email(&email).await?; - if existing.is_some() { - return Err(AppError::ConflictError("Email already registered".to_string())); - } - let hashed = bcrypt::hash(&password, 10) - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - let user = sqlx::query_as::<_, QrUserData>( - "INSERT INTO users (email, password, name, role, provider) VALUES ($1, $2, $3, 'user', 'local') RETURNING id, email, name, role, provider, created_at, updated_at" - ) - .bind(&email) - .bind(&hashed) - .bind(&name) - .fetch_one(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - let tokens = self.make_tokens(user.id, &user.role)?; - Ok((tokens, user)) - } - - async fn login(&self, email: String, password: String) -> Result<(AuthTokens, QrUserData), AppError> { - let row = self.find_user_by_email(&email).await? - .ok_or_else(|| AppError::AuthenticationError("Invalid credentials".to_string()))?; - let provider = row["provider"].as_str().unwrap_or("local"); - if provider != "local" { - return Err(AppError::AuthenticationError("Account uses social login".to_string())); - } - let stored_hash = row["password"].as_str() - .ok_or_else(|| AppError::AuthenticationError("Invalid credentials".to_string()))?; - let valid = bcrypt::verify(&password, stored_hash) - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - if !valid { - return Err(AppError::AuthenticationError("Invalid credentials".to_string())); - } - let user_id: Uuid = row["id"].as_str() - .and_then(|s| Uuid::parse_str(s).ok()) - .ok_or_else(|| AppError::InternalServerError("Invalid user ID".to_string()))?; - let user = self.find_user_by_id(user_id).await?; - let tokens = self.make_tokens(user.id, &user.role)?; - Ok((tokens, user)) - } - - async fn google_callback(&self, code: String) -> Result<(AuthTokens, QrUserData), AppError> { - let http = reqwest::Client::new(); - let token_res: serde_json::Value = http - .post("https://oauth2.googleapis.com/token") - .form(&[ - ("code", code.as_str()), - ("client_id", self.config.google_client_id.as_str()), - ("client_secret", self.config.google_client_secret.as_str()), - ("redirect_uri", self.config.google_redirect_url.as_str()), - ("grant_type", "authorization_code"), - ]) - .send() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))? - .json() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - if token_res.get("error").is_some() { - return Err(AppError::BadRequestError("Google OAuth error".to_string())); - } - let access_token = token_res["access_token"].as_str() - .ok_or_else(|| AppError::InternalServerError("Missing access token from Google".to_string()))?; - let google_user: serde_json::Value = http - .get("https://www.googleapis.com/oauth2/v2/userinfo") - .header("Authorization", format!("Bearer {}", access_token)) - .send() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))? - .json() - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - let email = google_user["email"].as_str() - .ok_or_else(|| AppError::InternalServerError("Missing email from Google".to_string()))?; - let name = google_user["name"].as_str().unwrap_or(email); - let provider_id = google_user["id"].as_str().unwrap_or(""); - let user = sqlx::query_as::<_, QrUserData>( - "INSERT INTO users (email, name, role, provider, provider_id) VALUES ($1, $2, 'user', 'google', $3) - ON CONFLICT (email) DO UPDATE SET provider_id = EXCLUDED.provider_id, updated_at = NOW() - RETURNING id, email, name, role, provider, created_at, updated_at" - ) - .bind(email) - .bind(name) - .bind(provider_id) - .fetch_one(self.pool.as_ref()) - .await - .map_err(|e| AppError::InternalServerError(e.to_string()))?; - let tokens = self.make_tokens(user.id, &user.role)?; - Ok((tokens, user)) - } - - async fn refresh_token(&self, refresh_token: String) -> Result { - let claims = self.jwt.verify_token(&refresh_token)?; - let user_id = Uuid::parse_str(&claims.sub) - .map_err(|_| AppError::AuthenticationError("Invalid token subject".to_string()))?; - let user = self.find_user_by_id(user_id).await?; - Ok(AuthTokens { - access_token: self.jwt.generate_token(user.id, &user.role)?, - refresh_token, - }) - } -} diff --git a/imphnen-qr/src/auth/application/mod.rs b/imphnen-qr/src/auth/application/mod.rs deleted file mode 100644 index 3fe88a6..0000000 --- a/imphnen-qr/src/auth/application/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod auth_service; diff --git a/imphnen-qr/src/auth/domain/mod.rs b/imphnen-qr/src/auth/domain/mod.rs deleted file mode 100644 index 1f278a4..0000000 --- a/imphnen-qr/src/auth/domain/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod service; diff --git a/imphnen-qr/src/auth/domain/service.rs b/imphnen-qr/src/auth/domain/service.rs deleted file mode 100644 index 4e41233..0000000 --- a/imphnen-qr/src/auth/domain/service.rs +++ /dev/null @@ -1,30 +0,0 @@ -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use uuid::Uuid; -use imphnen_utils::errors::AppError; - -#[derive(Debug, Serialize, Deserialize)] -pub struct AuthTokens { - pub access_token: String, - pub refresh_token: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema, sqlx::FromRow)] -pub struct QrUserData { - pub id: Uuid, - pub email: String, - pub name: String, - pub role: String, - pub provider: String, - pub created_at: Option>, - pub updated_at: Option>, -} - -#[async_trait] -pub trait QrAuthService: Send + Sync { - async fn register(&self, email: String, password: String, name: String) -> Result<(AuthTokens, QrUserData), AppError>; - async fn login(&self, email: String, password: String) -> Result<(AuthTokens, QrUserData), AppError>; - async fn google_callback(&self, code: String) -> Result<(AuthTokens, QrUserData), AppError>; - async fn refresh_token(&self, refresh_token: String) -> Result; -} diff --git a/imphnen-qr/src/auth/infrastructure/http/dto.rs b/imphnen-qr/src/auth/infrastructure/http/dto.rs deleted file mode 100644 index 0cc06f7..0000000 --- a/imphnen-qr/src/auth/infrastructure/http/dto.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use crate::auth::domain::service::QrUserData; - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct RegisterRequest { - pub email: String, - pub password: String, - pub name: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct LoginRequest { - pub email: String, - pub password: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct RefreshRequest { - pub refresh_token: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct AuthResponse { - pub access_token: String, - pub refresh_token: String, - pub user: QrUserData, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct TokensResponse { - pub access_token: String, - pub refresh_token: String, -} diff --git a/imphnen-qr/src/auth/infrastructure/http/handlers.rs b/imphnen-qr/src/auth/infrastructure/http/handlers.rs deleted file mode 100644 index 9036686..0000000 --- a/imphnen-qr/src/auth/infrastructure/http/handlers.rs +++ /dev/null @@ -1,72 +0,0 @@ -use axum::{Extension, Json, response::IntoResponse}; -use axum::extract::Query; -use std::sync::Arc; -use serde::Deserialize; -use imphnen_utils::response_format::ApiSuccess; -use imphnen_utils::errors::AppError; -use crate::auth::domain::service::QrAuthService; -use crate::config::QrConfig; -use super::dto::{RegisterRequest, LoginRequest, RefreshRequest, AuthResponse, TokensResponse}; - -pub async fn register_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - let (tokens, user) = service.register(body.email, body.password, body.name).await?; - Ok(ApiSuccess(AuthResponse { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - user, - }).into_response()) -} - -pub async fn login_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - let (tokens, user) = service.login(body.email, body.password).await?; - Ok(ApiSuccess(AuthResponse { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - user, - }).into_response()) -} - -pub async fn google_redirect_handler( - Extension(config): Extension>, -) -> Result { - let url = format!( - "https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&response_type=code&scope=email+profile", - config.google_client_id, - config.google_redirect_url, - ); - Ok(axum::response::Redirect::temporary(&url).into_response()) -} - -#[derive(Debug, Deserialize)] -pub struct GoogleCallbackQuery { - pub code: String, -} - -pub async fn google_callback_handler( - Extension(service): Extension>, - Query(params): Query, -) -> Result { - let (tokens, user) = service.google_callback(params.code).await?; - Ok(ApiSuccess(AuthResponse { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - user, - }).into_response()) -} - -pub async fn refresh_handler( - Extension(service): Extension>, - Json(body): Json, -) -> Result { - let tokens = service.refresh_token(body.refresh_token).await?; - Ok(ApiSuccess(TokensResponse { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - }).into_response()) -} diff --git a/imphnen-qr/src/auth/infrastructure/http/mod.rs b/imphnen-qr/src/auth/infrastructure/http/mod.rs deleted file mode 100644 index eee210d..0000000 --- a/imphnen-qr/src/auth/infrastructure/http/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod dto; -pub mod handlers; -pub mod routes; diff --git a/imphnen-qr/src/auth/infrastructure/http/routes.rs b/imphnen-qr/src/auth/infrastructure/http/routes.rs deleted file mode 100644 index 4cb070b..0000000 --- a/imphnen-qr/src/auth/infrastructure/http/routes.rs +++ /dev/null @@ -1,29 +0,0 @@ -use axum::{routing::{get, post}, Extension, Router}; -use sqlx::PgPool; -use std::sync::Arc; -use crate::auth::application::auth_service::QrAuthServiceImpl; -use crate::auth::domain::service::QrAuthService; -use crate::common::qr_jwt::QrJwtService; -use crate::config::QrConfig; -use super::handlers::{ - register_handler, - login_handler, - google_redirect_handler, - google_callback_handler, - refresh_handler, -}; - -pub fn qr_auth_routes(pool: Arc, jwt: Arc, config: Arc) -> Router { - let service: Arc = Arc::new( - QrAuthServiceImpl::new(pool, jwt, config.clone()) - ); - - Router::new() - .route("/auth/register", post(register_handler)) - .route("/auth/login", post(login_handler)) - .route("/auth/google", get(google_redirect_handler)) - .route("/auth/google/callback", get(google_callback_handler)) - .route("/auth/refresh", post(refresh_handler)) - .layer(Extension(service)) - .layer(Extension(config)) -} diff --git a/imphnen-qr/src/auth/infrastructure/mod.rs b/imphnen-qr/src/auth/infrastructure/mod.rs deleted file mode 100644 index 4c61c09..0000000 --- a/imphnen-qr/src/auth/infrastructure/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod http; -pub mod persistence; diff --git a/imphnen-qr/src/auth/infrastructure/persistence/mod.rs b/imphnen-qr/src/auth/infrastructure/persistence/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/imphnen-qr/src/auth/infrastructure/persistence/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/imphnen-qr/src/auth/mod.rs b/imphnen-qr/src/auth/mod.rs deleted file mode 100644 index c5ff86f..0000000 --- a/imphnen-qr/src/auth/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod domain; -pub mod application; -pub mod infrastructure; diff --git a/imphnen-qr/src/campaigns/infrastructure/http/routes.rs b/imphnen-qr/src/campaigns/infrastructure/http/routes.rs index 066faf5..3047a0e 100644 --- a/imphnen-qr/src/campaigns/infrastructure/http/routes.rs +++ b/imphnen-qr/src/campaigns/infrastructure/http/routes.rs @@ -18,11 +18,10 @@ use crate::{ persistence::postgres_campaign_repository::PostgresCampaignRepository, }, }, - common::qr_jwt::QrJwtService, middleware::qr_auth::qr_auth_middleware, }; -pub fn qr_campaigns_routes(pool: Arc, jwt: Arc) -> Router { +pub fn qr_campaigns_routes(pool: Arc) -> Router { let repo: Arc = Arc::new(PostgresCampaignRepository::new(pool.clone())); let service: Arc = Arc::new(QrCampaignServiceImpl::new(repo)); @@ -32,7 +31,6 @@ pub fn qr_campaigns_routes(pool: Arc, jwt: Arc) -> Router .route("/campaigns/:id", delete(delete_campaign_handler)) .route("/campaigns/process-image", post(process_image_handler)) .layer(Extension(service)) - .layer(Extension(jwt.clone())) .layer(Extension(pool)) .layer(from_fn(qr_auth_middleware)) } diff --git a/imphnen-qr/src/common/mod.rs b/imphnen-qr/src/common/mod.rs index b01715b..8b13789 100644 --- a/imphnen-qr/src/common/mod.rs +++ b/imphnen-qr/src/common/mod.rs @@ -1 +1 @@ -pub mod qr_jwt; + diff --git a/imphnen-qr/src/common/qr_jwt.rs b/imphnen-qr/src/common/qr_jwt.rs deleted file mode 100644 index 701280e..0000000 --- a/imphnen-qr/src/common/qr_jwt.rs +++ /dev/null @@ -1,59 +0,0 @@ -use chrono::{Duration, Utc}; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use imphnen_utils::errors::AppError; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct QrClaims { - pub sub: String, - pub role: String, - pub exp: usize, -} - -#[derive(Clone)] -pub struct QrJwtService { - encoding_key: EncodingKey, - decoding_key: DecodingKey, - expiry_minutes: i64, - refresh_expiry_days: i64, -} - -impl QrJwtService { - pub fn new(secret: &str, expiry_minutes: i64, refresh_expiry_days: i64) -> Self { - Self { - encoding_key: EncodingKey::from_secret(secret.as_bytes()), - decoding_key: DecodingKey::from_secret(secret.as_bytes()), - expiry_minutes, - refresh_expiry_days, - } - } - - pub fn generate_token(&self, user_id: Uuid, role: &str) -> Result { - let exp = (Utc::now() + Duration::minutes(self.expiry_minutes)).timestamp() as usize; - let claims = QrClaims { - sub: user_id.to_string(), - role: role.to_string(), - exp, - }; - encode(&Header::default(), &claims, &self.encoding_key) - .map_err(|e| AppError::InternalServerError(e.to_string())) - } - - pub fn generate_refresh_token(&self, user_id: Uuid, role: &str) -> Result { - let exp = (Utc::now() + Duration::days(self.refresh_expiry_days)).timestamp() as usize; - let claims = QrClaims { - sub: user_id.to_string(), - role: role.to_string(), - exp, - }; - encode(&Header::default(), &claims, &self.encoding_key) - .map_err(|e| AppError::InternalServerError(e.to_string())) - } - - pub fn verify_token(&self, token: &str) -> Result { - decode::(token, &self.decoding_key, &Validation::default()) - .map(|d| d.claims) - .map_err(|_| AppError::AuthenticationError("Invalid or expired token".to_string())) - } -} diff --git a/imphnen-qr/src/config.rs b/imphnen-qr/src/config.rs deleted file mode 100644 index 2fd0b5c..0000000 --- a/imphnen-qr/src/config.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::env; - -#[derive(Debug, Clone)] -pub struct QrConfig { - pub jwt_secret: String, - pub jwt_expiry_minutes: i64, - pub refresh_expiry_days: i64, - pub google_client_id: String, - pub google_client_secret: String, - pub google_redirect_url: String, -} - -impl QrConfig { - pub fn from_env() -> Self { - Self { - jwt_secret: env::var("QR_JWT_SECRET").expect("QR_JWT_SECRET must be set"), - jwt_expiry_minutes: env::var("QR_JWT_EXPIRY_MINUTES") - .unwrap_or_else(|_| "15".to_string()) - .parse() - .unwrap_or(15), - refresh_expiry_days: env::var("QR_JWT_REFRESH_EXPIRY_DAYS") - .unwrap_or_else(|_| "7".to_string()) - .parse() - .unwrap_or(7), - google_client_id: env::var("QR_GOOGLE_CLIENT_ID").unwrap_or_default(), - google_client_secret: env::var("QR_GOOGLE_CLIENT_SECRET").unwrap_or_default(), - google_redirect_url: env::var("QR_GOOGLE_REDIRECT_URL").unwrap_or_default(), - } - } -} diff --git a/imphnen-qr/src/lib.rs b/imphnen-qr/src/lib.rs index 07b37c1..c11e9ed 100644 --- a/imphnen-qr/src/lib.rs +++ b/imphnen-qr/src/lib.rs @@ -1,26 +1,14 @@ -pub mod config; pub mod common; pub mod middleware; -pub mod auth; pub mod users; pub mod campaigns; -pub use config::QrConfig; - use axum::Router; use sqlx::PgPool; use std::sync::Arc; -use common::qr_jwt::QrJwtService; - -pub fn qr_router(pool: Arc, config: Arc) -> Router { - let jwt = Arc::new(QrJwtService::new( - &config.jwt_secret, - config.jwt_expiry_minutes, - config.refresh_expiry_days, - )); +pub fn qr_router(pool: Arc) -> Router { Router::new() - .merge(auth::infrastructure::http::routes::qr_auth_routes(pool.clone(), jwt.clone(), config.clone())) - .merge(users::infrastructure::http::routes::qr_users_routes(pool.clone(), jwt.clone())) - .merge(campaigns::infrastructure::http::routes::qr_campaigns_routes(pool.clone(), jwt.clone())) + .merge(users::infrastructure::http::routes::qr_users_routes(pool.clone())) + .merge(campaigns::infrastructure::http::routes::qr_campaigns_routes(pool)) } diff --git a/imphnen-qr/src/middleware/qr_auth.rs b/imphnen-qr/src/middleware/qr_auth.rs index 26ae881..6bd3df7 100644 --- a/imphnen-qr/src/middleware/qr_auth.rs +++ b/imphnen-qr/src/middleware/qr_auth.rs @@ -1,9 +1,10 @@ use axum::{body::Body, extract::Request, middleware::Next, response::{IntoResponse, Response}}; use axum::http::StatusCode; +use sqlx::PgPool; use std::sync::Arc; use uuid::Uuid; use serde::{Deserialize, Serialize}; -use crate::common::qr_jwt::QrJwtService; +use imphnen_libs::decode_access_token; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QrAuthUser { @@ -12,7 +13,7 @@ pub struct QrAuthUser { } pub async fn qr_auth_middleware( - axum::Extension(jwt_service): axum::Extension>, + axum::Extension(pool): axum::Extension>, mut request: Request, next: Next, ) -> Result { @@ -26,14 +27,29 @@ pub async fn qr_auth_middleware( (StatusCode::UNAUTHORIZED, "Invalid Authorization header format").into_response() })?; - let claims = jwt_service.verify_token(token).map_err(|_| { + let token_data = decode_access_token(token).map_err(|_| { (StatusCode::UNAUTHORIZED, "Invalid or expired token").into_response() })?; - let user_id = Uuid::parse_str(&claims.sub).map_err(|_| { + let user_id = Uuid::parse_str(&token_data.claims.user_id).map_err(|_| { (StatusCode::UNAUTHORIZED, "Invalid user ID in token").into_response() })?; - request.extensions_mut().insert(QrAuthUser { user_id, role: claims.role }); + let _ = sqlx::query( + "INSERT INTO users (id, email, name, role, provider) VALUES ($1, $2, $2, 'user', 'external') ON CONFLICT (id) DO NOTHING" + ) + .bind(user_id) + .bind(&token_data.claims.sub) + .execute(pool.as_ref()) + .await; + + let role: String = sqlx::query_scalar("SELECT role FROM users WHERE id = $1") + .bind(user_id) + .fetch_optional(pool.as_ref()) + .await + .unwrap_or(None) + .unwrap_or_else(|| "user".to_string()); + + request.extensions_mut().insert(QrAuthUser { user_id, role }); Ok(next.run(request).await) } diff --git a/imphnen-qr/src/users/infrastructure/http/routes.rs b/imphnen-qr/src/users/infrastructure/http/routes.rs index 64b35e0..a27976b 100644 --- a/imphnen-qr/src/users/infrastructure/http/routes.rs +++ b/imphnen-qr/src/users/infrastructure/http/routes.rs @@ -7,7 +7,6 @@ use sqlx::PgPool; use std::sync::Arc; use crate::{ - common::qr_jwt::QrJwtService, middleware::qr_auth::qr_auth_middleware, users::{ application::user_service::QrUserServiceImpl, @@ -22,7 +21,7 @@ use crate::{ }, }; -pub fn qr_users_routes(pool: Arc, jwt: Arc) -> Router { +pub fn qr_users_routes(pool: Arc) -> Router { let repo: Arc = Arc::new(PostgresUserRepository::new(pool.clone())); let service: Arc = Arc::new(QrUserServiceImpl::new(repo)); @@ -32,7 +31,6 @@ pub fn qr_users_routes(pool: Arc, jwt: Arc) -> Router { .route("/users/:id/role", put(update_role_handler)) .route("/users/:id", delete(delete_user_handler)) .layer(Extension(service)) - .layer(Extension(jwt.clone())) .layer(Extension(pool)) .layer(from_fn(qr_auth_middleware)) }