From ff34b2bbf40ff1d2b20bebd4bbd6cfb5f6b1e3a9 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Fri, 6 Mar 2026 15:06:04 +0100 Subject: [PATCH] chore: Remove admin-ui, improve .env.example, add comprehensive README - Removed deprecated admin-ui Rust application - Updated .env.example with better organization and comments - Added comprehensive README.md with: - Architecture overview - Quick start guide - Development instructions - Feature documentation - Environment variable reference - API documentation link --- .env.example | 45 ++++- README.md | 151 +++++++++++++++++ apps/admin-ui/Cargo.toml | 17 -- apps/admin-ui/Dockerfile | 22 --- apps/admin-ui/src/main.rs | 337 -------------------------------------- 5 files changed, 193 insertions(+), 379 deletions(-) create mode 100644 README.md delete mode 100644 apps/admin-ui/Cargo.toml delete mode 100644 apps/admin-ui/Dockerfile delete mode 100644 apps/admin-ui/src/main.rs diff --git a/.env.example b/.env.example index 3bb00fc..fb53601 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,48 @@ +# Stripstream Librarian - Environment Configuration + +# ============================================================================= +# REQUIRED - Change these values in production! +# ============================================================================= + +# Master key for Meilisearch authentication (required) +MEILI_MASTER_KEY=change-me-in-production + +# Bootstrap token for initial API admin access (required) +# Use this token for the first API calls before creating proper API tokens +API_BOOTSTRAP_TOKEN=change-me-in-production + +# ============================================================================= +# Services Configuration +# ============================================================================= + +# API Service API_LISTEN_ADDR=0.0.0.0:8080 -BACKOFFICE_PORT=8082 API_BASE_URL=http://api:8080 + +# Indexer Service INDEXER_LISTEN_ADDR=0.0.0.0:8081 INDEXER_SCAN_INTERVAL_SECONDS=5 + +# Backoffice Web UI +BACKOFFICE_PORT=8082 + +# ============================================================================= +# Database Configuration +# ============================================================================= + +# PostgreSQL connection string DATABASE_URL=postgres://stripstream:stripstream@postgres:5432/stripstream + +# ============================================================================= +# Search Configuration +# ============================================================================= + +# Meilisearch connection URL MEILI_URL=http://meilisearch:7700 -MEILI_MASTER_KEY=change-me -API_BOOTSTRAP_TOKEN=change-me-bootstrap-token + +# ============================================================================= +# Optional - Development only +# ============================================================================= + +# Path to libraries directory (default: /libraries in Docker) +# LIBRARIES_ROOT_PATH=/libraries diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2d58c5 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# Stripstream Librarian + +A comprehensive comic book and e-book management system with automatic indexing, full-text search, and a modern web interface. + +## Architecture + +The project consists of the following components: + +- **API** (`apps/api/`) - Rust-based REST API service +- **Indexer** (`apps/indexer/`) - Rust-based background indexing service +- **Backoffice** (`apps/backoffice/`) - Next.js web administration interface +- **Infrastructure** (`infra/`) - Docker Compose setup with PostgreSQL and Meilisearch + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose +- (Optional) Rust toolchain for local development +- (Optional) Node.js 18+ for backoffice development + +### Environment Setup + +1. Copy the example environment file: + ```bash + cp .env.example .env + ``` + +2. Edit `.env` and set secure values for: + - `MEILI_MASTER_KEY` - Master key for Meilisearch + - `API_BOOTSTRAP_TOKEN` - Bootstrap token for initial API authentication + +### Running with Docker + +```bash +cd infra +docker compose up -d +``` + +This will start: +- PostgreSQL (port 5432) +- Meilisearch (port 7700) +- API service (port 8080) +- Indexer service (port 8081) +- Backoffice web UI (port 8082) + +### Accessing the Application + +- **Backoffice**: http://localhost:8082 +- **API**: http://localhost:8080 +- **Meilisearch**: http://localhost:7700 + +### Default Credentials + +The default bootstrap token is configured in your `.env` file. Use this for initial API authentication. + +## Development + +### Local Development (without Docker) + +#### API & Indexer (Rust) + +```bash +# Start dependencies +cd infra +docker compose up -d postgres meilisearch + +# Run API +cd apps/api +cargo run + +# Run Indexer (in another terminal) +cd apps/indexer +cargo run +``` + +#### Backoffice (Next.js) + +```bash +cd apps/backoffice +npm install +npm run dev +``` + +The backoffice will be available at http://localhost:3000 + +## Features + +### Libraries Management +- Create and manage multiple libraries +- Configure automatic scanning schedules (hourly, daily, weekly) +- Real-time file watcher for instant indexing +- Full and incremental rebuild options + +### Books Management +- Support for CBZ, CBR, and PDF formats +- Automatic metadata extraction +- Series and volume detection +- Full-text search with Meilisearch + +### Jobs Monitoring +- Real-time job progress tracking +- Detailed statistics (scanned, indexed, removed, errors) +- Job history and logs +- Cancel pending jobs + +### Search +- Full-text search across titles, authors, and series +- Library filtering +- Real-time suggestions + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `API_LISTEN_ADDR` | API service bind address | `0.0.0.0:8080` | +| `INDEXER_LISTEN_ADDR` | Indexer service bind address | `0.0.0.0:8081` | +| `BACKOFFICE_PORT` | Backoffice web UI port | `8082` | +| `DATABASE_URL` | PostgreSQL connection string | `postgres://stripstream:stripstream@postgres:5432/stripstream` | +| `MEILI_URL` | Meilisearch connection URL | `http://meilisearch:7700` | +| `MEILI_MASTER_KEY` | Meilisearch master key (required) | - | +| `API_BOOTSTRAP_TOKEN` | Initial API admin token (required) | - | +| `INDEXER_SCAN_INTERVAL_SECONDS` | Watcher scan interval | `5` | +| `LIBRARIES_ROOT_PATH` | Path to libraries directory | `/libraries` | + +## API Documentation + +The API is documented with OpenAPI/Swagger. When running locally, access the docs at: + +``` +http://localhost:8080/api-docs +``` + +## Project Structure + +``` +stripstream-librarian/ +├── apps/ +│ ├── api/ # Rust REST API +│ ├── indexer/ # Rust background indexer +│ └── backoffice/ # Next.js web UI +├── infra/ +│ ├── docker-compose.yml +│ └── migrations/ # SQL database migrations +├── libraries/ # Book storage (mounted volume) +└── .env # Environment configuration +``` + +## License + +[Your License Here] diff --git a/apps/admin-ui/Cargo.toml b/apps/admin-ui/Cargo.toml deleted file mode 100644 index 05f7842..0000000 --- a/apps/admin-ui/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "admin-ui" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -anyhow.workspace = true -axum.workspace = true -reqwest.workspace = true -serde.workspace = true -serde_json.workspace = true -stripstream-core = { path = "../../crates/core" } -tokio.workspace = true -tracing.workspace = true -tracing-subscriber.workspace = true -uuid.workspace = true diff --git a/apps/admin-ui/Dockerfile b/apps/admin-ui/Dockerfile deleted file mode 100644 index 16087e3..0000000 --- a/apps/admin-ui/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM rust:1-bookworm AS builder -WORKDIR /app - -COPY Cargo.toml ./ -COPY apps/api/Cargo.toml apps/api/Cargo.toml -COPY apps/indexer/Cargo.toml apps/indexer/Cargo.toml -COPY apps/admin-ui/Cargo.toml apps/admin-ui/Cargo.toml -COPY crates/core/Cargo.toml crates/core/Cargo.toml -COPY crates/parsers/Cargo.toml crates/parsers/Cargo.toml -COPY apps/api/src apps/api/src -COPY apps/indexer/src apps/indexer/src -COPY apps/admin-ui/src apps/admin-ui/src -COPY crates/core/src crates/core/src -COPY crates/parsers/src crates/parsers/src - -RUN cargo build --release -p admin-ui - -FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates wget && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/target/release/admin-ui /usr/local/bin/admin-ui -EXPOSE 8082 -CMD ["/usr/local/bin/admin-ui"] diff --git a/apps/admin-ui/src/main.rs b/apps/admin-ui/src/main.rs deleted file mode 100644 index 7c71b5e..0000000 --- a/apps/admin-ui/src/main.rs +++ /dev/null @@ -1,337 +0,0 @@ -use axum::{ - extract::{Form, State}, - response::{Html, Redirect}, - routing::{get, post}, - Router, -}; -use reqwest::Client; -use serde::Deserialize; -use stripstream_core::config::AdminUiConfig; -use tracing::info; -use uuid::Uuid; - -#[derive(Clone)] -struct AppState { - api_base_url: String, - api_token: String, - client: Client, -} - -#[derive(Deserialize)] -struct LibraryDto { - id: Uuid, - name: String, - root_path: String, - enabled: bool, -} - -#[derive(Deserialize)] -struct IndexJobDto { - id: Uuid, - #[serde(rename = "type")] - kind: String, - status: String, - created_at: String, -} - -#[derive(Deserialize)] -struct TokenDto { - id: Uuid, - name: String, - scope: String, - prefix: String, - revoked_at: Option, -} - -#[derive(Deserialize)] -struct CreatedToken { - token: String, -} - -#[derive(Deserialize)] -struct AddLibraryForm { - name: String, - root_path: String, -} - -#[derive(Deserialize)] -struct DeleteLibraryForm { - id: Uuid, -} - -#[derive(Deserialize)] -struct RebuildForm { - library_id: String, -} - -#[derive(Deserialize)] -struct CreateTokenForm { - name: String, - scope: String, -} - -#[derive(Deserialize)] -struct RevokeTokenForm { - id: Uuid, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt() - .with_env_filter( - std::env::var("RUST_LOG").unwrap_or_else(|_| "admin_ui=info,axum=info".to_string()), - ) - .init(); - - let config = AdminUiConfig::from_env()?; - let state = AppState { - api_base_url: config.api_base_url, - api_token: config.api_token, - client: Client::new(), - }; - - let app = Router::new() - .route("/health", get(health)) - .route("/", get(index)) - .route("/libraries", get(libraries_page)) - .route("/libraries/add", post(add_library)) - .route("/libraries/delete", post(delete_library)) - .route("/jobs", get(jobs_page)) - .route("/jobs/rebuild", post(rebuild_jobs)) - .route("/tokens", get(tokens_page)) - .route("/tokens/create", post(create_token)) - .route("/tokens/revoke", post(revoke_token)) - .with_state(state); - - let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?; - info!(addr = %config.listen_addr, "admin ui listening"); - axum::serve(listener, app).await?; - Ok(()) -} - -async fn health() -> &'static str { - "ok" -} - -async fn index() -> Html { - Html(layout( - "Dashboard", - "

Stripstream Admin

Gestion des librairies, jobs d'indexation et tokens API.

Libraries | Jobs | Tokens

", - )) -} - -async fn libraries_page(State(state): State) -> Html { - let libraries = fetch_libraries(&state).await.unwrap_or_default(); - let mut rows = String::new(); - for lib in libraries { - rows.push_str(&format!( - "{}{}{}
", - html_escape(&lib.name), - html_escape(&lib.root_path), - if lib.enabled { "yes" } else { "no" }, - lib.id - )); - } - - let content = format!( - "

Libraries

-
- - - -
- - - {} -
NameRoot PathEnabledActions
", - rows - ); - - Html(layout("Libraries", &content)) -} - -async fn add_library(State(state): State, Form(form): Form) -> Redirect { - let _ = state - .client - .post(format!("{}/libraries", state.api_base_url)) - .bearer_auth(&state.api_token) - .json(&serde_json::json!({"name": form.name, "root_path": form.root_path})) - .send() - .await; - Redirect::to("/libraries") -} - -async fn delete_library(State(state): State, Form(form): Form) -> Redirect { - let _ = state - .client - .delete(format!("{}/libraries/{}", state.api_base_url, form.id)) - .bearer_auth(&state.api_token) - .send() - .await; - Redirect::to("/libraries") -} - -async fn jobs_page(State(state): State) -> Html { - let jobs = fetch_jobs(&state).await.unwrap_or_default(); - let mut rows = String::new(); - for job in jobs { - rows.push_str(&format!( - "{}{}{}{}", - job.id, - html_escape(&job.kind), - html_escape(&job.status), - html_escape(&job.created_at) - )); - } - - let content = format!( - "

