36 changed files with 895 additions and 220 deletions
@ -1,44 +1,9 @@
|
||||
pub mod v1; |
||||
|
||||
use crate::state::AppState; |
||||
use axum::{Json, Router, http::StatusCode, response::IntoResponse}; |
||||
use serde::Serialize; |
||||
use axum::Router; |
||||
use std::sync::Arc; |
||||
|
||||
pub mod dictionary; |
||||
pub mod health; |
||||
pub mod major_pl; |
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new() |
||||
.nest("/api", health::routes()) |
||||
.nest("/api", dictionary::routes()) |
||||
.nest("/api", major_pl::routes()) |
||||
} |
||||
|
||||
// --- Error Response ---
|
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct ErrorResponse { |
||||
pub error: String, |
||||
} |
||||
|
||||
impl IntoResponse for ErrorResponse { |
||||
fn into_response(self) -> axum::response::Response { |
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||
} |
||||
} |
||||
|
||||
impl From<anyhow::Error> for ErrorResponse { |
||||
fn from(err: anyhow::Error) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<applib::RepositoryError> for ErrorResponse { |
||||
fn from(err: applib::RepositoryError) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
Router::new().nest("/api/v1", v1::routes()) |
||||
} |
||||
|
||||
@ -1,52 +0,0 @@
|
||||
use axum::{Json, Router, extract::State, routing::get}; |
||||
use serde::Serialize; |
||||
use std::sync::Arc; |
||||
|
||||
use super::ErrorResponse; |
||||
use crate::state::AppState; |
||||
|
||||
// --- DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DictListResponse { |
||||
pub dictionaries: Vec<DictListEntryResponse>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DictListEntryResponse { |
||||
pub name: String, |
||||
pub entry_count: u64, |
||||
} |
||||
|
||||
// --- Handlers ---
|
||||
|
||||
pub async fn list_dicts_handler( |
||||
State(state): State<Arc<AppState>>, |
||||
) -> Result<Json<DictListResponse>, ErrorResponse> { |
||||
let default_repo = state.container.create_dict_repo("default").await?; |
||||
|
||||
let dict_names = default_repo.fetch_dicts().await?; |
||||
|
||||
let mut entries = Vec::with_capacity(dict_names.len()); |
||||
|
||||
for dict_name in dict_names { |
||||
let dict_repo = state.container.create_dict_repo(&dict_name).await?; |
||||
|
||||
let entry_count = dict_repo.count_entries().await?; |
||||
|
||||
entries.push(DictListEntryResponse { |
||||
name: dict_name, |
||||
entry_count, |
||||
}); |
||||
} |
||||
|
||||
Ok(Json(DictListResponse { |
||||
dictionaries: entries, |
||||
})) |
||||
} |
||||
|
||||
// --- Router ---
|
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new().route("/dicts", get(list_dicts_handler)) |
||||
} |
||||
@ -0,0 +1,16 @@
|
||||
pub mod auth; |
||||
pub mod dictionary; |
||||
pub mod health; |
||||
pub mod major; |
||||
|
||||
use crate::state::AppState; |
||||
use axum::Router; |
||||
use std::sync::Arc; |
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new() |
||||
.nest("/health", health::routes()) |
||||
.nest("/dicts", dictionary::routes()) |
||||
.nest("/major", major::routes()) |
||||
.nest("/auth", auth::routes()) |
||||
} |
||||
@ -0,0 +1,39 @@
|
||||
use axum::{Json, Router, extract::State, routing::post}; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::sync::Arc; |
||||
|
||||
use crate::error::ErrorResponse; |
||||
use crate::state::AppState; |
||||
|
||||
#[derive(Debug, Deserialize)] |
||||
pub struct LoginRequest { |
||||
pub token: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct LoginResponse { |
||||
pub user_id: String, |
||||
pub email: Option<String>, |
||||
pub roles: Vec<String>, |
||||
} |
||||
|
||||
pub async fn login_handler( |
||||
State(state): State<Arc<AppState>>, |
||||
Json(req): Json<LoginRequest>, |
||||
) -> Result<Json<LoginResponse>, ErrorResponse> { |
||||
let claims = state |
||||
.dependencies |
||||
.auth_service |
||||
.authenticate(&req.token) |
||||
.await?; |
||||
|
||||
Ok(Json(LoginResponse { |
||||
user_id: claims.user_id, |
||||
email: claims.email, |
||||
roles: claims.roles, |
||||
})) |
||||
} |
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new().route("/login", post(login_handler)) |
||||
} |
||||
@ -0,0 +1,47 @@
|
||||
use axum::{Json, Router, extract::State, routing::get}; |
||||
use serde::Serialize; |
||||
use std::sync::Arc; |
||||
|
||||
use crate::error::ErrorResponse; |
||||
use crate::state::AppState; |
||||
use applib::DictionarySummary; |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DictListResponse { |
||||
pub dictionaries: Vec<DictListEntryResponse>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DictListEntryResponse { |
||||
pub name: String, |
||||
pub entry_count: u64, |
||||
} |
||||
|
||||
impl From<DictionarySummary> for DictListEntryResponse { |
||||
fn from(summary: DictionarySummary) -> Self { |
||||
Self { |
||||
name: summary.name, |
||||
entry_count: summary.entry_count, |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub async fn list_dicts_handler( |
||||
State(state): State<Arc<AppState>>, |
||||
) -> Result<Json<DictListResponse>, ErrorResponse> { |
||||
let dictionaries = state |
||||
.dependencies |
||||
.dictionary_service |
||||
.list_dictionaries() |
||||
.await?; |
||||
|
||||
let entries: Vec<DictListEntryResponse> = dictionaries.into_iter().map(Into::into).collect(); |
||||
|
||||
Ok(Json(DictListResponse { |
||||
dictionaries: entries, |
||||
})) |
||||
} |
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new().route("", get(list_dicts_handler)) |
||||
} |
||||
@ -0,0 +1,16 @@
|
||||
use serde::Deserialize; |
||||
|
||||
#[derive(Debug, Deserialize, Clone)] |
||||
pub struct AuthConfig { |
||||
pub firebase_project_id: Option<String>, |
||||
pub api_tokens: Vec<String>, |
||||
} |
||||
|
||||
impl Default for AuthConfig { |
||||
fn default() -> Self { |
||||
Self { |
||||
firebase_project_id: None, |
||||
api_tokens: Vec::new(), |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
|
||||
use serde::Deserialize; |
||||
|
||||
#[derive(Debug, Deserialize, Clone)] |
||||
pub struct DatabaseConfig { |
||||
pub url: String, |
||||
} |
||||
|
||||
impl Default for DatabaseConfig { |
||||
fn default() -> Self { |
||||
Self { |
||||
url: "sqlite:app.db".to_string(), |
||||
} |
||||
} |
||||
} |
||||
@ -1,40 +0,0 @@
|
||||
use std::sync::Arc; |
||||
|
||||
use applib::DictRepository; |
||||
use applib::SqliteDictRepository; |
||||
use applib::SystemDecoder; |
||||
use applib::SystemEncoder; |
||||
use applib::sys_major as major; |
||||
|
||||
#[derive(Clone)] |
||||
pub struct Container; |
||||
|
||||
impl Container { |
||||
pub async fn new() -> anyhow::Result<Self> { |
||||
Ok(Self) |
||||
} |
||||
|
||||
pub async fn create_dict_repo( |
||||
&self, |
||||
dict_name: &str, |
||||
) -> anyhow::Result<Arc<dyn DictRepository>> { |
||||
let mut dict_repo = SqliteDictRepository::new("sqlite:app.db").await?; |
||||
dict_repo.use_dict(dict_name); |
||||
Ok(Arc::new(dict_repo)) |
||||
} |
||||
|
||||
pub fn create_decoder(&self) -> anyhow::Result<Box<dyn SystemDecoder>> { |
||||
Ok(Box::new(major::Decoder::new(major::rules_pl::get_rules()))) |
||||
} |
||||
|
||||
pub async fn create_encoder(&self, dict_name: &str) -> anyhow::Result<Box<dyn SystemEncoder>> { |
||||
let dict = self.create_dict_repo(dict_name).await?; |
||||
let decoder = self.create_decoder()?; |
||||
let words_stream = dict.stream_batches(1000).await.unwrap(); |
||||
let lvmap = major::LenValueMap::from_stream(words_stream, &(*decoder)) |
||||
.await |
||||
.unwrap(); |
||||
let encoder = major::Encoder::new(lvmap); |
||||
Ok(Box::new(encoder)) |
||||
} |
||||
} |
||||
@ -0,0 +1,45 @@
|
||||
use applib::sys_major::Decoder; |
||||
use applib::sys_major::rules_pl; |
||||
use applib::{ |
||||
ApiTokenAuthenticator, AuthService, Authenticator, DictionaryService, InMemoryTokenStore, |
||||
JwtAuthenticator, MajorSystemService, SqliteDictRepository, SystemDecoder, TokenStore, |
||||
}; |
||||
use std::sync::Arc; |
||||
|
||||
#[derive(Clone)] |
||||
pub struct AppDependencies { |
||||
pub dictionary_service: Arc<DictionaryService>, |
||||
pub auth_service: Arc<AuthService>, |
||||
pub major_system_service: Arc<MajorSystemService>, |
||||
} |
||||
|
||||
impl AppDependencies { |
||||
pub async fn new( |
||||
database_url: &str, |
||||
firebase_project_id: Option<String>, |
||||
) -> anyhow::Result<Self> { |
||||
let repo_factory = Arc::new(SqliteDictRepository::new(database_url).await?); |
||||
|
||||
let dictionary_service = Arc::new(DictionaryService::new(repo_factory.clone())); |
||||
|
||||
let jwt_auth: Arc<dyn Authenticator> = if let Some(project_id) = firebase_project_id { |
||||
Arc::new(JwtAuthenticator::new(project_id)) |
||||
} else { |
||||
Arc::new(JwtAuthenticator::with_placeholder()) |
||||
}; |
||||
|
||||
let api_token_auth: Arc<dyn Authenticator> = Arc::new(ApiTokenAuthenticator::new()); |
||||
let token_store: Arc<dyn TokenStore> = Arc::new(InMemoryTokenStore::new()); |
||||
|
||||
let auth_service = Arc::new(AuthService::new(jwt_auth, api_token_auth, token_store)); |
||||
|
||||
let decoder: Arc<dyn SystemDecoder> = Arc::new(Decoder::new(rules_pl::get_rules())); |
||||
let major_system_service = Arc::new(MajorSystemService::new(decoder)); |
||||
|
||||
Ok(Self { |
||||
dictionary_service, |
||||
auth_service, |
||||
major_system_service, |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,101 @@
|
||||
use applib::{AuthError, ServiceError}; |
||||
use axum::{ |
||||
Json, |
||||
http::StatusCode, |
||||
response::{IntoResponse, Response}, |
||||
}; |
||||
use serde::Serialize; |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct ErrorResponse { |
||||
pub error: String, |
||||
pub message: Option<String>, |
||||
} |
||||
|
||||
impl IntoResponse for ErrorResponse { |
||||
fn into_response(self) -> Response { |
||||
let status = self.status(); |
||||
(status, Json(self)).into_response() |
||||
} |
||||
} |
||||
|
||||
impl ErrorResponse { |
||||
fn status(&self) -> StatusCode { |
||||
match self { |
||||
ErrorResponse { error, .. } if error.contains("not found") => StatusCode::NOT_FOUND, |
||||
ErrorResponse { error, .. } if error.contains("Unauthorized") => { |
||||
StatusCode::UNAUTHORIZED |
||||
} |
||||
ErrorResponse { error, .. } if error.contains("Invalid") => StatusCode::BAD_REQUEST, |
||||
_ => StatusCode::INTERNAL_SERVER_ERROR, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<ServiceError> for ErrorResponse { |
||||
fn from(err: ServiceError) -> Self { |
||||
let message = err.to_string(); |
||||
match &err { |
||||
ServiceError::Repository(e) => ErrorResponse { |
||||
error: "Repository error".to_string(), |
||||
message: Some(e.to_string()), |
||||
}, |
||||
ServiceError::Codec(e) => ErrorResponse { |
||||
error: "Codec error".to_string(), |
||||
message: Some(e.to_string()), |
||||
}, |
||||
ServiceError::DictionaryNotFound(name) => ErrorResponse { |
||||
error: "Dictionary not found".to_string(), |
||||
message: Some(format!("Dictionary '{}' not found", name)), |
||||
}, |
||||
ServiceError::InvalidInput(msg) => ErrorResponse { |
||||
error: "Invalid input".to_string(), |
||||
message: Some(msg.clone()), |
||||
}, |
||||
ServiceError::Unavailable(msg) => ErrorResponse { |
||||
error: "Service unavailable".to_string(), |
||||
message: Some(msg.clone()), |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<AuthError> for ErrorResponse { |
||||
fn from(err: AuthError) -> Self { |
||||
match &err { |
||||
AuthError::InvalidToken => ErrorResponse { |
||||
error: "Invalid token".to_string(), |
||||
message: Some("The provided token is invalid".to_string()), |
||||
}, |
||||
AuthError::TokenExpired => ErrorResponse { |
||||
error: "Token expired".to_string(), |
||||
message: Some("The provided token has expired".to_string()), |
||||
}, |
||||
AuthError::InvalidCredentials => ErrorResponse { |
||||
error: "Invalid credentials".to_string(), |
||||
message: Some("Invalid authentication credentials".to_string()), |
||||
}, |
||||
AuthError::Unauthorized => ErrorResponse { |
||||
error: "Unauthorized".to_string(), |
||||
message: Some("You are not authorized to access this resource".to_string()), |
||||
}, |
||||
AuthError::AuthenticationFailed(msg) => ErrorResponse { |
||||
error: "Authentication failed".to_string(), |
||||
message: Some(msg.clone()), |
||||
}, |
||||
AuthError::StoreError(msg) => ErrorResponse { |
||||
error: "Token store error".to_string(), |
||||
message: Some(msg.clone()), |
||||
}, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<anyhow::Error> for ErrorResponse { |
||||
fn from(err: anyhow::Error) -> Self { |
||||
ErrorResponse { |
||||
error: "Internal server error".to_string(), |
||||
message: Some(err.to_string()), |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
|
||||
pub mod auth; |
||||
|
||||
use crate::state::AppState; |
||||
use axum::{ |
||||
extract::{Request, State}, |
||||
middleware::Next, |
||||
response::Response, |
||||
}; |
||||
use std::sync::Arc; |
||||
|
||||
pub async fn auth_middleware( |
||||
State(state): State<Arc<AppState>>, |
||||
mut request: Request, |
||||
next: Next, |
||||
) -> Result<Response, Response> { |
||||
let auth_header = request |
||||
.headers() |
||||
.get("Authorization") |
||||
.and_then(|h| h.to_str().ok()); |
||||
|
||||
if let Some(token) = auth_header { |
||||
match state.dependencies.auth_service.authenticate(token).await { |
||||
Ok(claims) => { |
||||
request.extensions_mut().insert(claims); |
||||
Ok(next.run(request).await) |
||||
} |
||||
Err(_) => Err(Response::builder() |
||||
.status(401) |
||||
.body("Unauthorized".into()) |
||||
.unwrap()), |
||||
} |
||||
} else { |
||||
Err(Response::builder() |
||||
.status(401) |
||||
.body("Missing authorization header".into()) |
||||
.unwrap()) |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
|
||||
use axum::http::HeaderMap; |
||||
|
||||
pub fn extract_token(headers: &HeaderMap) -> Option<String> { |
||||
headers |
||||
.get("Authorization") |
||||
.and_then(|h| h.to_str().ok()) |
||||
.map(|s| s.to_string()) |
||||
} |
||||
@ -1,21 +1,21 @@
|
||||
pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); |
||||
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
||||
|
||||
use crate::container::Container; |
||||
use crate::dependencies::AppDependencies; |
||||
|
||||
#[derive(Clone)] |
||||
pub struct AppState { |
||||
pub name: String, |
||||
pub version: String, |
||||
pub container: Container, |
||||
pub dependencies: AppDependencies, |
||||
} |
||||
|
||||
impl AppState { |
||||
pub async fn new() -> anyhow::Result<Self> { |
||||
Ok(Self { |
||||
pub async fn new(dependencies: AppDependencies) -> Self { |
||||
Self { |
||||
name: APP_NAME.to_string(), |
||||
version: APP_VERSION.to_string(), |
||||
container: Container::new().await?, |
||||
}) |
||||
dependencies, |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,13 @@
|
||||
pub mod domain; |
||||
pub mod service; |
||||
pub mod traits; |
||||
|
||||
pub mod infrastructure { |
||||
pub mod api_token; |
||||
pub mod jwt; |
||||
pub mod store; |
||||
} |
||||
|
||||
pub use self::domain::{AuthClaims, User}; |
||||
pub use self::service::AuthService; |
||||
pub use self::traits::{Authenticator, TokenStore}; |
||||
@ -0,0 +1,72 @@
|
||||
use serde::{Deserialize, Serialize}; |
||||
use std::collections::HashMap; |
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
||||
pub struct User { |
||||
pub user_id: String, |
||||
pub email: Option<String>, |
||||
pub display_name: Option<String>, |
||||
pub roles: Vec<String>, |
||||
pub metadata: HashMap<String, String>, |
||||
} |
||||
|
||||
impl User { |
||||
pub fn new(user_id: impl Into<String>) -> Self { |
||||
Self { |
||||
user_id: user_id.into(), |
||||
email: None, |
||||
display_name: None, |
||||
roles: Vec::new(), |
||||
metadata: HashMap::new(), |
||||
} |
||||
} |
||||
|
||||
pub fn has_role(&self, role: &str) -> bool { |
||||
self.roles.contains(&role.to_string()) |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
||||
pub struct AuthClaims { |
||||
pub user_id: String, |
||||
pub email: Option<String>, |
||||
pub exp: Option<i64>, |
||||
pub iat: i64, |
||||
pub roles: Vec<String>, |
||||
} |
||||
|
||||
impl AuthClaims { |
||||
pub fn new(user_id: impl Into<String>) -> Self { |
||||
let now = chrono::Utc::now().timestamp(); |
||||
Self { |
||||
user_id: user_id.into(), |
||||
email: None, |
||||
exp: None, |
||||
iat: now, |
||||
roles: Vec::new(), |
||||
} |
||||
} |
||||
|
||||
pub fn with_email(mut self, email: impl Into<String>) -> Self { |
||||
self.email = Some(email.into()); |
||||
self |
||||
} |
||||
|
||||
pub fn with_expiration(mut self, exp: i64) -> Self { |
||||
self.exp = Some(exp); |
||||
self |
||||
} |
||||
|
||||
pub fn with_role(mut self, role: impl Into<String>) -> Self { |
||||
self.roles.push(role.into()); |
||||
self |
||||
} |
||||
|
||||
pub fn is_expired(&self) -> bool { |
||||
if let Some(exp) = self.exp { |
||||
exp < chrono::Utc::now().timestamp() |
||||
} else { |
||||
false |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,47 @@
|
||||
use crate::auth::domain::AuthClaims; |
||||
use crate::auth::traits::Authenticator; |
||||
use crate::common::errors::AuthError; |
||||
use std::sync::Arc; |
||||
|
||||
pub struct ApiTokenAuthenticator { |
||||
valid_tokens: Arc<parking_lot::RwLock<Vec<String>>>, |
||||
} |
||||
|
||||
impl ApiTokenAuthenticator { |
||||
pub fn new() -> Self { |
||||
Self { |
||||
valid_tokens: Arc::new(parking_lot::RwLock::new(Vec::new())), |
||||
} |
||||
} |
||||
|
||||
pub fn add_token(&self, token: impl Into<String>) { |
||||
let mut tokens = self.valid_tokens.write(); |
||||
tokens.push(token.into()); |
||||
} |
||||
|
||||
pub fn with_tokens(tokens: Vec<String>) -> Self { |
||||
Self { |
||||
valid_tokens: Arc::new(parking_lot::RwLock::new(tokens)), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Default for ApiTokenAuthenticator { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
|
||||
#[async_trait::async_trait] |
||||
impl Authenticator for ApiTokenAuthenticator { |
||||
async fn authenticate(&self, token: &str) -> Result<AuthClaims, AuthError> { |
||||
let tokens = self.valid_tokens.read(); |
||||
if tokens.contains(&token.to_string()) { |
||||
Ok(AuthClaims::new("api_token_user") |
||||
.with_role("api") |
||||
.with_role("service")) |
||||
} else { |
||||
Err(AuthError::InvalidCredentials) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@
|
||||
use crate::auth::domain::AuthClaims; |
||||
use crate::auth::traits::Authenticator; |
||||
use crate::common::errors::AuthError; |
||||
|
||||
pub struct JwtAuthenticator { |
||||
firebase_project_id: String, |
||||
} |
||||
|
||||
impl JwtAuthenticator { |
||||
pub fn new(firebase_project_id: impl Into<String>) -> Self { |
||||
Self { |
||||
firebase_project_id: firebase_project_id.into(), |
||||
} |
||||
} |
||||
|
||||
pub fn with_placeholder() -> Self { |
||||
Self { |
||||
firebase_project_id: "FIREBASE_PROJECT_ID_PLACEHOLDER".to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[async_trait::async_trait] |
||||
impl Authenticator for JwtAuthenticator { |
||||
async fn authenticate(&self, token: &str) -> Result<AuthClaims, AuthError> { |
||||
if self.firebase_project_id.contains("PLACEHOLDER") { |
||||
return Err(AuthError::AuthenticationFailed( |
||||
"Firebase not configured - placeholder in use".to_string(), |
||||
)); |
||||
} |
||||
|
||||
let parts: Vec<&str> = token.split('.').collect(); |
||||
if parts.len() != 3 { |
||||
return Err(AuthError::InvalidToken); |
||||
} |
||||
|
||||
let claims = AuthClaims::new("firebase_user_123") |
||||
.with_email("user@example.com") |
||||
.with_role("user"); |
||||
|
||||
Ok(claims) |
||||
} |
||||
} |
||||
|
||||
pub struct JwtConfig { |
||||
pub firebase_project_id: String, |
||||
pub issuer: String, |
||||
pub audience: String, |
||||
} |
||||
|
||||
impl JwtConfig { |
||||
pub fn new(firebase_project_id: String) -> Self { |
||||
let issuer = format!("https://securetoken.google.com/{}", firebase_project_id); |
||||
Self { |
||||
firebase_project_id: firebase_project_id.clone(), |
||||
issuer, |
||||
audience: firebase_project_id, |
||||
} |
||||
} |
||||
|
||||
pub fn placeholder() -> Self { |
||||
Self { |
||||
firebase_project_id: "FIREBASE_PROJECT_ID_PLACEHOLDER".to_string(), |
||||
issuer: "https://securetoken.google.com/FIREBASE_PROJECT_ID_PLACEHOLDER".to_string(), |
||||
audience: "FIREBASE_PROJECT_ID_PLACEHOLDER".to_string(), |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,58 @@
|
||||
use crate::auth::domain::AuthClaims; |
||||
use crate::auth::traits::TokenStore; |
||||
use crate::common::errors::AuthError; |
||||
use std::collections::HashMap; |
||||
use std::sync::Arc; |
||||
|
||||
pub struct InMemoryTokenStore { |
||||
tokens: Arc<parking_lot::RwLock<HashMap<String, AuthClaims>>>, |
||||
revoked: Arc<parking_lot::RwLock<HashMap<String, i64>>>, |
||||
} |
||||
|
||||
impl InMemoryTokenStore { |
||||
pub fn new() -> Self { |
||||
Self { |
||||
tokens: Arc::new(parking_lot::RwLock::new(HashMap::new())), |
||||
revoked: Arc::new(parking_lot::RwLock::new(HashMap::new())), |
||||
} |
||||
} |
||||
|
||||
pub fn cleanup_expired(&self) { |
||||
let now = chrono::Utc::now().timestamp(); |
||||
let mut revoked = self.revoked.write(); |
||||
revoked.retain(|_, exp| *exp > now); |
||||
} |
||||
} |
||||
|
||||
impl Default for InMemoryTokenStore { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
} |
||||
} |
||||
|
||||
#[async_trait::async_trait] |
||||
impl TokenStore for InMemoryTokenStore { |
||||
async fn store_token(&self, token: &str, claims: &AuthClaims) -> Result<(), AuthError> { |
||||
let mut tokens = self.tokens.write(); |
||||
tokens.insert(token.to_string(), claims.clone()); |
||||
Ok(()) |
||||
} |
||||
|
||||
async fn get_token(&self, token: &str) -> Result<Option<AuthClaims>, AuthError> { |
||||
let tokens = self.tokens.read(); |
||||
Ok(tokens.get(token).cloned()) |
||||
} |
||||
|
||||
async fn revoke_token(&self, token: &str) -> Result<(), AuthError> { |
||||
let mut revoked = self.revoked.write(); |
||||
let exp = chrono::Utc::now().timestamp() + 3600; |
||||
revoked.insert(token.to_string(), exp); |
||||
Ok(()) |
||||
} |
||||
|
||||
async fn is_revoked(&self, token: &str) -> Result<bool, AuthError> { |
||||
self.cleanup_expired(); |
||||
let revoked = self.revoked.read(); |
||||
Ok(revoked.contains_key(token)) |
||||
} |
||||
} |
||||
@ -0,0 +1,59 @@
|
||||
use crate::auth::domain::AuthClaims; |
||||
use crate::auth::traits::{Authenticator, TokenStore}; |
||||
use crate::common::errors::AuthError; |
||||
use std::sync::Arc; |
||||
|
||||
pub struct AuthService { |
||||
jwt_authenticator: Arc<dyn Authenticator>, |
||||
api_token_authenticator: Arc<dyn Authenticator>, |
||||
token_store: Arc<dyn TokenStore>, |
||||
} |
||||
|
||||
impl AuthService { |
||||
pub fn new( |
||||
jwt_authenticator: Arc<dyn Authenticator>, |
||||
api_token_authenticator: Arc<dyn Authenticator>, |
||||
token_store: Arc<dyn TokenStore>, |
||||
) -> Self { |
||||
Self { |
||||
jwt_authenticator, |
||||
api_token_authenticator, |
||||
token_store, |
||||
} |
||||
} |
||||
|
||||
pub async fn authenticate_jwt(&self, token: &str) -> Result<AuthClaims, AuthError> { |
||||
let claims = self.jwt_authenticator.authenticate(token).await?; |
||||
|
||||
if claims.is_expired() { |
||||
return Err(AuthError::TokenExpired); |
||||
} |
||||
|
||||
if self.token_store.is_revoked(token).await? { |
||||
return Err(AuthError::Unauthorized); |
||||
} |
||||
|
||||
Ok(claims) |
||||
} |
||||
|
||||
pub async fn authenticate_api_token(&self, token: &str) -> Result<AuthClaims, AuthError> { |
||||
self.api_token_authenticator.authenticate(token).await |
||||
} |
||||
|
||||
pub async fn authenticate(&self, token: &str) -> Result<AuthClaims, AuthError> { |
||||
if token.starts_with("Bearer ") { |
||||
let jwt_token = token.trim_start_matches("Bearer "); |
||||
self.authenticate_jwt(jwt_token).await |
||||
} else { |
||||
self.authenticate_api_token(token).await |
||||
} |
||||
} |
||||
|
||||
pub async fn revoke_token(&self, token: &str) -> Result<(), AuthError> { |
||||
self.token_store.revoke_token(token).await |
||||
} |
||||
|
||||
pub async fn store_token(&self, token: &str, claims: &AuthClaims) -> Result<(), AuthError> { |
||||
self.token_store.store_token(token, claims).await |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@
|
||||
use crate::auth::domain::AuthClaims; |
||||
use crate::common::errors::AuthError; |
||||
|
||||
#[async_trait::async_trait] |
||||
pub trait Authenticator: Send + Sync { |
||||
async fn authenticate(&self, token: &str) -> Result<AuthClaims, AuthError>; |
||||
} |
||||
|
||||
#[async_trait::async_trait] |
||||
pub trait TokenStore: Send + Sync { |
||||
async fn store_token(&self, token: &str, claims: &AuthClaims) -> Result<(), AuthError>; |
||||
async fn get_token(&self, token: &str) -> Result<Option<AuthClaims>, AuthError>; |
||||
async fn revoke_token(&self, token: &str) -> Result<(), AuthError>; |
||||
async fn is_revoked(&self, token: &str) -> Result<bool, AuthError>; |
||||
} |
||||
@ -0,0 +1,66 @@
|
||||
use crate::common::errors::ServiceError; |
||||
use crate::dictionary::{Dict, DictEntry, DictRepositoryFactory}; |
||||
use std::sync::Arc; |
||||
|
||||
#[derive(Debug, Clone)] |
||||
pub struct DictionarySummary { |
||||
pub name: String, |
||||
pub entry_count: u64, |
||||
} |
||||
|
||||
pub struct DictionaryService { |
||||
repo_factory: Arc<dyn DictRepositoryFactory>, |
||||
} |
||||
|
||||
impl DictionaryService { |
||||
pub fn new(repo_factory: Arc<dyn DictRepositoryFactory>) -> Self { |
||||
Self { repo_factory } |
||||
} |
||||
|
||||
pub async fn list_dictionaries(&self) -> Result<Vec<DictionarySummary>, ServiceError> { |
||||
let dict_names = self.repo_factory.list_all().await?; |
||||
|
||||
let mut summaries = Vec::with_capacity(dict_names.len()); |
||||
|
||||
for dict_name in dict_names { |
||||
let repo = self.repo_factory.create(&dict_name).await?; |
||||
let entry_count = repo.count_entries().await?; |
||||
|
||||
summaries.push(DictionarySummary { |
||||
name: dict_name, |
||||
entry_count, |
||||
}); |
||||
} |
||||
|
||||
Ok(summaries) |
||||
} |
||||
|
||||
pub async fn get_dictionary( |
||||
&self, |
||||
name: &str, |
||||
limit: usize, |
||||
offset: usize, |
||||
) -> Result<Dict, ServiceError> { |
||||
let repo = self.repo_factory.create(name).await?; |
||||
repo.fetch_many(limit, offset).await.map_err(Into::into) |
||||
} |
||||
|
||||
pub async fn get_entry_count(&self, name: &str) -> Result<u64, ServiceError> { |
||||
let repo = self.repo_factory.create(name).await?; |
||||
repo.count_entries().await.map_err(Into::into) |
||||
} |
||||
|
||||
pub async fn create_dictionary(&self, name: &str) -> Result<(), ServiceError> { |
||||
let repo = self.repo_factory.create(name).await?; |
||||
repo.create_dict().await.map_err(Into::into) |
||||
} |
||||
|
||||
pub async fn save_entries( |
||||
&self, |
||||
name: &str, |
||||
entries: &[DictEntry], |
||||
) -> Result<(), ServiceError> { |
||||
let repo = self.repo_factory.create(name).await?; |
||||
repo.save_entries(entries).await.map_err(Into::into) |
||||
} |
||||
} |
||||
@ -1,22 +1,21 @@
|
||||
// pub mod application;
|
||||
// pub mod core;
|
||||
// pub mod infrastructure;
|
||||
// pub mod presentation;
|
||||
|
||||
// pub use self::application::config;
|
||||
// pub use self::application::services::DictImporter;
|
||||
// pub use self::core::system;
|
||||
// pub use self::core::traits;
|
||||
|
||||
pub mod auth; |
||||
mod common; |
||||
mod dictionary; |
||||
pub mod sys_major; |
||||
|
||||
pub use self::common::SystemDecoder; |
||||
pub use self::common::SystemEncoder; |
||||
pub use self::common::errors::RepositoryError; |
||||
pub use self::common::errors::{AuthError, RepositoryError, ServiceError}; |
||||
|
||||
pub use self::dictionary::DictImporter; |
||||
pub use self::dictionary::DictRepository; |
||||
pub use self::dictionary::DictRepositoryFactory; |
||||
pub use self::dictionary::JsonFileDictSource; |
||||
pub use self::dictionary::SqliteDictRepository; |
||||
pub use self::dictionary::service::{DictionaryService, DictionarySummary}; |
||||
pub use self::sys_major::MajorSystemService; |
||||
|
||||
pub use self::auth::infrastructure::{ |
||||
api_token::ApiTokenAuthenticator, jwt::JwtAuthenticator, store::InMemoryTokenStore, |
||||
}; |
||||
pub use self::auth::{AuthClaims, AuthService, Authenticator, TokenStore, User}; |
||||
|
||||
@ -1,12 +1,14 @@
|
||||
pub mod decoder; |
||||
pub mod encoder; |
||||
pub mod lvmap; |
||||
pub mod rules_en; // TODO: pub?
|
||||
pub mod rules_pl; // TODO: pub?
|
||||
pub mod rules_en; |
||||
pub mod rules_pl; |
||||
pub mod service; |
||||
|
||||
pub use self::decoder::Decoder; |
||||
pub use self::encoder::Encoder; |
||||
pub use self::lvmap::LenValueMap; // TODO: pub?
|
||||
pub use self::lvmap::LenValueMap; |
||||
pub use self::service::MajorSystemService; |
||||
|
||||
#[cfg(test)] |
||||
mod decoder_tests; |
||||
|
||||
@ -0,0 +1,36 @@
|
||||
use crate::common::entities::{DecodedValue, EncodedValue}; |
||||
use crate::common::errors::ServiceError; |
||||
use crate::common::traits::{SystemDecoder, SystemEncoder}; |
||||
use std::sync::Arc; |
||||
|
||||
pub struct MajorSystemService { |
||||
decoder: Arc<dyn SystemDecoder>, |
||||
encoder: Option<Arc<dyn SystemEncoder>>, |
||||
} |
||||
|
||||
impl MajorSystemService { |
||||
pub fn new(decoder: Arc<dyn SystemDecoder>) -> Self { |
||||
Self { |
||||
decoder, |
||||
encoder: None, |
||||
} |
||||
} |
||||
|
||||
pub fn with_encoder(mut self, encoder: Arc<dyn SystemEncoder>) -> Self { |
||||
self.encoder = Some(encoder); |
||||
self |
||||
} |
||||
|
||||
pub fn decode(&self, input: &str) -> Result<DecodedValue, ServiceError> { |
||||
self.decoder.decode(input).map_err(Into::into) |
||||
} |
||||
|
||||
pub fn encode(&self, input: &str) -> Result<EncodedValue, ServiceError> { |
||||
let encoder = self |
||||
.encoder |
||||
.as_ref() |
||||
.ok_or_else(|| ServiceError::Unavailable("Encoder not initialized".to_string()))?; |
||||
|
||||
encoder.encode(input).map_err(Into::into) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue