Browse Source

WIP: basic API

develop-api-fb
chodak166 4 months ago
parent
commit
b0e64ccc98
  1. 43
      apps/app_api/src/api.rs
  2. 52
      apps/app_api/src/api/dictionary.rs
  3. 16
      apps/app_api/src/api/v1.rs
  4. 39
      apps/app_api/src/api/v1/auth.rs
  5. 47
      apps/app_api/src/api/v1/dictionary.rs
  6. 14
      apps/app_api/src/api/v1/health.rs
  7. 23
      apps/app_api/src/api/v1/major.rs
  8. 7
      apps/app_api/src/app.rs
  9. 7
      apps/app_api/src/commands.rs
  10. 3
      apps/app_api/src/commands/listen.rs
  11. 15
      apps/app_api/src/config.rs
  12. 16
      apps/app_api/src/config/auth.rs
  13. 14
      apps/app_api/src/config/database.rs
  14. 40
      apps/app_api/src/container.rs
  15. 45
      apps/app_api/src/dependencies.rs
  16. 101
      apps/app_api/src/error.rs
  17. 4
      apps/app_api/src/main.rs
  18. 38
      apps/app_api/src/middleware.rs
  19. 8
      apps/app_api/src/middleware/auth.rs
  20. 4
      apps/app_api/src/router.rs
  21. 12
      apps/app_api/src/state.rs
  22. 13
      lib/src/auth.rs
  23. 72
      lib/src/auth/domain.rs
  24. 47
      lib/src/auth/infrastructure/api_token.rs
  25. 68
      lib/src/auth/infrastructure/jwt.rs
  26. 58
      lib/src/auth/infrastructure/store.rs
  27. 59
      lib/src/auth/service.rs
  28. 15
      lib/src/auth/traits.rs
  29. 1
      lib/src/common.rs
  30. 54
      lib/src/common/errors.rs
  31. 14
      lib/src/dictionary.rs
  32. 35
      lib/src/dictionary/infrastructure/sqlite_dict_repository.rs
  33. 66
      lib/src/dictionary/service.rs
  34. 21
      lib/src/lib.rs
  35. 8
      lib/src/sys_major.rs
  36. 36
      lib/src/sys_major/service.rs

43
apps/app_api/src/api.rs

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

52
apps/app_api/src/api/dictionary.rs

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

16
apps/app_api/src/api/v1.rs

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

39
apps/app_api/src/api/v1/auth.rs

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

47
apps/app_api/src/api/v1/dictionary.rs

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

14
apps/app_api/src/api/health.rs → apps/app_api/src/api/v1/health.rs

@ -8,11 +8,8 @@ use serde::Serialize;
use serde_json::Value;
use std::sync::Arc;
use super::ErrorResponse;
use crate::state::AppState;
// --- DTOs ---
#[derive(Debug, Serialize)]
pub struct EchoResponse {
pub data: Value,
@ -25,17 +22,14 @@ pub struct VersionResponse {
pub version: String,
}
// --- Handlers ---
pub async fn echo_handler(
State(_state): State<Arc<AppState>>,
Json(payload): Json<Value>,
) -> Result<Json<EchoResponse>, ErrorResponse> {
let response = EchoResponse {
) -> Json<EchoResponse> {
Json(EchoResponse {
data: payload,
timestamp: Utc::now().to_rfc3339(),
};
Ok(Json(response))
})
}
pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<VersionResponse> {
@ -45,8 +39,6 @@ pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<Version
})
}
// --- Router ---
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
.route("/echo", post(echo_handler))

23
apps/app_api/src/api/major_pl.rs → apps/app_api/src/api/v1/major.rs