Index Jobs

-
- - -
- - - {} -
IDTypeStatusCreated
", - rows - ); - - Html(layout("Jobs", &content)) -} - -async fn rebuild_jobs(State(state): State, Form(form): Form) -> Redirect { - let body = if form.library_id.trim().is_empty() { - serde_json::json!({}) - } else { - serde_json::json!({"library_id": form.library_id.trim()}) - }; - let _ = state - .client - .post(format!("{}/index/rebuild", state.api_base_url)) - .bearer_auth(&state.api_token) - .json(&body) - .send() - .await; - Redirect::to("/jobs") -} - -async fn tokens_page(State(state): State) -> Html { - let tokens = fetch_tokens(&state).await.unwrap_or_default(); - let mut rows = String::new(); - for token in tokens { - rows.push_str(&format!( - "{}{}{}{}
", - html_escape(&token.name), - html_escape(&token.scope), - html_escape(&token.prefix), - if token.revoked_at.is_some() { "yes" } else { "no" }, - token.id - )); - } - - let content = format!( - "

API Tokens

-
- - - -
- - - {} -
NameScopePrefixRevokedActions
", - rows - ); - - Html(layout("Tokens", &content)) -} - -async fn create_token(State(state): State, Form(form): Form) -> Html { - let response = state - .client - .post(format!("{}/admin/tokens", state.api_base_url)) - .bearer_auth(&state.api_token) - .json(&serde_json::json!({"name": form.name, "scope": form.scope})) - .send() - .await; - - match response { - Ok(resp) if resp.status().is_success() => { - let created: Result = resp.json().await; - match created { - Ok(token) => Html(layout( - "Token Created", - &format!( - "

