refactor: centralize auth system across all modules
All modules now use the main IAM JWT (ACCESS_TOKEN_SECRET) for authentication, removing three separate auth systems (hackathon Supabase, hackathon JWT, QR JWT). Changes: - hackathon: replace HackathonJwtService with decode_access_token() from imphnen-libs - remove entire src/auth/ (Supabase signup/login/GitHub/forgot-reset) - remove common/hackathon_jwt.rs, common/supabase_client.rs - remove Supabase from HackathonConfig (JWT, GitHub OAuth, Supabase anon/service keys) - replace Supabase Storage with MinioService from imphnen-libs - all route jwt params removed; hackathon_router takes MinioService instead - qr: replace QrJwtService with decode_access_token() from imphnen-libs - remove entire src/auth/ (register/login/Google OAuth/refresh) - remove common/qr_jwt.rs, src/config.rs - qr_auth_middleware now lazy-upserts users into QR DB on first access - qr_router(pool) — no config needed - gateway: create MinioService once and pass to hackathon_router; qr_router simplified Users now register/login via /v1/auth/* and use the same JWT for all endpoints. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4bba182ea3
commit
2ae43b3bcc
Generated
+1
-49
@@ -434,19 +434,6 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
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]]
|
[[package]]
|
||||||
name = "bigdecimal"
|
name = "bigdecimal"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -515,16 +502,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "borsh"
|
name = "borsh"
|
||||||
version = "1.5.7"
|
version = "1.5.7"
|
||||||
@@ -660,16 +637,6 @@ dependencies = [
|
|||||||
"stacker",
|
"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]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1942,12 +1909,10 @@ version = "0.2.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
"jsonwebtoken",
|
|
||||||
"lettre",
|
"lettre",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
@@ -2082,15 +2047,11 @@ version = "0.2.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
|
||||||
"bcrypt",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"image",
|
"image",
|
||||||
|
"imphnen-libs",
|
||||||
"imphnen-utils",
|
"imphnen-utils",
|
||||||
"jsonwebtoken",
|
|
||||||
"oauth2",
|
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"reqwest",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -2154,15 +2115,6 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"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]]
|
[[package]]
|
||||||
name = "interpolate_name"
|
name = "interpolate_name"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ use imphnen_dimentorin::{
|
|||||||
};
|
};
|
||||||
use imphnen_gacha::gacha_router;
|
use imphnen_gacha::gacha_router;
|
||||||
use imphnen_hackathon::{hackathon_router, HackathonConfig};
|
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::{
|
use imphnen_iam::{
|
||||||
auth_public_routes,
|
auth_public_routes,
|
||||||
permissions_protected_routes,
|
permissions_protected_routes,
|
||||||
@@ -46,7 +47,11 @@ pub async fn gateway_service(
|
|||||||
let db = state.postgres_connection.conn.clone();
|
let db = state.postgres_connection.conn.clone();
|
||||||
let state_arc = Arc::new(state.clone());
|
let state_arc = Arc::new(state.clone());
|
||||||
let hackathon_config = Arc::new(HackathonConfig::from_env());
|
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(
|
let qr_pool = Arc::new(
|
||||||
sqlx::PgPool::connect(
|
sqlx::PgPool::connect(
|
||||||
&std::env::var("QR_DATABASE_URL").expect("QR_DATABASE_URL must be set"),
|
&std::env::var("QR_DATABASE_URL").expect("QR_DATABASE_URL must be set"),
|
||||||
@@ -76,8 +81,8 @@ pub async fn gateway_service(
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(Redirect::to("/docs")))
|
.route("/", get(Redirect::to("/docs")))
|
||||||
.nest("/v1", public_routes.merge(protected_routes))
|
.nest("/v1", public_routes.merge(protected_routes))
|
||||||
.nest("/v1/hackathon", hackathon_router(db.clone(), hackathon_config))
|
.nest("/v1/hackathon", hackathon_router(db.clone(), hackathon_config, minio))
|
||||||
.nest("/v1/qr", qr_router(qr_pool, qr_config))
|
.nest("/v1/qr", qr_router(qr_pool))
|
||||||
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
||||||
.layer(cors_middleware())
|
.layer(cors_middleware())
|
||||||
.layer(from_fn(security_headers_middleware))
|
.layer(from_fn(security_headers_middleware))
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ edition = "2024"
|
|||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
imphnen-libs.workspace = true
|
imphnen-libs.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
axum-extra.workspace = true
|
|
||||||
sea-orm.workspace = true
|
sea-orm.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
@@ -20,6 +19,5 @@ tokio.workspace = true
|
|||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
lettre.workspace = true
|
lettre.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
jsonwebtoken.workspace = true
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use imphnen_utils::{errors::AppError, response_format::{ApiSuccess, ApiMessage}};
|
use imphnen_utils::{errors::AppError, response_format::{ApiSuccess, ApiMessage}};
|
||||||
use crate::middleware::{admin_only::admin_only, hackathon_auth::hackathon_auth_middleware};
|
use crate::middleware::{admin_only::admin_only, hackathon_auth::hackathon_auth_middleware};
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PageQuery {
|
struct PageQuery {
|
||||||
@@ -173,7 +172,7 @@ async fn admin_list_winners(
|
|||||||
Ok(ApiSuccess(rows).into_response())
|
Ok(ApiSuccess(rows).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hackathon_admin_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn hackathon_admin_routes(pool: Arc<PgPool>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/admin/users", get(admin_list_users))
|
.route("/admin/users", get(admin_list_users))
|
||||||
.route("/admin/users/:user_id", get(admin_get_user).delete(admin_delete_user))
|
.route("/admin/users/:user_id", get(admin_get_user).delete(admin_delete_user))
|
||||||
@@ -185,7 +184,6 @@ pub fn hackathon_admin_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>)
|
|||||||
.route("/admin/winners/:team_id", delete(admin_remove_winner))
|
.route("/admin/winners/:team_id", delete(admin_remove_winner))
|
||||||
.layer(Extension(pool.clone()))
|
.layer(Extension(pool.clone()))
|
||||||
.layer(from_fn(admin_only))
|
.layer(from_fn(admin_only))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PgPool>,
|
|
||||||
jwt: Arc<HackathonJwtService>,
|
|
||||||
supabase: Arc<SupabaseClient>,
|
|
||||||
config: Arc<HackathonConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HackathonAuthServiceImpl {
|
|
||||||
pub fn new(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>, supabase: Arc<SupabaseClient>, config: Arc<HackathonConfig>) -> Self {
|
|
||||||
Self { pool, jwt, supabase, config }
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_user_by_id(&self, user_id: Uuid) -> Result<HackathonUserData, AppError> {
|
|
||||||
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<HackathonUserData, AppError> {
|
|
||||||
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<HackathonUserData, AppError> {
|
|
||||||
let existing: Option<HackathonUserData> = 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<serde_json::Value> = 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<HackathonUserData, AppError> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod auth_service;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod service;
|
|
||||||
@@ -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<String>,
|
|
||||||
pub phone_number: Option<String>,
|
|
||||||
pub location: Option<String>,
|
|
||||||
pub bio: Option<String>,
|
|
||||||
pub skills: Option<Vec<String>>,
|
|
||||||
pub is_active: Option<bool>,
|
|
||||||
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
||||||
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<HackathonUserData, AppError>;
|
|
||||||
async fn forgot_password(&self, email: String) -> Result<(), AppError>;
|
|
||||||
async fn reset_password(&self, access_token: String, new_password: String) -> Result<(), AppError>;
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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<Arc<dyn HackathonAuthService>>,
|
|
||||||
Json(body): Json<SignupRequest>,
|
|
||||||
) -> Result<ApiMessage, imphnen_utils::errors::AppError> {
|
|
||||||
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<Arc<dyn HackathonAuthService>>,
|
|
||||||
Json(body): Json<LoginRequest>,
|
|
||||||
) -> Result<axum::response::Response, imphnen_utils::errors::AppError> {
|
|
||||||
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<Arc<dyn HackathonAuthService>>,
|
|
||||||
Json(body): Json<GitHubAuthRequest>,
|
|
||||||
) -> Result<axum::response::Response, imphnen_utils::errors::AppError> {
|
|
||||||
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<Arc<dyn HackathonAuthService>>,
|
|
||||||
Extension(auth_user): Extension<HackathonAuthUser>,
|
|
||||||
) -> Result<axum::response::Response, imphnen_utils::errors::AppError> {
|
|
||||||
let user = service.get_session(auth_user.user_id).await?;
|
|
||||||
Ok(ApiSuccess(user).into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn forgot_password_handler(
|
|
||||||
Extension(service): Extension<Arc<dyn HackathonAuthService>>,
|
|
||||||
Json(body): Json<ForgotPasswordRequest>,
|
|
||||||
) -> Result<ApiMessage, imphnen_utils::errors::AppError> {
|
|
||||||
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<Arc<dyn HackathonAuthService>>,
|
|
||||||
Json(body): Json<ResetPasswordRequest>,
|
|
||||||
) -> Result<ApiMessage, imphnen_utils::errors::AppError> {
|
|
||||||
service.reset_password(body.access_token, body.new_password).await?;
|
|
||||||
Ok(ApiMessage::ok("Password has been successfully reset."))
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod dto;
|
|
||||||
pub mod handlers;
|
|
||||||
pub mod routes;
|
|
||||||
@@ -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<PgPool>, jwt: Arc<HackathonJwtService>, supabase: Arc<SupabaseClient>, config: Arc<HackathonConfig>) -> Router {
|
|
||||||
let service: Arc<dyn HackathonAuthService> = 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)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod http;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
pub mod domain;
|
|
||||||
pub mod application;
|
|
||||||
pub mod infrastructure;
|
|
||||||
|
|
||||||
pub use infrastructure::http::routes::hackathon_auth_routes;
|
|
||||||
@@ -4,11 +4,10 @@ use std::sync::Arc;
|
|||||||
use crate::chat::application::chat_service::ChatServiceImpl;
|
use crate::chat::application::chat_service::ChatServiceImpl;
|
||||||
use crate::chat::domain::service::ChatService;
|
use crate::chat::domain::service::ChatService;
|
||||||
use crate::chat::infrastructure::persistence::PostgresChatRepository;
|
use crate::chat::infrastructure::persistence::PostgresChatRepository;
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||||
use super::handlers::*;
|
use super::handlers::*;
|
||||||
|
|
||||||
pub fn build_chat_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn build_chat_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let service: Arc<dyn ChatService> = Arc::new(ChatServiceImpl::new(
|
let service: Arc<dyn ChatService> = Arc::new(ChatServiceImpl::new(
|
||||||
Arc::new(PostgresChatRepository::new(pool.clone())),
|
Arc::new(PostgresChatRepository::new(pool.clone())),
|
||||||
));
|
));
|
||||||
@@ -16,7 +15,6 @@ pub fn build_chat_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Ro
|
|||||||
.route("/chat/teams/:team_id", get(get_team_messages_handler).post(send_message_handler))
|
.route("/chat/teams/:team_id", get(get_team_messages_handler).post(send_message_handler))
|
||||||
.route("/chat/messages/:message_id", delete(delete_message_handler))
|
.route("/chat/messages/:message_id", delete(delete_message_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String, AppError> {
|
|
||||||
self.generate_token_with_type(user_id, "access", self.expiry_hours)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_refresh_token(&self, user_id: Uuid) -> Result<String, AppError> {
|
|
||||||
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<String, AppError> {
|
|
||||||
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<HackathonClaims, AppError> {
|
|
||||||
decode::<HackathonClaims>(token, &self.decoding_key, &Validation::default())
|
|
||||||
.map(|d| d.claims)
|
|
||||||
.map_err(|_| AppError::AuthenticationError("Invalid or expired token".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
pub mod cities;
|
pub mod cities;
|
||||||
pub mod hackathon_jwt;
|
|
||||||
pub mod supabase_client;
|
|
||||||
|
|||||||
@@ -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<Value, AppError> {
|
|
||||||
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::<Value>().await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(&self, email: &str, password: &str) -> Result<Value, AppError> {
|
|
||||||
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::<Value>().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<String, AppError> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,42 +2,20 @@ use std::env;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HackathonConfig {
|
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_host: String,
|
||||||
pub smtp_user: String,
|
pub smtp_user: String,
|
||||||
pub smtp_password: String,
|
pub smtp_password: String,
|
||||||
pub from_email: String,
|
pub from_email: String,
|
||||||
pub storage_bucket: String,
|
|
||||||
pub frontend_url: String,
|
pub frontend_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HackathonConfig {
|
impl HackathonConfig {
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
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_host: env::var("HACKATHON_SMTP_HOST").unwrap_or_default(),
|
||||||
smtp_user: env::var("HACKATHON_SMTP_USER").unwrap_or_default(),
|
smtp_user: env::var("HACKATHON_SMTP_USER").unwrap_or_default(),
|
||||||
smtp_password: env::var("HACKATHON_SMTP_PASSWORD").unwrap_or_default(),
|
smtp_password: env::var("HACKATHON_SMTP_PASSWORD").unwrap_or_default(),
|
||||||
from_email: env::var("HACKATHON_FROM_EMAIL").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")
|
frontend_url: env::var("HACKATHON_FRONTEND_URL")
|
||||||
.unwrap_or_else(|_| "https://hackathon.imphnen.dev".to_string()),
|
.unwrap_or_else(|_| "https://hackathon.imphnen.dev".to_string()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ use std::sync::Arc;
|
|||||||
use crate::invitations::application::invitation_service::InvitationServiceImpl;
|
use crate::invitations::application::invitation_service::InvitationServiceImpl;
|
||||||
use crate::invitations::domain::service::InvitationService;
|
use crate::invitations::domain::service::InvitationService;
|
||||||
use crate::invitations::infrastructure::persistence::PostgresInvitationRepository;
|
use crate::invitations::infrastructure::persistence::PostgresInvitationRepository;
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||||
use super::handlers::*;
|
use super::handlers::*;
|
||||||
|
|
||||||
pub fn build_invitation_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn build_invitation_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let service: Arc<dyn InvitationService> = Arc::new(InvitationServiceImpl::new(
|
let service: Arc<dyn InvitationService> = Arc::new(InvitationServiceImpl::new(
|
||||||
Arc::new(PostgresInvitationRepository::new(pool.clone())),
|
Arc::new(PostgresInvitationRepository::new(pool.clone())),
|
||||||
));
|
));
|
||||||
@@ -17,7 +16,6 @@ pub fn build_invitation_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>)
|
|||||||
.route("/invitations/:invitation_id/respond", post(respond_to_invitation_handler))
|
.route("/invitations/:invitation_id/respond", post(respond_to_invitation_handler))
|
||||||
.route("/invitations/teams/:team_id/invite", post(invite_team_member_handler))
|
.route("/invitations/teams/:team_id/invite", post(invite_team_member_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ use std::sync::Arc;
|
|||||||
use crate::join_requests::application::join_request_service::JoinRequestServiceImpl;
|
use crate::join_requests::application::join_request_service::JoinRequestServiceImpl;
|
||||||
use crate::join_requests::domain::service::JoinRequestService;
|
use crate::join_requests::domain::service::JoinRequestService;
|
||||||
use crate::join_requests::infrastructure::persistence::PostgresJoinRequestRepository;
|
use crate::join_requests::infrastructure::persistence::PostgresJoinRequestRepository;
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||||
use super::handlers::*;
|
use super::handlers::*;
|
||||||
|
|
||||||
pub fn build_join_request_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn build_join_request_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let service: Arc<dyn JoinRequestService> = Arc::new(JoinRequestServiceImpl::new(
|
let service: Arc<dyn JoinRequestService> = Arc::new(JoinRequestServiceImpl::new(
|
||||||
Arc::new(PostgresJoinRequestRepository::new(pool.clone())),
|
Arc::new(PostgresJoinRequestRepository::new(pool.clone())),
|
||||||
));
|
));
|
||||||
@@ -18,7 +17,6 @@ pub fn build_join_request_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService
|
|||||||
.route("/join-requests/teams/:team_id/pending", get(get_team_join_requests_handler))
|
.route("/join-requests/teams/:team_id/pending", get(get_team_join_requests_handler))
|
||||||
.route("/join-requests/:request_id/respond", post(respond_to_join_request_handler))
|
.route("/join-requests/:request_id/respond", post(respond_to_join_request_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ pub mod config;
|
|||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod auth;
|
|
||||||
pub mod certificates;
|
pub mod certificates;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
@@ -14,7 +13,6 @@ pub mod join_requests;
|
|||||||
pub mod winners;
|
pub mod winners;
|
||||||
|
|
||||||
pub use admin::hackathon_admin_routes;
|
pub use admin::hackathon_admin_routes;
|
||||||
pub use auth::hackathon_auth_routes;
|
|
||||||
pub use certificates::hackathon_certificates_routes;
|
pub use certificates::hackathon_certificates_routes;
|
||||||
pub use chat::build_chat_routes;
|
pub use chat::build_chat_routes;
|
||||||
pub use storage::hackathon_storage_routes;
|
pub use storage::hackathon_storage_routes;
|
||||||
@@ -29,28 +27,20 @@ pub use config::HackathonConfig;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use common::{hackathon_jwt::HackathonJwtService, supabase_client::SupabaseClient};
|
use imphnen_libs::MinioService;
|
||||||
|
|
||||||
pub fn hackathon_router(db: DatabaseConnection, config: Arc<HackathonConfig>) -> Router {
|
pub fn hackathon_router(db: DatabaseConnection, _config: Arc<HackathonConfig>, minio: Arc<MinioService>) -> Router {
|
||||||
let pool = Arc::new(db.get_postgres_connection_pool().clone());
|
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()
|
Router::new()
|
||||||
.merge(hackathon_auth_routes(pool.clone(), jwt.clone(), supabase.clone(), config.clone()))
|
.merge(hackathon_users_routes(pool.clone()))
|
||||||
.merge(hackathon_users_routes(pool.clone(), jwt.clone()))
|
.merge(build_team_routes(pool.clone()))
|
||||||
.merge(build_team_routes(pool.clone(), jwt.clone()))
|
.merge(build_invitation_routes(pool.clone()))
|
||||||
.merge(build_invitation_routes(pool.clone(), jwt.clone()))
|
.merge(build_join_request_routes(pool.clone()))
|
||||||
.merge(build_join_request_routes(pool.clone(), jwt.clone()))
|
.merge(build_chat_routes(pool.clone()))
|
||||||
.merge(build_chat_routes(pool.clone(), jwt.clone()))
|
.merge(hackathon_submissions_routes(pool.clone()))
|
||||||
.merge(hackathon_submissions_routes(pool.clone(), jwt.clone()))
|
.merge(hackathon_storage_routes(pool.clone(), minio))
|
||||||
.merge(hackathon_storage_routes(pool.clone(), jwt.clone(), supabase))
|
|
||||||
.merge(hackathon_certificates_routes(pool.clone()))
|
.merge(hackathon_certificates_routes(pool.clone()))
|
||||||
.merge(hackathon_winners_routes(pool.clone()))
|
.merge(hackathon_winners_routes(pool.clone()))
|
||||||
.merge(hackathon_admin_routes(pool, jwt))
|
.merge(hackathon_admin_routes(pool))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use sqlx::PgPool;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
use imphnen_libs::decode_access_token;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct HackathonAuthUser {
|
pub struct HackathonAuthUser {
|
||||||
@@ -13,7 +13,6 @@ pub struct HackathonAuthUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hackathon_auth_middleware(
|
pub async fn hackathon_auth_middleware(
|
||||||
axum::Extension(jwt_service): axum::Extension<Arc<HackathonJwtService>>,
|
|
||||||
axum::Extension(pool): axum::Extension<Arc<PgPool>>,
|
axum::Extension(pool): axum::Extension<Arc<PgPool>>,
|
||||||
mut request: Request<Body>,
|
mut request: Request<Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
@@ -28,20 +27,22 @@ pub async fn hackathon_auth_middleware(
|
|||||||
(StatusCode::UNAUTHORIZED, "Invalid Authorization header format").into_response()
|
(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()
|
(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()
|
(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")
|
let is_admin: bool = sqlx::query_scalar(
|
||||||
.bind(user_id)
|
"SELECT COALESCE(is_admin, false) FROM hackathon_users WHERE id = $1"
|
||||||
.fetch_optional(pool.as_ref())
|
)
|
||||||
.await
|
.bind(user_id)
|
||||||
.unwrap_or(None)
|
.fetch_optional(pool.as_ref())
|
||||||
.unwrap_or(false);
|
.await
|
||||||
|
.unwrap_or(None)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
request.extensions_mut().insert(HackathonAuthUser { user_id, is_admin });
|
request.extensions_mut().insert(HackathonAuthUser { user_id, is_admin });
|
||||||
Ok(next.run(request).await)
|
Ok(next.run(request).await)
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use std::sync::Arc;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
|
use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
use imphnen_libs::MinioService;
|
||||||
use crate::common::supabase_client::SupabaseClient;
|
|
||||||
use crate::middleware::hackathon_auth::{hackathon_auth_middleware, HackathonAuthUser};
|
use crate::middleware::hackathon_auth::{hackathon_auth_middleware, HackathonAuthUser};
|
||||||
use super::service::StorageService;
|
use super::service::StorageService;
|
||||||
|
|
||||||
@@ -57,15 +56,14 @@ async fn upload_submission_handler(
|
|||||||
Ok(ApiSuccess(UploadResponse { url }).into_response())
|
Ok(ApiSuccess(UploadResponse { url }).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hackathon_storage_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>, supabase: Arc<SupabaseClient>) -> Router {
|
pub fn hackathon_storage_routes(pool: Arc<PgPool>, minio: Arc<MinioService>) -> Router {
|
||||||
let service = Arc::new(StorageService::new(supabase));
|
let service = Arc::new(StorageService::new(minio));
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/upload", post(upload_file_handler))
|
.route("/upload", post(upload_file_handler))
|
||||||
.route("/upload/avatar", post(upload_avatar_handler))
|
.route("/upload/avatar", post(upload_avatar_handler))
|
||||||
.route("/upload/team", post(upload_team_handler))
|
.route("/upload/team", post(upload_team_handler))
|
||||||
.route("/upload/submission", post(upload_submission_handler))
|
.route("/upload/submission", post(upload_submission_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use base64::Engine;
|
|
||||||
use chrono::Utc;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use chrono::Utc;
|
||||||
use imphnen_utils::errors::AppError;
|
use imphnen_utils::errors::AppError;
|
||||||
use crate::common::supabase_client::SupabaseClient;
|
use imphnen_libs::MinioService;
|
||||||
|
|
||||||
pub struct StorageService {
|
pub struct StorageService {
|
||||||
supabase: Arc<SupabaseClient>,
|
minio: Arc<MinioService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StorageService {
|
impl StorageService {
|
||||||
pub fn new(supabase: Arc<SupabaseClient>) -> Self { Self { supabase } }
|
pub fn new(minio: Arc<MinioService>) -> Self { Self { minio } }
|
||||||
|
|
||||||
pub async fn upload(&self, folder: &str, user_id: Uuid, filename: &str, content_type: &str, data_base64: &str) -> Result<String, AppError> {
|
pub async fn upload(&self, folder: &str, user_id: Uuid, filename: &str, content_type: &str, data_base64: &str) -> Result<String, AppError> {
|
||||||
let ext = filename.rsplit('.').next().unwrap_or("bin");
|
let ext = filename.rsplit('.').next().unwrap_or("bin");
|
||||||
let path = format!("{}/{}-{}.{}", folder, user_id, Utc::now().timestamp_millis(), ext);
|
let unique_name = format!("{}-{}.{}", user_id, Utc::now().timestamp_millis(), ext);
|
||||||
let data = base64::engine::general_purpose::STANDARD.decode(data_base64)
|
self.minio
|
||||||
.map_err(|_| AppError::BadRequestError("Invalid base64 data".to_string()))?;
|
.upload_base64_file(data_base64, content_type, folder, &unique_name)
|
||||||
self.supabase.upload_file(&path, content_type, &data).await
|
.await
|
||||||
|
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ use std::sync::Arc;
|
|||||||
use crate::submissions::application::submission_service::SubmissionServiceImpl;
|
use crate::submissions::application::submission_service::SubmissionServiceImpl;
|
||||||
use crate::submissions::domain::service::SubmissionService;
|
use crate::submissions::domain::service::SubmissionService;
|
||||||
use crate::submissions::infrastructure::persistence::PostgresSubmissionRepository;
|
use crate::submissions::infrastructure::persistence::PostgresSubmissionRepository;
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||||
use super::handlers::*;
|
use super::handlers::*;
|
||||||
|
|
||||||
pub fn hackathon_submissions_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn hackathon_submissions_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let service: Arc<dyn SubmissionService> = Arc::new(SubmissionServiceImpl::new(Arc::new(PostgresSubmissionRepository::new(pool.clone()))));
|
let service: Arc<dyn SubmissionService> = Arc::new(SubmissionServiceImpl::new(Arc::new(PostgresSubmissionRepository::new(pool.clone()))));
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/submissions/teams/:team_id", get(get_team_submission_handler).post(create_submission_handler))
|
.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<PgPool>, jwt: Arc<HackathonJwtServ
|
|||||||
.route("/submissions/:submission_id/confirm", post(confirm_submission_handler))
|
.route("/submissions/:submission_id/confirm", post(confirm_submission_handler))
|
||||||
.route("/submissions/:submission_id/cancel", post(cancel_submission_handler))
|
.route("/submissions/:submission_id/cancel", post(cancel_submission_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ use std::sync::Arc;
|
|||||||
use crate::teams::application::team_service::TeamServiceImpl;
|
use crate::teams::application::team_service::TeamServiceImpl;
|
||||||
use crate::teams::domain::service::TeamService;
|
use crate::teams::domain::service::TeamService;
|
||||||
use crate::teams::infrastructure::persistence::PostgresTeamRepository;
|
use crate::teams::infrastructure::persistence::PostgresTeamRepository;
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||||
use super::handlers::*;
|
use super::handlers::*;
|
||||||
|
|
||||||
pub fn build_team_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn build_team_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let repo = Arc::new(PostgresTeamRepository::new(pool.clone()));
|
let repo = Arc::new(PostgresTeamRepository::new(pool.clone()));
|
||||||
let service: Arc<dyn TeamService> = Arc::new(TeamServiceImpl::new(repo));
|
let service: Arc<dyn TeamService> = Arc::new(TeamServiceImpl::new(repo));
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ pub fn build_team_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Ro
|
|||||||
.route("/teams/:team_id/members/:member_id", delete(remove_member_handler))
|
.route("/teams/:team_id/members/:member_id", delete(remove_member_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(pool.clone()))
|
.layer(Extension(pool.clone()))
|
||||||
.layer(Extension(jwt))
|
|
||||||
.layer(from_fn(hackathon_auth_middleware));
|
.layer(from_fn(hackathon_auth_middleware));
|
||||||
|
|
||||||
Router::new().merge(public).merge(protected)
|
Router::new().merge(public).merge(protected)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
|||||||
use crate::users::application::user_service::HackathonUserServiceImpl;
|
use crate::users::application::user_service::HackathonUserServiceImpl;
|
||||||
use crate::users::domain::service::HackathonUserService;
|
use crate::users::domain::service::HackathonUserService;
|
||||||
use crate::users::infrastructure::persistence::PostgresHackathonUserRepository;
|
use crate::users::infrastructure::persistence::PostgresHackathonUserRepository;
|
||||||
use crate::common::hackathon_jwt::HackathonJwtService;
|
|
||||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||||
use super::handlers::*;
|
use super::handlers::*;
|
||||||
|
|
||||||
@@ -13,14 +12,13 @@ fn build_service(pool: Arc<PgPool>) -> Arc<dyn HackathonUserService> {
|
|||||||
Arc::new(HackathonUserServiceImpl::new(repo))
|
Arc::new(HackathonUserServiceImpl::new(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hackathon_users_routes(pool: Arc<PgPool>, jwt: Arc<HackathonJwtService>) -> Router {
|
pub fn hackathon_users_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let service = build_service(pool.clone());
|
let service = build_service(pool.clone());
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/users/me", get(get_me_handler).put(update_me_handler))
|
.route("/users/me", get(get_me_handler).put(update_me_handler))
|
||||||
.route("/users/:user_id", get(get_user_handler))
|
.route("/users/:user_id", get(get_user_handler))
|
||||||
.route("/users/:user_id/teams", get(get_user_teams_handler))
|
.route("/users/:user_id/teams", get(get_user_teams_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(hackathon_auth_middleware))
|
.layer(from_fn(hackathon_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,15 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
imphnen-utils.workspace = true
|
imphnen-utils.workspace = true
|
||||||
|
imphnen-libs.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
axum-extra.workspace = true
|
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
jsonwebtoken.workspace = true
|
|
||||||
bcrypt.workspace = true
|
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
reqwest.workspace = true
|
|
||||||
oauth2.workspace = true
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
|
|||||||
@@ -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<PgPool>,
|
|
||||||
jwt: Arc<QrJwtService>,
|
|
||||||
config: Arc<QrConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QrAuthServiceImpl {
|
|
||||||
pub fn new(pool: Arc<PgPool>, jwt: Arc<QrJwtService>, config: Arc<QrConfig>) -> Self {
|
|
||||||
Self { pool, jwt, config }
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_user_by_id(&self, id: Uuid) -> Result<QrUserData, AppError> {
|
|
||||||
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<Option<serde_json::Value>, 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<AuthTokens, AppError> {
|
|
||||||
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<AuthTokens, AppError> {
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod auth_service;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod service;
|
|
||||||
@@ -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<chrono::DateTime<chrono::Utc>>,
|
|
||||||
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<AuthTokens, AppError>;
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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<Arc<dyn QrAuthService>>,
|
|
||||||
Json(body): Json<RegisterRequest>,
|
|
||||||
) -> Result<axum::response::Response, AppError> {
|
|
||||||
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<Arc<dyn QrAuthService>>,
|
|
||||||
Json(body): Json<LoginRequest>,
|
|
||||||
) -> Result<axum::response::Response, AppError> {
|
|
||||||
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<Arc<QrConfig>>,
|
|
||||||
) -> Result<axum::response::Response, AppError> {
|
|
||||||
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<Arc<dyn QrAuthService>>,
|
|
||||||
Query(params): Query<GoogleCallbackQuery>,
|
|
||||||
) -> Result<axum::response::Response, AppError> {
|
|
||||||
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<Arc<dyn QrAuthService>>,
|
|
||||||
Json(body): Json<RefreshRequest>,
|
|
||||||
) -> Result<axum::response::Response, AppError> {
|
|
||||||
let tokens = service.refresh_token(body.refresh_token).await?;
|
|
||||||
Ok(ApiSuccess(TokensResponse {
|
|
||||||
access_token: tokens.access_token,
|
|
||||||
refresh_token: tokens.refresh_token,
|
|
||||||
}).into_response())
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod dto;
|
|
||||||
pub mod handlers;
|
|
||||||
pub mod routes;
|
|
||||||
@@ -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<PgPool>, jwt: Arc<QrJwtService>, config: Arc<QrConfig>) -> Router {
|
|
||||||
let service: Arc<dyn QrAuthService> = 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))
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod http;
|
|
||||||
pub mod persistence;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod domain;
|
|
||||||
pub mod application;
|
|
||||||
pub mod infrastructure;
|
|
||||||
@@ -18,11 +18,10 @@ use crate::{
|
|||||||
persistence::postgres_campaign_repository::PostgresCampaignRepository,
|
persistence::postgres_campaign_repository::PostgresCampaignRepository,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
common::qr_jwt::QrJwtService,
|
|
||||||
middleware::qr_auth::qr_auth_middleware,
|
middleware::qr_auth::qr_auth_middleware,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn qr_campaigns_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>) -> Router {
|
pub fn qr_campaigns_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let repo: Arc<dyn CampaignRepository> = Arc::new(PostgresCampaignRepository::new(pool.clone()));
|
let repo: Arc<dyn CampaignRepository> = Arc::new(PostgresCampaignRepository::new(pool.clone()));
|
||||||
let service: Arc<dyn QrCampaignService> = Arc::new(QrCampaignServiceImpl::new(repo));
|
let service: Arc<dyn QrCampaignService> = Arc::new(QrCampaignServiceImpl::new(repo));
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ pub fn qr_campaigns_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>) -> Router
|
|||||||
.route("/campaigns/:id", delete(delete_campaign_handler))
|
.route("/campaigns/:id", delete(delete_campaign_handler))
|
||||||
.route("/campaigns/process-image", post(process_image_handler))
|
.route("/campaigns/process-image", post(process_image_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(qr_auth_middleware))
|
.layer(from_fn(qr_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
pub mod qr_jwt;
|
|
||||||
|
|||||||
@@ -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<String, AppError> {
|
|
||||||
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<String, AppError> {
|
|
||||||
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<QrClaims, AppError> {
|
|
||||||
decode::<QrClaims>(token, &self.decoding_key, &Validation::default())
|
|
||||||
.map(|d| d.claims)
|
|
||||||
.map_err(|_| AppError::AuthenticationError("Invalid or expired token".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-15
@@ -1,26 +1,14 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
pub mod auth;
|
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod campaigns;
|
pub mod campaigns;
|
||||||
|
|
||||||
pub use config::QrConfig;
|
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use common::qr_jwt::QrJwtService;
|
|
||||||
|
|
||||||
pub fn qr_router(pool: Arc<PgPool>, config: Arc<QrConfig>) -> Router {
|
|
||||||
let jwt = Arc::new(QrJwtService::new(
|
|
||||||
&config.jwt_secret,
|
|
||||||
config.jwt_expiry_minutes,
|
|
||||||
config.refresh_expiry_days,
|
|
||||||
));
|
|
||||||
|
|
||||||
|
pub fn qr_router(pool: Arc<PgPool>) -> Router {
|
||||||
Router::new()
|
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()))
|
||||||
.merge(users::infrastructure::http::routes::qr_users_routes(pool.clone(), jwt.clone()))
|
.merge(campaigns::infrastructure::http::routes::qr_campaigns_routes(pool))
|
||||||
.merge(campaigns::infrastructure::http::routes::qr_campaigns_routes(pool.clone(), jwt.clone()))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use axum::{body::Body, extract::Request, middleware::Next, response::{IntoResponse, Response}};
|
use axum::{body::Body, extract::Request, middleware::Next, response::{IntoResponse, Response}};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::common::qr_jwt::QrJwtService;
|
use imphnen_libs::decode_access_token;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct QrAuthUser {
|
pub struct QrAuthUser {
|
||||||
@@ -12,7 +13,7 @@ pub struct QrAuthUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn qr_auth_middleware(
|
pub async fn qr_auth_middleware(
|
||||||
axum::Extension(jwt_service): axum::Extension<Arc<QrJwtService>>,
|
axum::Extension(pool): axum::Extension<Arc<PgPool>>,
|
||||||
mut request: Request<Body>,
|
mut request: Request<Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, Response> {
|
) -> Result<Response, Response> {
|
||||||
@@ -26,14 +27,29 @@ pub async fn qr_auth_middleware(
|
|||||||
(StatusCode::UNAUTHORIZED, "Invalid Authorization header format").into_response()
|
(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()
|
(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()
|
(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)
|
Ok(next.run(request).await)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use sqlx::PgPool;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::qr_jwt::QrJwtService,
|
|
||||||
middleware::qr_auth::qr_auth_middleware,
|
middleware::qr_auth::qr_auth_middleware,
|
||||||
users::{
|
users::{
|
||||||
application::user_service::QrUserServiceImpl,
|
application::user_service::QrUserServiceImpl,
|
||||||
@@ -22,7 +21,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn qr_users_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>) -> Router {
|
pub fn qr_users_routes(pool: Arc<PgPool>) -> Router {
|
||||||
let repo: Arc<dyn UserRepository> = Arc::new(PostgresUserRepository::new(pool.clone()));
|
let repo: Arc<dyn UserRepository> = Arc::new(PostgresUserRepository::new(pool.clone()));
|
||||||
let service: Arc<dyn QrUserService> = Arc::new(QrUserServiceImpl::new(repo));
|
let service: Arc<dyn QrUserService> = Arc::new(QrUserServiceImpl::new(repo));
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ pub fn qr_users_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>) -> Router {
|
|||||||
.route("/users/:id/role", put(update_role_handler))
|
.route("/users/:id/role", put(update_role_handler))
|
||||||
.route("/users/:id", delete(delete_user_handler))
|
.route("/users/:id", delete(delete_user_handler))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension(jwt.clone()))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(from_fn(qr_auth_middleware))
|
.layer(from_fn(qr_auth_middleware))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user