@ -1,4 +1,3 @@
use super::ErrorResponse;
use axum::{
Json, Router,
extract::{Path, Query, State},
@ -7,6 +6,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::error::ErrorResponse;
use crate::state::AppState;
#[derive(Debug, Deserialize)]
@ -42,14 +42,7 @@ pub async fn encode_handler(
Query(params): Query<EncodeQuery>,
) -> Result<Json<EncodeResponse>, ErrorResponse> {
let dict_name = params.dict.unwrap_or_else(|| "demo_pl".to_string());
let encoder = state
.container
.create_encoder(&dict_name)
.await
.map_err(|e| anyhow::anyhow!("Failed to create encoder: {}", e))?;
let result = encoder
.encode(&input)
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
let result = state.dependencies.major_system_service.encode(&input)?;
let encoded_parts: Vec<Vec<EncodePart>> = result
.iter()
@ -76,13 +69,7 @@ pub async fn decode_handler(
Path(input): Path<String>,
Query(_params): Query<DecodeQuery>,
) -> Result<Json<DecodeResponse>, ErrorResponse> {
let decoder = state
.container
.create_decoder()
.map_err(|e| anyhow::anyhow!("Failed to create decoder: {}", e))?;
let result = decoder
.decode(&input)
.map_err(|e| anyhow::anyhow!("Failed to decode: {}", e))?;
let result = state.dependencies.major_system_service.decode(&input)?;
Ok(Json(DecodeResponse {
input,
@ -92,6 +79,6 @@ pub async fn decode_handler(
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
.route("/encode/major_pl/{input}", get(encode_handler))
.route("/decode/major_pl/{input}", get(decode_handler))
.route("/encode/pl/{input}", get(encode_handler))
.route("/decode/pl/{input}", get(decode_handler))
}

7
apps/app_api/src/app.rs

@ -1,13 +1,11 @@
use crate::commands::{AppCommand, CliArgs};
use crate::config::AppConfig;
use crate::container::Container;
use anyhow::Result;
use clap::Parser;
use tracing::debug;
pub struct Application {
config: AppConfig,
container: Container,
command: Box<dyn AppCommand>,
}
@ -27,16 +25,13 @@ impl Application {
debug!("Bootstrapping application...");
let container = Container::new().await?;
Ok(Self {
config,
container,
command: app_cmd,
})
}
pub async fn run(self) -> Result<()> {
self.command.execute(&self.config, &self.container).await
self.command.execute(&self.config).await
}
}

7
apps/app_api/src/commands.rs

@ -1,7 +1,6 @@
pub mod listen;
use crate::config::AppConfig;
use crate::container::Container;
use anyhow::Result;
use async_trait::async_trait;
use clap::Subcommand;
@ -12,7 +11,6 @@ use std::path::PathBuf;
#[derive(Subcommand, Debug, Clone)]
pub enum Command {
/// Decode a word using given system
Listen(listen::ListenCmd),
}
@ -36,7 +34,6 @@ pub struct CliArgs {
#[derive(ClapArgs, Debug)]
pub struct GlobalArgs {
/// Path to config file
#[arg(short, long, default_value = "config.toml")]
pub config: PathBuf,
@ -57,11 +54,9 @@ pub trait Configurable {
#[async_trait]
pub trait Executable {
async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()>;
async fn execute(&self, config: &AppConfig) -> Result<()>;
}
// AppCommand must be dyn-compatible. Configurable is already dyn-compatible.
// Executable is dyn-compatible because of #[async_trait].
pub trait AppCommand: Configurable + Executable {}
impl<T: Configurable + Executable> AppCommand for T {}

3
apps/app_api/src/commands/listen.rs

@ -1,6 +1,5 @@
use crate::commands::{ClapArgs, Configurable, Executable};
use crate::config::AppConfig;
use crate::container::Container;
use crate::router;
use anyhow::Result;
@ -55,7 +54,7 @@ impl Configurable for ListenCmd {
#[async_trait]
impl Executable for ListenCmd {
async fn execute(&self, config: &AppConfig, _container: &Container) -> Result<()> {
async fn execute(&self, config: &AppConfig) -> Result<()> {
let listen_config = config
.listen
.as_ref()

15
apps/app_api/src/config.rs

@ -1,37 +1,42 @@
pub mod auth;
pub mod database;
use crate::commands::*;
use anyhow::{Context, Result};
use config::{Config, Environment, File};
use serde::Deserialize;
pub use self::auth::AuthConfig;
pub use self::database::DatabaseConfig;
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
#[serde(default)]
pub listen: Option<listen::Config>,
pub log_level: String,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub database: DatabaseConfig,
}
impl AppConfig {
pub fn build(args: &GlobalArgs, handler: &dyn Configurable) -> Result<Self> {
let mut builder = Config::builder();
// Command-specific defaults via Trait
builder = handler.apply_defaults(builder)?;
// File Layer
let config_path = &args.config;
let is_default_path = config_path.to_str() == Some("config.toml");
builder = builder.add_source(File::from(config_path.as_path()).required(!is_default_path));
// Environment Layer (e.g. APP_LISTEN_PORT)
builder = builder.add_source(Environment::with_prefix("APP").separator("_"));
// Global log level override
if let Some(ref level) = args.log_level {
builder = builder.set_override("log_level", level.clone())?;
}
// Command-specific overrides via Trait
builder = handler.apply_overrides(builder)?;
builder

16
apps/app_api/src/config/auth.rs

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

14
apps/app_api/src/config/database.rs

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

40
apps/app_api/src/container.rs

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

45
apps/app_api/src/dependencies.rs

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

101
apps/app_api/src/error.rs

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

4
apps/app_api/src/main.rs

@ -2,7 +2,9 @@ mod api;
mod app;
mod commands;
mod config;
mod container;
mod dependencies;
mod error;
mod middleware;
mod router;
mod state;

38
apps/app_api/src/middleware.rs

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

8
apps/app_api/src/middleware/auth.rs

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

4
apps/app_api/src/router.rs

@ -3,10 +3,12 @@ use std::sync::Arc;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use crate::api;
use crate::dependencies::AppDependencies;
use crate::state::AppState;
pub async fn create_router() -> anyhow::Result<Router> {
let state = Arc::new(AppState::new().await?);
let dependencies = AppDependencies::new("sqlite:app.db", None).await?;
let state = Arc::new(AppState::new(dependencies).await);
Ok(api::routes()
.with_state(state)

12
apps/app_api/src/state.rs

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

13
lib/src/auth.rs

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

72
lib/src/auth/domain.rs

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

47
lib/src/auth/infrastructure/api_token.rs

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

68
lib/src/auth/infrastructure/jwt.rs

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

58
lib/src/auth/infrastructure/store.rs

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

59
lib/src/auth/service.rs

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

15
lib/src/auth/traits.rs

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

1
lib/src/common.rs

@ -2,5 +2,6 @@ pub mod entities;
pub mod errors;
pub mod traits;
pub use self::errors::{AuthError, ServiceError};
pub use self::traits::SystemDecoder;
pub use self::traits::SystemEncoder;

54
lib/src/common/errors.rs

@ -5,7 +5,7 @@ pub enum RepositoryError {
#[error("Data source connection failed")]
ConnectionFailed,
#[error("'{0}' not found")]
#[error("\'{0}\' not found")]
NotFound(String),
#[error("Storage error: {0}")]
@ -29,3 +29,55 @@ pub enum CodecError {
#[error("unexpected error: {0}")]
UnexpectedError(String),
}
#[derive(Debug, Error)]
pub enum ServiceError {
#[error("Repository error: {0}")]
Repository(#[from] RepositoryError),
#[error("Codec error: {0}")]
Codec(#[from] CodecError),
#[error("Dictionary not found: {0}")]
DictionaryNotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Service unavailable: {0}")]
Unavailable(String),
}
impl From<crate::sys_major::lvmap::LenValueMapError> for ServiceError {
fn from(err: crate::sys_major::lvmap::LenValueMapError) -> Self {
match err {
crate::sys_major::lvmap::LenValueMapError::Codec(e) => ServiceError::Codec(e),
crate::sys_major::lvmap::LenValueMapError::Repository(e) => ServiceError::Repository(e),
crate::sys_major::lvmap::LenValueMapError::Build(e) => ServiceError::Unavailable(e),
crate::sys_major::lvmap::LenValueMapError::Parse(e) => {
ServiceError::InvalidInput(e.to_string())
}
}
}
}
#[derive(Debug, Error)]
pub enum AuthError {
#[error("Invalid token")]
InvalidToken,
#[error("Token expired")]
TokenExpired,
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Unauthorized access")]
Unauthorized,
#[error("Authentication failed: {0}")]
AuthenticationFailed(String),
#[error("Token store error: {0}")]
StoreError(String),
}

14
lib/src/dictionary.rs

@ -1,5 +1,6 @@
mod dict_importer;
mod infrastructure;
pub mod service;
use futures::stream::BoxStream;
@ -58,23 +59,22 @@ pub trait DictRepository: Send + Sync {
async fn count_entries(&self) -> Result<u64, RepositoryError>;
/// "Upsert" logic:
/// - If entry exists (by text), update metadata.
/// - If not, insert new.
/// - IDs are handled by the Database.
async fn save_entries(&self, entries: &[DictEntry]) -> Result<(), RepositoryError>;
/// Fetch a page of entries.
async fn fetch_many(&self, limit: usize, offset: usize) -> Result<Dict, RepositoryError>;
/// Returns a cold stream that fetches strings in chunks.
/// The stream yields `Result<Vec<String>, RepositoryError>`.
async fn stream_batches(
&self,
batch_size: usize,
) -> Result<BoxStream<'_, Result<Vec<String>, RepositoryError>>, RepositoryError>;
}
#[async_trait::async_trait]
pub trait DictRepositoryFactory: Send + Sync {
async fn create(&self, dict_name: &str) -> Result<Box<dyn DictRepository>, RepositoryError>;
async fn list_all(&self) -> Result<Vec<String>, RepositoryError>;
}
pub trait DictSource {
fn next_entry(&mut self) -> Option<Result<DictEntry, anyhow::Error>>;
}

35
lib/src/dictionary/infrastructure/sqlite_dict_repository.rs

@ -1,34 +1,30 @@
use crate::common::errors::RepositoryError;
use crate::dictionary::{Dict, DictEntry, DictRepository};
use crate::dictionary::{Dict, DictEntry, DictRepository, DictRepositoryFactory};
use futures::TryStreamExt;
use futures::stream::BoxStream;
use sqlx::{Row, SqlitePool, sqlite::SqliteConnectOptions};
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
#[derive(sqlx::FromRow)]
struct SqliteEntryDto {
id: i64,
text: String,
// sqlx reads the DB column into this specific wrapper
metadata: sqlx::types::Json<HashMap<String, String>>,
}
// Mapper: DTO -> Domain Entity
impl From<SqliteEntryDto> for DictEntry {
fn from(dto: SqliteEntryDto) -> Self {
Self {
id: Some(dto.id as u64),
text: dto.text,
// Unwrap the sqlx wrapper to get the inner HashMap
metadata: dto.metadata.0,
}
}
}
// --- REPOSITORY IMPLEMENTATION ---
#[derive(Clone)]
pub struct SqliteDictRepository {
pool: SqlitePool,
@ -45,7 +41,6 @@ impl SqliteDictRepository {
.await
.map_err(|_| RepositoryError::ConnectionFailed)?;
// Ensure tables exist with proper Normalization and Constraints
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS dictionaries (
@ -61,7 +56,6 @@ impl SqliteDictRepository {
metadata TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(dictionary_id) REFERENCES dictionaries(id) ON DELETE CASCADE,
-- This constraint allows us to update existing words instead of duplicating them
UNIQUE(dictionary_id, text)
);
"#,
@ -71,12 +65,11 @@ impl SqliteDictRepository {
.map_err(|e| RepositoryError::StorageError(e.to_string()))?;
Ok(Self {
pool: pool,
pool,
dict_name: "default_dict".into(),
})
}
// Helper: Resolve dictionary name to ID
async fn get_dict_id(&self) -> Result<i64, RepositoryError> {
let row = sqlx::query("SELECT id FROM dictionaries WHERE name = ?")
.bind(&self.dict_name)
@ -250,7 +243,27 @@ impl DictRepository for SqliteDictRepository {
RepositoryError::StorageError(e.to_string())
});
// 4. Box the stream to erase the complex iterator type (Type Erasure)
Ok(Box::pin(stream))
}
}
#[async_trait::async_trait]
impl DictRepositoryFactory for SqliteDictRepository {
async fn create(&self, dict_name: &str) -> Result<Box<dyn DictRepository>, RepositoryError> {
let mut repo = Self {
pool: self.pool.clone(),
dict_name: dict_name.to_string(),
};
repo.create_dict().await?;
Ok(Box::new(repo))
}
async fn list_all(&self) -> Result<Vec<String>, RepositoryError> {
let rows = sqlx::query("SELECT name FROM dictionaries ORDER BY name")
.fetch_all(&self.pool)
.await
.map_err(|e| RepositoryError::StorageError(e.to_string()))?;
Ok(rows.iter().map(|row| row.get("name")).collect())
}
}

66
lib/src/dictionary/service.rs

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

21
lib/src/lib.rs

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

8
lib/src/sys_major.rs

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

36
lib/src/sys_major/service.rs

@ -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…
Cancel
Save