Token Created

Copie ce token maintenant (il ne sera plus affiche):

{}

Back to tokens

", - html_escape(&token.token) - ), - )), - Err(_) => Html(layout("Error", "

Token created but response parse failed.

Back

")), - } - } - _ => Html(layout("Error", "

Token creation failed.

Back

")), - } -} - -async fn revoke_token(State(state): State, Form(form): Form) -> Redirect { - let _ = state - .client - .delete(format!("{}/admin/tokens/{}", state.api_base_url, form.id)) - .bearer_auth(&state.api_token) - .send() - .await; - Redirect::to("/tokens") -} - -async fn fetch_libraries(state: &AppState) -> Result, reqwest::Error> { - state - .client - .get(format!("{}/libraries", state.api_base_url)) - .bearer_auth(&state.api_token) - .send() - .await? - .json::>() - .await -} - -async fn fetch_jobs(state: &AppState) -> Result, reqwest::Error> { - state - .client - .get(format!("{}/index/status", state.api_base_url)) - .bearer_auth(&state.api_token) - .send() - .await? - .json::>() - .await -} - -async fn fetch_tokens(state: &AppState) -> Result, reqwest::Error> { - state - .client - .get(format!("{}/admin/tokens", state.api_base_url)) - .bearer_auth(&state.api_token) - .send() - .await? - .json::>() - .await -} - -fn layout(title: &str, content: &str) -> String { - format!( - "{}
{}", - html_escape(title), - content - ) -} - -fn html_escape(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) -}