Files
imphnen-backend-service/imphnen-qr/src/auth/application/auth_service.rs
T
maulanasdqnandClaude Sonnet 4.6 4bba182ea3 feat: migrate imphnen-backend-qr into workspace as imphnen-qr crate
Ports the Go QR campaign overlay service to a self-contained Rust crate
nested at /v1/qr/... in the gateway.

Features:
- Auth: register, login, Google OAuth, JWT refresh (bcrypt compat with Go DB)
- Users: profile management + admin CRUD (list/role/delete)
- Campaigns: create (auto-generates QR PNG via qrcode crate), list,
  activate, delete; process-image endpoint overlays active campaign QR
  onto uploaded images (bottom-right corner, image crate)
- QR pool connects to imphnen_qr database via QR_DATABASE_URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 16:03:39 +07:00

155 lines
6.7 KiB
Rust

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