Compare commits
28 Commits
master
...
develop-re
| Author | SHA1 | Date |
|---|---|---|
|
|
a1d5d6977a | 4 months ago |
|
|
7aa92ae6c1 | 4 months ago |
|
|
da43970a46 | 4 months ago |
|
|
239e26b08c | 4 months ago |
|
|
b37c0d4e66 | 4 months ago |
|
|
c8c6cfd57f | 4 months ago |
|
|
fa25f6c927 | 4 months ago |
|
|
307971e600 | 4 months ago |
|
|
21f3b09591 | 4 months ago |
|
|
d6a475a0ca | 5 months ago |
|
|
c5466baebb | 5 months ago |
|
|
dd1b3ddc99 | 5 months ago |
|
|
7e19771353 | 5 months ago |
|
|
6189b4318b | 5 months ago |
|
|
05be6054a2 | 5 months ago |
|
|
c67bc5b2e9 | 5 months ago |
|
|
c281ea2b2f | 5 months ago |
|
|
b86a375b1c | 5 months ago |
|
|
0be1288638 | 5 months ago |
|
|
091c317266 | 5 months ago |
|
|
6d481552d7 | 5 months ago |
|
|
19e80cb072 | 6 months ago |
|
|
150020df89 | 6 months ago |
|
|
301eb8e3a0 | 6 months ago |
|
|
b7110d8d85 | 6 months ago |
|
|
ff94981dde | 6 months ago |
|
|
03cd46441c | 6 months ago |
|
|
93ea6c305d | 6 months ago |
64 changed files with 117 additions and 2186 deletions
@ -1,11 +1,11 @@ |
|||||||
[package] |
[package] |
||||||
name = "phomnemic-cli" |
name = "phomnemic" |
||||||
version = "0.1.0" |
version = "0.1.0" |
||||||
edition = "2024" |
edition = "2024" |
||||||
|
|
||||||
[dependencies] |
[dependencies] |
||||||
# Internal |
# Internal |
||||||
applib = { path = "../../lib" } |
applib = { path = "../lib" } |
||||||
|
|
||||||
|
|
||||||
# Runtime & Async |
# Runtime & Async |
||||||
@ -1,29 +0,0 @@ |
|||||||
[package] |
|
||||||
name = "phomnemic-server" |
|
||||||
version = "0.1.0" |
|
||||||
edition = "2024" |
|
||||||
|
|
||||||
[dependencies] |
|
||||||
# Internal |
|
||||||
applib = { path = "../../lib" } |
|
||||||
|
|
||||||
# Runtime & Async |
|
||||||
tokio = { version = "1.48", features = ["full"] } |
|
||||||
anyhow = "1.0" |
|
||||||
tracing = "0.1" |
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] } |
|
||||||
async-trait = "0.1" |
|
||||||
|
|
||||||
# Configuration & Inputs |
|
||||||
serde = { version = "1.0", features = ["derive"] } |
|
||||||
serde_json = "1.0" |
|
||||||
toml = "0.9.8" |
|
||||||
clap = { version = "4.5", features = ["derive", "env"] } |
|
||||||
config = "0.15.19" |
|
||||||
const_format = "0.2.35" |
|
||||||
chrono = { version = "0.4", features = ["serde"] } |
|
||||||
|
|
||||||
# Web Framework |
|
||||||
axum = "0.8" |
|
||||||
tower = "0.5" |
|
||||||
tower-http = { version = "0.6", features = ["trace", "cors"] } |
|
||||||
@ -1,13 +0,0 @@ |
|||||||
[listen] |
|
||||||
host = "0.0.0.0" |
|
||||||
port = 3000 |
|
||||||
|
|
||||||
log_level = "info" |
|
||||||
|
|
||||||
[auth] |
|
||||||
firebase_project_id = "your-firebase-project-id" |
|
||||||
firebase_emulator_url = "http://localhost:9099" |
|
||||||
api_tokens = ["your-api-key-1", "your-api-key-2"] |
|
||||||
|
|
||||||
[database] |
|
||||||
url = "sqlite:app.db" |
|
||||||
@ -1,9 +0,0 @@ |
|||||||
pub mod v1; |
|
||||||
|
|
||||||
use crate::state::AppState; |
|
||||||
use axum::Router; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
pub fn routes(state: Arc<AppState>) -> Router<Arc<AppState>> { |
|
||||||
Router::new().nest("/api/v1", v1::routes(state)) |
|
||||||
} |
|
||||||
@ -1,75 +0,0 @@ |
|||||||
pub mod auth; |
|
||||||
pub mod dictionary; |
|
||||||
pub mod info; |
|
||||||
pub mod major; |
|
||||||
|
|
||||||
use crate::state::AppState; |
|
||||||
use axum::{ |
|
||||||
Json, Router, |
|
||||||
extract::Request, |
|
||||||
extract::State, |
|
||||||
http::StatusCode, |
|
||||||
middleware::Next, |
|
||||||
response::{IntoResponse, Response}, |
|
||||||
}; |
|
||||||
use serde::Serialize; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
struct ErrorResponseBody { |
|
||||||
error: String, |
|
||||||
} |
|
||||||
|
|
||||||
pub fn routes(state: Arc<AppState>) -> Router<Arc<AppState>> { |
|
||||||
Router::new() |
|
||||||
.nest("/info", info::routes()) |
|
||||||
.nest("/auth", auth::routes()) |
|
||||||
.nest("/dicts", dictionary::routes()) |
|
||||||
.nest("/major", major::routes()) |
|
||||||
.route_layer(axum::middleware::from_fn_with_state( |
|
||||||
state, |
|
||||||
|state: State<Arc<AppState>>, request: Request, next: Next| async move { |
|
||||||
auth_middleware_inner(state, request, next).await |
|
||||||
}, |
|
||||||
)) |
|
||||||
} |
|
||||||
|
|
||||||
async fn auth_middleware_inner( |
|
||||||
state: State<Arc<AppState>>, |
|
||||||
mut request: Request, |
|
||||||
next: Next, |
|
||||||
) -> Response { |
|
||||||
let auth_header = request |
|
||||||
.headers() |
|
||||||
.get("Authorization") |
|
||||||
.and_then(|h| h.to_str().ok()); |
|
||||||
|
|
||||||
let api_key_header = request |
|
||||||
.headers() |
|
||||||
.get("X-API-Key") |
|
||||||
.and_then(|h| h.to_str().ok()); |
|
||||||
|
|
||||||
let token = if let Some(header) = auth_header { |
|
||||||
header.to_string() |
|
||||||
} else if let Some(key) = api_key_header { |
|
||||||
key.to_string() |
|
||||||
} else { |
|
||||||
let error = ErrorResponseBody { |
|
||||||
error: "Missing authorization header or API key".to_string(), |
|
||||||
}; |
|
||||||
return (StatusCode::UNAUTHORIZED, Json(error)).into_response(); |
|
||||||
}; |
|
||||||
|
|
||||||
match state.0.dependencies.auth_service.authenticate(&token).await { |
|
||||||
Ok(claims) => { |
|
||||||
request.extensions_mut().insert(claims); |
|
||||||
next.run(request).await |
|
||||||
} |
|
||||||
Err(_) => { |
|
||||||
let error = ErrorResponseBody { |
|
||||||
error: "Unauthorized".to_string(), |
|
||||||
}; |
|
||||||
(StatusCode::UNAUTHORIZED, Json(error)).into_response() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,40 +0,0 @@ |
|||||||
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: Option<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 token = req.token.ok_or_else(|| ErrorResponse { |
|
||||||
error: "Invalid input".to_string(), |
|
||||||
message: Some("Token field is required".to_string()), |
|
||||||
})?; |
|
||||||
|
|
||||||
let claims = state.dependencies.auth_service.authenticate(&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)) |
|
||||||
} |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
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)) |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
use axum::{ |
|
||||||
Json, Router, |
|
||||||
extract::State, |
|
||||||
routing::{get, post}, |
|
||||||
}; |
|
||||||
use chrono::Utc; |
|
||||||
use serde::Serialize; |
|
||||||
use serde_json::Value; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
use crate::state::AppState; |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct EchoResponse { |
|
||||||
pub data: Value, |
|
||||||
pub timestamp: String, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct VersionResponse { |
|
||||||
pub name: String, |
|
||||||
pub version: String, |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn echo_handler( |
|
||||||
State(_state): State<Arc<AppState>>, |
|
||||||
Json(payload): Json<Value>, |
|
||||||
) -> Json<EchoResponse> { |
|
||||||
Json(EchoResponse { |
|
||||||
data: payload, |
|
||||||
timestamp: Utc::now().to_rfc3339(), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<VersionResponse> { |
|
||||||
Json(VersionResponse { |
|
||||||
name: state.name.clone(), |
|
||||||
version: state.version.clone(), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn routes() -> Router<Arc<AppState>> { |
|
||||||
Router::new() |
|
||||||
.route("/echo", post(echo_handler)) |
|
||||||
.route("/version", get(version_handler)) |
|
||||||
} |
|
||||||
@ -1,84 +0,0 @@ |
|||||||
use axum::{ |
|
||||||
Json, Router, |
|
||||||
extract::{Path, Query, State}, |
|
||||||
routing::get, |
|
||||||
}; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
use crate::error::ErrorResponse; |
|
||||||
use crate::state::AppState; |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)] |
|
||||||
pub struct EncodeQuery { |
|
||||||
pub dict: Option<String>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)] |
|
||||||
pub struct DecodeQuery {} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct EncodeResponse { |
|
||||||
pub input: String, |
|
||||||
pub dict: String, |
|
||||||
pub result: Vec<Vec<EncodePart>>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct EncodePart { |
|
||||||
pub value: u64, |
|
||||||
pub words: Vec<String>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct DecodeResponse { |
|
||||||
pub input: String, |
|
||||||
pub result: String, |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn encode_handler( |
|
||||||
State(state): State<Arc<AppState>>, |
|
||||||
Path(input): Path<String>, |
|
||||||
Query(params): Query<EncodeQuery>, |
|
||||||
) -> Result<Json<EncodeResponse>, ErrorResponse> { |
|
||||||
let dict_name = params.dict.unwrap_or_else(|| "demo_pl".to_string()); |
|
||||||
let result = state.dependencies.major_system_service.encode(&input)?; |
|
||||||
|
|
||||||
let encoded_parts: Vec<Vec<EncodePart>> = result |
|
||||||
.iter() |
|
||||||
.map(|split| { |
|
||||||
split |
|
||||||
.iter() |
|
||||||
.map(|part| EncodePart { |
|
||||||
value: part.value, |
|
||||||
words: part.words.clone(), |
|
||||||
}) |
|
||||||
.collect() |
|
||||||
}) |
|
||||||
.collect(); |
|
||||||
|
|
||||||
Ok(Json(EncodeResponse { |
|
||||||
input, |
|
||||||
dict: dict_name, |
|
||||||
result: encoded_parts, |
|
||||||
})) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn decode_handler( |
|
||||||
State(state): State<Arc<AppState>>, |
|
||||||
Path(input): Path<String>, |
|
||||||
Query(_params): Query<DecodeQuery>, |
|
||||||
) -> Result<Json<DecodeResponse>, ErrorResponse> { |
|
||||||
let result = state.dependencies.major_system_service.decode(&input)?; |
|
||||||
|
|
||||||
Ok(Json(DecodeResponse { |
|
||||||
input, |
|
||||||
result: result.as_str().to_string(), |
|
||||||
})) |
|
||||||
} |
|
||||||
|
|
||||||
pub fn routes() -> Router<Arc<AppState>> { |
|
||||||
Router::new() |
|
||||||
.route("/encode/pl/{input}", get(encode_handler)) |
|
||||||
.route("/decode/pl/{input}", get(decode_handler)) |
|
||||||
} |
|
||||||
@ -1,37 +0,0 @@ |
|||||||
use crate::commands::{AppCommand, CliArgs}; |
|
||||||
use crate::config::AppConfig; |
|
||||||
use anyhow::Result; |
|
||||||
use clap::Parser; |
|
||||||
use tracing::debug; |
|
||||||
|
|
||||||
pub struct Application { |
|
||||||
config: AppConfig, |
|
||||||
command: Box<dyn AppCommand>, |
|
||||||
} |
|
||||||
|
|
||||||
impl Application { |
|
||||||
pub async fn build() -> Result<Self> { |
|
||||||
let args = CliArgs::parse(); |
|
||||||
|
|
||||||
let app_cmd = args.command.into_app_command(); |
|
||||||
|
|
||||||
let config = AppConfig::build(&args.global, app_cmd.as_ref())?; |
|
||||||
|
|
||||||
tracing_subscriber::fmt() |
|
||||||
.compact() |
|
||||||
.with_env_filter(&config.log_level) |
|
||||||
.with_target(false) |
|
||||||
.init(); |
|
||||||
|
|
||||||
debug!("Bootstrapping application..."); |
|
||||||
|
|
||||||
Ok(Self { |
|
||||||
config, |
|
||||||
command: app_cmd, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn run(self) -> Result<()> { |
|
||||||
self.command.execute(&self.config).await |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,69 +0,0 @@ |
|||||||
pub mod listen; |
|
||||||
|
|
||||||
use crate::config::AppConfig; |
|
||||||
use anyhow::Result; |
|
||||||
use async_trait::async_trait; |
|
||||||
use clap::Subcommand; |
|
||||||
use clap::{Args as ClapArgs, Parser}; |
|
||||||
use config::ConfigBuilder; |
|
||||||
use config::builder::DefaultState; |
|
||||||
use std::path::PathBuf; |
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone)] |
|
||||||
pub enum Command { |
|
||||||
Listen(listen::ListenCmd), |
|
||||||
} |
|
||||||
|
|
||||||
impl Command { |
|
||||||
pub fn into_app_command(self) -> Box<dyn AppCommand> { |
|
||||||
match self { |
|
||||||
Command::Listen(cmd) => Box::new(cmd), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Parser, Debug)] |
|
||||||
#[command(author, version, about)] |
|
||||||
pub struct CliArgs { |
|
||||||
#[command(flatten)] |
|
||||||
pub global: GlobalArgs, |
|
||||||
|
|
||||||
#[command(subcommand)] |
|
||||||
pub command: Command, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(ClapArgs, Debug)] |
|
||||||
pub struct GlobalArgs { |
|
||||||
#[arg(short, long, default_value = "config.toml")] |
|
||||||
pub config: PathBuf, |
|
||||||
|
|
||||||
#[arg(long, help = defaults::HELP_LOG)] |
|
||||||
pub log_level: Option<String>, |
|
||||||
} |
|
||||||
|
|
||||||
pub trait Configurable { |
|
||||||
fn apply_defaults( |
|
||||||
&self, |
|
||||||
builder: ConfigBuilder<DefaultState>, |
|
||||||
) -> Result<ConfigBuilder<DefaultState>>; |
|
||||||
fn apply_overrides( |
|
||||||
&self, |
|
||||||
builder: ConfigBuilder<DefaultState>, |
|
||||||
) -> Result<ConfigBuilder<DefaultState>>; |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
pub trait Executable { |
|
||||||
async fn execute(&self, config: &AppConfig) -> Result<()>; |
|
||||||
} |
|
||||||
|
|
||||||
pub trait AppCommand: Configurable + Executable {} |
|
||||||
|
|
||||||
impl<T: Configurable + Executable> AppCommand for T {} |
|
||||||
|
|
||||||
mod defaults { |
|
||||||
use const_format::formatcp; |
|
||||||
|
|
||||||
pub const LOG_LEVEL: &str = "info"; |
|
||||||
pub const HELP_LOG: &str = formatcp!("Override Log Level [default: {}]", LOG_LEVEL); |
|
||||||
} |
|
||||||
@ -1,113 +0,0 @@ |
|||||||
use crate::commands::{ClapArgs, Configurable, Executable}; |
|
||||||
use crate::config::AppConfig; |
|
||||||
use crate::router; |
|
||||||
|
|
||||||
use anyhow::Result; |
|
||||||
use async_trait::async_trait; |
|
||||||
use config::ConfigBuilder; |
|
||||||
use config::builder::DefaultState; |
|
||||||
use serde::Deserialize; |
|
||||||
use tokio::net::TcpListener; |
|
||||||
use tracing::info; |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)] |
|
||||||
pub struct Config { |
|
||||||
pub host: String, |
|
||||||
pub port: u16, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(ClapArgs, Debug, Clone)] |
|
||||||
pub struct ListenCmd { |
|
||||||
#[arg(short = 'H', long, help = defaults::HELP_LISTEN_HOST)] |
|
||||||
pub host: Option<String>, |
|
||||||
|
|
||||||
#[arg(short, long, help = defaults::HELP_LISTEN_PORT)] |
|
||||||
pub port: Option<u16>, |
|
||||||
} |
|
||||||
|
|
||||||
impl Configurable for ListenCmd { |
|
||||||
fn apply_defaults( |
|
||||||
&self, |
|
||||||
builder: ConfigBuilder<DefaultState>, |
|
||||||
) -> Result<ConfigBuilder<DefaultState>> { |
|
||||||
builder |
|
||||||
.set_default("log_level", defaults::LOG_LEVEL)? |
|
||||||
.set_default("listen.host", defaults::LISTEN_HOST)? |
|
||||||
.set_default("listen.port", defaults::LISTEN_PORT) |
|
||||||
.map_err(Into::into) |
|
||||||
} |
|
||||||
|
|
||||||
fn apply_overrides( |
|
||||||
&self, |
|
||||||
builder: ConfigBuilder<DefaultState>, |
|
||||||
) -> Result<ConfigBuilder<DefaultState>> { |
|
||||||
let mut builder = builder; |
|
||||||
|
|
||||||
if let Some(host) = &self.host { |
|
||||||
builder = builder.set_override("listen.host", host.clone())?; |
|
||||||
} |
|
||||||
if let Some(port) = &self.port { |
|
||||||
builder = builder.set_override("listen.port", port.clone())?; |
|
||||||
} |
|
||||||
Ok(builder) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl Executable for ListenCmd { |
|
||||||
async fn execute(&self, config: &AppConfig) -> Result<()> { |
|
||||||
let listen_config = config |
|
||||||
.listen |
|
||||||
.as_ref() |
|
||||||
.ok_or_else(|| anyhow::anyhow!("Listen config missing"))?; |
|
||||||
|
|
||||||
let app = router::create_router(config).await?; |
|
||||||
let addr = format!("{}:{}", listen_config.host, listen_config.port); |
|
||||||
let listener = TcpListener::bind(&addr).await?; |
|
||||||
|
|
||||||
info!("Starting server on {}", addr); |
|
||||||
|
|
||||||
// axum::serve(listener, app.into_make_service()).await?;
|
|
||||||
axum::serve(listener, app) |
|
||||||
.with_graceful_shutdown(shutdown_signal()) |
|
||||||
.await?; |
|
||||||
|
|
||||||
info!("Server shut down gracefully"); |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async fn shutdown_signal() { |
|
||||||
let ctrl_c = async { |
|
||||||
tokio::signal::ctrl_c() |
|
||||||
.await |
|
||||||
.expect("Failed to install Ctrl+C handler"); |
|
||||||
}; |
|
||||||
|
|
||||||
#[cfg(unix)] |
|
||||||
let terminate = async { |
|
||||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) |
|
||||||
.expect("Failed to install signal handler") |
|
||||||
.recv() |
|
||||||
.await; |
|
||||||
info!("Received SIGTERM signal"); |
|
||||||
}; |
|
||||||
|
|
||||||
#[cfg(not(unix))] |
|
||||||
let terminate = std::future::pending::<()>(); |
|
||||||
|
|
||||||
tokio::select! { |
|
||||||
_ = ctrl_c => {}, |
|
||||||
_ = terminate => {}, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
mod defaults { |
|
||||||
use const_format::formatcp; |
|
||||||
pub const LOG_LEVEL: &str = "info"; |
|
||||||
pub const LISTEN_HOST: &str = "0.0.0.0"; |
|
||||||
pub const LISTEN_PORT: u16 = 3000; |
|
||||||
pub const HELP_LISTEN_HOST: &str = formatcp!("Host address [default: {}]", LISTEN_HOST); |
|
||||||
pub const HELP_LISTEN_PORT: &str = formatcp!("Port to listen on [default: {}]", LISTEN_PORT); |
|
||||||
} |
|
||||||
@ -1,48 +0,0 @@ |
|||||||
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(); |
|
||||||
|
|
||||||
builder = handler.apply_defaults(builder)?; |
|
||||||
|
|
||||||
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)); |
|
||||||
|
|
||||||
builder = builder.add_source(Environment::with_prefix("APP").separator("_")); |
|
||||||
|
|
||||||
if let Some(ref level) = args.log_level { |
|
||||||
builder = builder.set_override("log_level", level.clone())?; |
|
||||||
} |
|
||||||
|
|
||||||
builder = handler.apply_overrides(builder)?; |
|
||||||
|
|
||||||
builder |
|
||||||
.build() |
|
||||||
.context("Failed to build configuration layers")? |
|
||||||
.try_deserialize() |
|
||||||
.context("Failed to deserialize Config") |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,18 +0,0 @@ |
|||||||
use serde::Deserialize; |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)] |
|
||||||
pub struct AuthConfig { |
|
||||||
pub firebase_project_id: Option<String>, |
|
||||||
pub firebase_emulator_url: Option<String>, |
|
||||||
pub api_tokens: Vec<String>, |
|
||||||
} |
|
||||||
|
|
||||||
impl Default for AuthConfig { |
|
||||||
fn default() -> Self { |
|
||||||
Self { |
|
||||||
firebase_project_id: None, |
|
||||||
firebase_emulator_url: None, |
|
||||||
api_tokens: Vec::new(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,14 +0,0 @@ |
|||||||
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,52 +0,0 @@ |
|||||||
use applib::sys_major::rules_pl; |
|
||||||
use applib::sys_major::{Decoder, Encoder, LenValueMap}; |
|
||||||
use applib::{ |
|
||||||
ApiTokenAuthenticator, AuthService, Authenticator, DictionaryService, InMemoryTokenStore, |
|
||||||
JwtAuthenticator, MajorSystemService, SqliteDictRepository, SystemDecoder, SystemEncoder, |
|
||||||
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>, |
|
||||||
firebase_emulator_url: Option<String>, |
|
||||||
api_tokens: Vec<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(emulator_url) = firebase_emulator_url { |
|
||||||
Arc::new(JwtAuthenticator::with_emulator(emulator_url)) |
|
||||||
} else if let Some(project_id) = firebase_project_id { |
|
||||||
Arc::new(JwtAuthenticator::new(project_id)) |
|
||||||
} else { |
|
||||||
Arc::new(JwtAuthenticator::with_placeholder()) |
|
||||||
}; |
|
||||||
|
|
||||||
let api_token_authenticator = ApiTokenAuthenticator::with_tokens(api_tokens); |
|
||||||
let api_token_auth: Arc<dyn Authenticator> = Arc::new(api_token_authenticator); |
|
||||||
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 encoder: Arc<dyn SystemEncoder> = Arc::new(Encoder::new(LenValueMap::new())); |
|
||||||
let major_system_service = Arc::new(MajorSystemService::new(decoder).with_encoder(encoder)); |
|
||||||
|
|
||||||
Ok(Self { |
|
||||||
dictionary_service, |
|
||||||
auth_service, |
|
||||||
major_system_service, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,100 +0,0 @@ |
|||||||
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 { |
|
||||||
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()), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,18 +0,0 @@ |
|||||||
mod api; |
|
||||||
mod app; |
|
||||||
mod commands; |
|
||||||
mod config; |
|
||||||
mod dependencies; |
|
||||||
mod error; |
|
||||||
mod router; |
|
||||||
mod state; |
|
||||||
|
|
||||||
use anyhow::Result; |
|
||||||
use app::Application; |
|
||||||
|
|
||||||
#[tokio::main] |
|
||||||
async fn main() -> Result<()> { |
|
||||||
let app = Application::build().await?; |
|
||||||
app.run().await?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
@ -1,26 +0,0 @@ |
|||||||
use std::sync::Arc; |
|
||||||
use tower_http::{cors::CorsLayer, trace::TraceLayer}; |
|
||||||
|
|
||||||
use crate::api; |
|
||||||
use crate::config::AppConfig; |
|
||||||
use crate::dependencies::AppDependencies; |
|
||||||
use crate::state::AppState; |
|
||||||
|
|
||||||
pub async fn create_router(config: &AppConfig) -> anyhow::Result<axum::Router> { |
|
||||||
let database_url = &config.database.url; |
|
||||||
|
|
||||||
let dependencies = AppDependencies::new( |
|
||||||
database_url, |
|
||||||
config.auth.firebase_project_id.clone(), |
|
||||||
config.auth.firebase_emulator_url.clone(), |
|
||||||
config.auth.api_tokens.clone(), |
|
||||||
) |
|
||||||
.await?; |
|
||||||
|
|
||||||
let state = Arc::new(AppState::new(dependencies).await); |
|
||||||
|
|
||||||
Ok(api::routes(state.clone()) |
|
||||||
.with_state(state) |
|
||||||
.layer(TraceLayer::new_for_http()) |
|
||||||
.layer(CorsLayer::permissive())) |
|
||||||
} |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); |
|
||||||
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
|
||||||
|
|
||||||
use crate::dependencies::AppDependencies; |
|
||||||
|
|
||||||
#[derive(Clone)] |
|
||||||
pub struct AppState { |
|
||||||
pub name: String, |
|
||||||
pub version: String, |
|
||||||
pub dependencies: AppDependencies, |
|
||||||
} |
|
||||||
|
|
||||||
impl AppState { |
|
||||||
pub async fn new(dependencies: AppDependencies) -> Self { |
|
||||||
Self { |
|
||||||
name: APP_NAME.to_string(), |
|
||||||
version: APP_VERSION.to_string(), |
|
||||||
dependencies, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,77 +0,0 @@ |
|||||||
use crate::commands::{ClapArgs, Configurable, Executable}; |
|
||||||
use crate::config::AppConfig; |
|
||||||
use crate::container::Container; |
|
||||||
use anyhow::Result; |
|
||||||
use async_trait::async_trait; |
|
||||||
use config::ConfigBuilder; |
|
||||||
use config::builder::DefaultState; |
|
||||||
use serde::Deserialize; |
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)] |
|
||||||
pub struct Config { |
|
||||||
pub show_counts: bool, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(ClapArgs, Debug, Clone)] |
|
||||||
pub struct ListDictsCmd { |
|
||||||
#[arg(short, long, help = defaults::HELP_LIST_DICTS_COUNTS)] |
|
||||||
pub show_counts: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl Configurable for ListDictsCmd { |
|
||||||
fn apply_defaults( |
|
||||||
&self, |
|
||||||
builder: ConfigBuilder<DefaultState>, |
|
||||||
) -> Result<ConfigBuilder<DefaultState>> { |
|
||||||
builder |
|
||||||
.set_default("list_dicts.show_counts", defaults::SHOW_COUNTS) |
|
||||||
.map_err(Into::into) |
|
||||||
} |
|
||||||
|
|
||||||
fn apply_overrides( |
|
||||||
&self, |
|
||||||
builder: ConfigBuilder<DefaultState>, |
|
||||||
) -> Result<ConfigBuilder<DefaultState>> { |
|
||||||
builder |
|
||||||
.set_override("list_dicts.show_counts", self.show_counts) |
|
||||||
.map_err(Into::into) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[async_trait] |
|
||||||
impl Executable for ListDictsCmd { |
|
||||||
async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()> { |
|
||||||
let config = config |
|
||||||
.list_dicts |
|
||||||
.as_ref() |
|
||||||
.expect("ListDicts config not set"); |
|
||||||
|
|
||||||
let repo = container.create_dict_repo("default").await?; |
|
||||||
|
|
||||||
let dicts = repo.fetch_dicts().await?; |
|
||||||
|
|
||||||
if dicts.is_empty() { |
|
||||||
println!("No dictionaries found."); |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
|
|
||||||
println!("Dictionaries:"); |
|
||||||
for dict_name in &dicts { |
|
||||||
if config.show_counts { |
|
||||||
let count_repo = container.create_dict_repo(dict_name).await?; |
|
||||||
let count = count_repo.count_entries().await?; |
|
||||||
println!(" - {} ({} entries)", dict_name, count); |
|
||||||
} else { |
|
||||||
println!(" - {}", dict_name); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
mod defaults { |
|
||||||
use const_format::formatcp; |
|
||||||
pub const SHOW_COUNTS: bool = false; |
|
||||||
pub const HELP_LIST_DICTS_COUNTS: &str = formatcp!("Show entry counts for each dictionary"); |
|
||||||
} |
|
||||||
@ -1,13 +1,6 @@ |
|||||||
log_level = "trace" |
# Logging configuration |
||||||
|
log_level = "info" |
||||||
|
|
||||||
[listen] |
# Server configuration |
||||||
host = "0.0.0.0" |
[server] |
||||||
port = 3000 |
port = 8080 |
||||||
|
|
||||||
[auth] |
|
||||||
firebase_project_id = "phomnemic" |
|
||||||
# firebase_emulator_url = "http://192.168.1.23:9099" |
|
||||||
api_tokens = ["AIzaSyCCgWH9Qg5vLTMFYLTuU0tyLFKBgtBkucE", "test-api-key"] |
|
||||||
|
|
||||||
[database] |
|
||||||
url = "sqlite:app.db" |
|
||||||
|
|||||||
@ -1,13 +0,0 @@ |
|||||||
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}; |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,288 +0,0 @@ |
|||||||
use crate::auth::domain::AuthClaims; |
|
||||||
use crate::auth::traits::Authenticator; |
|
||||||
use crate::common::errors::AuthError; |
|
||||||
use base64::Engine; |
|
||||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header}; |
|
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
use std::collections::HashMap; |
|
||||||
use std::sync::Arc; |
|
||||||
use std::time::Duration; |
|
||||||
use tokio::sync::RwLock; |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
||||||
struct FirebaseClaims { |
|
||||||
iss: String, |
|
||||||
aud: String, |
|
||||||
auth_time: i64, |
|
||||||
user_id: String, |
|
||||||
sub: String, |
|
||||||
iat: i64, |
|
||||||
exp: i64, |
|
||||||
email: Option<String>, |
|
||||||
email_verified: Option<bool>, |
|
||||||
firebase: Option<FirebaseData>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
||||||
struct FirebaseData { |
|
||||||
identities: Option<serde_json::Value>, |
|
||||||
sign_in_provider: Option<String>, |
|
||||||
tenant: Option<String>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)] |
|
||||||
struct PublicKey { |
|
||||||
n: String, |
|
||||||
e: String, |
|
||||||
kid: String, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)] |
|
||||||
struct JwksResponse { |
|
||||||
keys: Vec<PublicKey>, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Clone)] |
|
||||||
struct KeyCache { |
|
||||||
keys: HashMap<String, DecodingKey>, |
|
||||||
expires_at: i64, |
|
||||||
} |
|
||||||
|
|
||||||
pub struct JwtAuthenticator { |
|
||||||
firebase_project_id: String, |
|
||||||
issuer: String, |
|
||||||
audience: String, |
|
||||||
emulator_url: Option<String>, |
|
||||||
key_cache: Arc<RwLock<Option<KeyCache>>>, |
|
||||||
} |
|
||||||
|
|
||||||
impl JwtAuthenticator { |
|
||||||
pub fn new(firebase_project_id: impl Into<String>) -> Self { |
|
||||||
let project_id = firebase_project_id.into(); |
|
||||||
let issuer = format!("https://securetoken.google.com/{}", project_id); |
|
||||||
Self { |
|
||||||
firebase_project_id: project_id.clone(), |
|
||||||
issuer, |
|
||||||
audience: project_id, |
|
||||||
emulator_url: None, |
|
||||||
key_cache: Arc::new(RwLock::new(None)), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn with_emulator(emulator_url: impl Into<String>) -> Self { |
|
||||||
Self { |
|
||||||
firebase_project_id: "emulator".to_string(), |
|
||||||
issuer: "https://securetoken.google.com/emulator".to_string(), |
|
||||||
audience: "emulator".to_string(), |
|
||||||
emulator_url: Some(emulator_url.into()), |
|
||||||
key_cache: Arc::new(RwLock::new(None)), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub fn with_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(), |
|
||||||
emulator_url: None, |
|
||||||
key_cache: Arc::new(RwLock::new(None)), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn decode_emulator_token(&self, token: &str) -> Result<AuthClaims, AuthError> { |
|
||||||
let parts: Vec<&str> = token.split('.').collect(); |
|
||||||
if parts.len() != 3 { |
|
||||||
return Err(AuthError::InvalidToken); |
|
||||||
} |
|
||||||
|
|
||||||
let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD |
|
||||||
.decode(parts[0]) |
|
||||||
.map_err(|e| { |
|
||||||
eprintln!("Failed to decode emulated header: {}", e); |
|
||||||
AuthError::InvalidToken |
|
||||||
})?; |
|
||||||
let header: serde_json::Value = |
|
||||||
serde_json::from_slice(&header_json).map_err(|_| AuthError::InvalidToken)?; |
|
||||||
|
|
||||||
if header.get("alg").and_then(|v| v.as_str()) != Some("none") { |
|
||||||
return Err(AuthError::InvalidToken); |
|
||||||
} |
|
||||||
|
|
||||||
let payload_json = base64::engine::general_purpose::URL_SAFE_NO_PAD |
|
||||||
.decode(parts[1]) |
|
||||||
.map_err(|e| { |
|
||||||
eprintln!("Failed to decode emulated payload: {}", e); |
|
||||||
AuthError::InvalidToken |
|
||||||
})?; |
|
||||||
let claims: FirebaseClaims = |
|
||||||
serde_json::from_slice(&payload_json).map_err(|_| AuthError::InvalidToken)?; |
|
||||||
|
|
||||||
let now = chrono::Utc::now().timestamp(); |
|
||||||
|
|
||||||
if claims.exp < now { |
|
||||||
return Err(AuthError::AuthenticationFailed("Token expired".to_string())); |
|
||||||
} |
|
||||||
|
|
||||||
if claims.aud != self.audience { |
|
||||||
return Err(AuthError::AuthenticationFailed( |
|
||||||
"Invalid audience".to_string(), |
|
||||||
)); |
|
||||||
} |
|
||||||
|
|
||||||
if claims.iss != self.issuer { |
|
||||||
return Err(AuthError::AuthenticationFailed( |
|
||||||
"Invalid issuer".to_string(), |
|
||||||
)); |
|
||||||
} |
|
||||||
|
|
||||||
let mut auth_claims = AuthClaims::new(claims.user_id) |
|
||||||
.with_expiration(claims.exp) |
|
||||||
.with_iat(claims.iat); |
|
||||||
|
|
||||||
if let Some(email) = claims.email { |
|
||||||
auth_claims = auth_claims.with_email(email); |
|
||||||
} |
|
||||||
|
|
||||||
if let Some(ref firebase) = claims.firebase { |
|
||||||
if let Some(ref provider) = firebase.sign_in_provider { |
|
||||||
auth_claims = auth_claims.with_role(format!("auth:{}", provider)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
auth_claims = auth_claims.with_role("authenticated"); |
|
||||||
|
|
||||||
Ok(auth_claims) |
|
||||||
} |
|
||||||
|
|
||||||
async fn get_public_keys(&self) -> Result<HashMap<String, DecodingKey>, AuthError> { |
|
||||||
let cache = self.key_cache.read().await; |
|
||||||
if let Some(ref cached) = *cache { |
|
||||||
let now = chrono::Utc::now().timestamp(); |
|
||||||
if cached.expires_at > now { |
|
||||||
return Ok(cached.keys.clone()); |
|
||||||
} |
|
||||||
} |
|
||||||
drop(cache); |
|
||||||
|
|
||||||
let url = if let Some(ref emulator) = self.emulator_url { |
|
||||||
format!("{}/.well-known/jwks.json", emulator.trim_end_matches('/')) |
|
||||||
} else { |
|
||||||
format!( |
|
||||||
"https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com" |
|
||||||
) |
|
||||||
}; |
|
||||||
|
|
||||||
let client = reqwest::Client::builder() |
|
||||||
.timeout(Duration::from_secs(10)) |
|
||||||
.build() |
|
||||||
.map_err(|e| { |
|
||||||
AuthError::AuthenticationFailed(format!("Failed to build HTTP client: {}", e)) |
|
||||||
})?; |
|
||||||
|
|
||||||
let response = client.get(&url).send().await.map_err(|e| { |
|
||||||
AuthError::AuthenticationFailed(format!("Failed to fetch public keys: {}", e)) |
|
||||||
})?; |
|
||||||
|
|
||||||
if !response.status().is_success() { |
|
||||||
return Err(AuthError::AuthenticationFailed(format!( |
|
||||||
"Failed to fetch public keys: HTTP {}", |
|
||||||
response.status() |
|
||||||
))); |
|
||||||
} |
|
||||||
|
|
||||||
let body = response.text().await.map_err(|e| { |
|
||||||
AuthError::AuthenticationFailed(format!("Failed to read response: {}", e)) |
|
||||||
})?; |
|
||||||
|
|
||||||
let jwks: JwksResponse = serde_json::from_str(&body) |
|
||||||
.map_err(|e| AuthError::AuthenticationFailed(format!("Failed to parse JWKS: {}", e)))?; |
|
||||||
|
|
||||||
let mut keys = HashMap::new(); |
|
||||||
for key in jwks.keys { |
|
||||||
let decoding_key = DecodingKey::from_rsa_components(&key.n, &key.e).map_err(|e| { |
|
||||||
AuthError::AuthenticationFailed(format!("Failed to create decoding key: {}", e)) |
|
||||||
})?; |
|
||||||
keys.insert(key.kid, decoding_key); |
|
||||||
} |
|
||||||
|
|
||||||
let expires_at = chrono::Utc::now().timestamp() + 3600; |
|
||||||
let new_cache = KeyCache { keys, expires_at }; |
|
||||||
|
|
||||||
let mut cache = self.key_cache.write().await; |
|
||||||
*cache = Some(new_cache); |
|
||||||
|
|
||||||
Ok(cache.as_ref().unwrap().keys.clone()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[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 header_result = decode_header(token); |
|
||||||
|
|
||||||
let header = match header_result { |
|
||||||
Ok(h) => h, |
|
||||||
Err(e) => { |
|
||||||
if self.emulator_url.is_some() { |
|
||||||
if let Ok(claims) = self.decode_emulator_token(token) { |
|
||||||
return Ok(claims); |
|
||||||
} |
|
||||||
} |
|
||||||
tracing::debug!("Failed to decode header: {}", e.to_string()); |
|
||||||
return Err(AuthError::InvalidToken); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
let kid = header |
|
||||||
.kid |
|
||||||
.ok_or_else(|| AuthError::AuthenticationFailed("Token missing key ID".to_string()))?; |
|
||||||
|
|
||||||
let public_keys = self.get_public_keys().await.map_err(|e| { |
|
||||||
AuthError::AuthenticationFailed(format!("Failed to fetch public keys: {}", e)) |
|
||||||
})?; |
|
||||||
let decoding_key = public_keys |
|
||||||
.get(&kid) |
|
||||||
.ok_or_else(|| AuthError::AuthenticationFailed("Unknown key ID".to_string()))?; |
|
||||||
|
|
||||||
let mut validation = Validation::new(Algorithm::RS256); |
|
||||||
validation.set_audience(&[&self.audience]); |
|
||||||
validation.set_issuer(&[&self.issuer]); |
|
||||||
validation.validate_exp = true; |
|
||||||
|
|
||||||
let token_data = |
|
||||||
decode::<FirebaseClaims>(token, decoding_key, &validation).map_err(|e| { |
|
||||||
AuthError::AuthenticationFailed(format!("Token validation failed: {}", e)) |
|
||||||
})?; |
|
||||||
|
|
||||||
let mut claims = AuthClaims::new(token_data.claims.user_id) |
|
||||||
.with_expiration(token_data.claims.exp) |
|
||||||
.with_iat(token_data.claims.iat); |
|
||||||
|
|
||||||
if let Some(email) = token_data.claims.email { |
|
||||||
claims = claims.with_email(email); |
|
||||||
} |
|
||||||
|
|
||||||
if let Some(ref firebase) = token_data.claims.firebase { |
|
||||||
if let Some(ref provider) = firebase.sign_in_provider { |
|
||||||
claims = claims.with_role(format!("auth:{}", provider)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
claims = claims.with_role("authenticated"); |
|
||||||
|
|
||||||
Ok(claims) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl AuthClaims { |
|
||||||
pub fn with_iat(mut self, iat: i64) -> Self { |
|
||||||
self.iat = iat; |
|
||||||
self |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,58 +0,0 @@ |
|||||||
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)) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,59 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,15 +0,0 @@ |
|||||||
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,66 +0,0 @@ |
|||||||
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,14 +1,12 @@ |
|||||||
pub mod decoder; |
pub mod decoder; |
||||||
pub mod encoder; |
pub mod encoder; |
||||||
pub mod lvmap; |
pub mod lvmap; |
||||||
pub mod rules_en; |
pub mod rules_en; // TODO: pub?
|
||||||
pub mod rules_pl; |
pub mod rules_pl; // TODO: pub?
|
||||||
pub mod service; |
|
||||||
|
|
||||||
pub use self::decoder::Decoder; |
pub use self::decoder::Decoder; |
||||||
pub use self::encoder::Encoder; |
pub use self::encoder::Encoder; |
||||||
pub use self::lvmap::LenValueMap; |
pub use self::lvmap::LenValueMap; // TODO: pub?
|
||||||
pub use self::service::MajorSystemService; |
|
||||||
|
|
||||||
#[cfg(test)] |
#[cfg(test)] |
||||||
mod decoder_tests; |
mod decoder_tests; |
||||||
|
|||||||
@ -1,36 +0,0 @@ |
|||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,15 +0,0 @@ |
|||||||
|
|
||||||
if [ -d .venv ]; then |
|
||||||
source .venv/bin/activate |
|
||||||
else |
|
||||||
python3 -m venv .venv |
|
||||||
source .venv/bin/activate |
|
||||||
pip install -r requirements.txt |
|
||||||
fi |
|
||||||
|
|
||||||
export TEST_SERVER_ADDRESS="127.0.0.1:3000" |
|
||||||
export TEST_API_BASE="api/v1" |
|
||||||
|
|
||||||
export TEST_API_KEY="test-api-key" |
|
||||||
export TEST_USER_ID="test-user-id" |
|
||||||
export TEST_VALID_TOKEN="test-api-key" |
|
||||||
@ -1,20 +0,0 @@ |
|||||||
#!/usr/bin/env bash |
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) |
|
||||||
cd "$SCRIPT_DIR" |
|
||||||
|
|
||||||
if [ -z "$TEST_SERVER_ADDRESS" ]; then |
|
||||||
source export.sh |
|
||||||
fi |
|
||||||
|
|
||||||
tavern-ci --alluredir=reports test_plans/version_test.tavern.yaml |
|
||||||
# tavern-ci --alluredir=reports test_plans/auth_test.tavern.yaml |
|
||||||
tavern-ci --alluredir=reports test_plans/decode_test.tavern.yaml |
|
||||||
tavern-ci --alluredir=reports test_plans/dictionary_test.tavern.yaml |
|
||||||
tavern-ci --alluredir=reports test_plans/encode_test.tavern.yaml |
|
||||||
|
|
||||||
# if command -v allure > /dev/null; then |
|
||||||
# allure generate --clean --single-file --output /tmp/vm-allure-report --name index.html reports |
|
||||||
# fi |
|
||||||
|
|
||||||
# allure package: https://github.com/allure-framework/allure2/releases/download/2.34.0/allure_2.34.0-1_all.deb |
|
||||||
@ -1,19 +0,0 @@ |
|||||||
#!/usr/bin/env bash |
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) |
|
||||||
cd "$SCRIPT_DIR" |
|
||||||
|
|
||||||
if [ -z "$1" ]; then |
|
||||||
echo "Usage: $0 <test plan>" |
|
||||||
exit 1 |
|
||||||
fi |
|
||||||
|
|
||||||
if [ -z "$TEST_SERVER_ADDRESS" ]; then |
|
||||||
source export.sh |
|
||||||
fi |
|
||||||
|
|
||||||
tavern-ci --log-cli-level=DEBUG --alluredir=reports $1 |
|
||||||
|
|
||||||
# allure generate --clean --single-file --output /tmp/vm-allure-report --name index.html reports |
|
||||||
|
|
||||||
# allure package: https://github.com/allure-framework/allure2/releases/download/2.34.0/allure_2.34.0-1_all.deb |
|
||||||
@ -1,29 +0,0 @@ |
|||||||
--- |
|
||||||
|
|
||||||
stages: |
|
||||||
- id: clear_test_user |
|
||||||
name: "Purge test user data" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/user/{test_user_id}/clear" |
|
||||||
method: POST |
|
||||||
headers: |
|
||||||
Content-Type: application/json |
|
||||||
Authorization: Bearer {api_key} |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 200 |
|
||||||
|
|
||||||
- id: register_test_user |
|
||||||
name: "Register test user" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/user" |
|
||||||
method: POST |
|
||||||
headers: |
|
||||||
Content-Type: application/json |
|
||||||
Authorization: Bearer {api_key} |
|
||||||
body: |
|
||||||
username: {test_user_id} |
|
||||||
password: password |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 201 |
|
||||||
@ -1,29 +0,0 @@ |
|||||||
test_name: "Test major decode endpoint" |
|
||||||
|
|
||||||
includes: |
|
||||||
- !include includes.yaml |
|
||||||
|
|
||||||
stages: |
|
||||||
|
|
||||||
- name: "Successful decode with valid encoded input" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/major/decode/pl/test" |
|
||||||
method: GET |
|
||||||
headers: |
|
||||||
X-API-Key: "{api_key}" |
|
||||||
response: |
|
||||||
strict: True |
|
||||||
status_code: 200 |
|
||||||
json: |
|
||||||
input: "test" |
|
||||||
result: "101" |
|
||||||
|
|
||||||
- name: "Missing authentication returns 401 error" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/major/decode/pl/hello" |
|
||||||
method: GET |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 401 |
|
||||||
json: |
|
||||||
error: !anystr |
|
||||||
@ -1,30 +0,0 @@ |
|||||||
test_name: "Test dictionary API endpoint" |
|
||||||
|
|
||||||
includes: |
|
||||||
- !include includes.yaml |
|
||||||
|
|
||||||
stages: |
|
||||||
|
|
||||||
- name: "Successful list dictionaries with valid authentication" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/dicts" |
|
||||||
method: GET |
|
||||||
headers: |
|
||||||
X-API-Key: "{api_key}" |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 200 |
|
||||||
json: |
|
||||||
dictionaries: |
|
||||||
- name: !anystr |
|
||||||
entry_count: !anyint |
|
||||||
|
|
||||||
- name: "Missing authentication returns 401 error" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/dicts" |
|
||||||
method: GET |
|
||||||
response: |
|
||||||
strict: True |
|
||||||
status_code: 401 |
|
||||||
json: |
|
||||||
error: !anystr |
|
||||||
@ -1,44 +0,0 @@ |
|||||||
test_name: "Test major encode endpoint" |
|
||||||
|
|
||||||
includes: |
|
||||||
- !include includes.yaml |
|
||||||
|
|
||||||
stages: |
|
||||||
|
|
||||||
- name: "Successful encode with default dictionary" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/major/encode/pl/hello" |
|
||||||
method: GET |
|
||||||
headers: |
|
||||||
X-API-Key: "{api_key}" |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 200 |
|
||||||
json: |
|
||||||
input: "hello" |
|
||||||
dict: "demo_pl" |
|
||||||
result: !anylist |
|
||||||
|
|
||||||
- name: "Successful encode with custom dictionary" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/major/encode/pl/test?dict=demo_pl" |
|
||||||
method: GET |
|
||||||
headers: |
|
||||||
X-API-Key: "{api_key}" |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 200 |
|
||||||
json: |
|
||||||
input: "test" |
|
||||||
dict: "demo_pl" |
|
||||||
result: !anylist |
|
||||||
|
|
||||||
- name: "Missing authentication returns 401 error" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/{api_base}/major/encode/pl/hello" |
|
||||||
method: GET |
|
||||||
response: |
|
||||||
strict: False |
|
||||||
status_code: 401 |
|
||||||
json: |
|
||||||
error: !anystr |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
variables: |
|
||||||
server_address: "{tavern.env_vars.TEST_SERVER_ADDRESS}" |
|
||||||
api_base: "{tavern.env_vars.TEST_API_BASE}" |
|
||||||
api_key: "{tavern.env_vars.TEST_API_KEY}" |
|
||||||
user_id: "{tavern.env_vars.TEST_USER_ID}" |
|
||||||
|
|
||||||
@ -1,20 +0,0 @@ |
|||||||
test_name: "Test version endpoint" |
|
||||||
|
|
||||||
includes: |
|
||||||
- !include includes.yaml |
|
||||||
|
|
||||||
stages: |
|
||||||
- name: "Successful version test - valid authentication returns version info" |
|
||||||
request: |
|
||||||
url: "http://{server_address}/api/v1/info/version" |
|
||||||
method: GET |
|
||||||
headers: |
|
||||||
X-API-Key: "{api_key}" |
|
||||||
response: |
|
||||||
strict: True |
|
||||||
status_code: 200 |
|
||||||
json: |
|
||||||
name: "phomnemic-server" |
|
||||||
version: !anystr |
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in new issue