crates/cli/ents-web/src/lib.rs
| 1 | //! `ents-web`: the web UI (`docs/development-plan.adoc`, phase 7) -- |
| 2 | //! a second leaf, sibling to `git-ents`, in the layering |
| 3 | //! `docs/abstractions.adoc` states (`substrate -> kernel -> {forge, kiln} |
| 4 | //! -> {git-ents, ents-web}`). |
| 5 | //! |
| 6 | //! This crate's one responsibility is rendering the kernel's and every |
| 7 | //! installed package's own state as HTML, and accepting signed, |
| 8 | //! CSRF-checked mutations back -- never a second copy of forge or kiln |
| 9 | //! business logic. Every page is a thin caller into `ents-model`, |
| 10 | //! `ents-anchor`, `ents-query`, `ents-receive`, `ents-forge`, or |
| 11 | //! `ents-kiln`, exactly as `git-ents`'s own `commands` modules are thin |
| 12 | //! callers into the same crates ([`crate::pages`]'s own module doc draws |
| 13 | //! the line between the generic, reflection-driven pages and the |
| 14 | //! legitimate custom ones). |
| 15 | //! |
| 16 | //! # Deployment-agnostic by construction (`roots.web-agnostic`) |
| 17 | //! |
| 18 | //! Nothing in this crate binds a socket except [`serve_on`], and nothing |
| 19 | //! upstream of it assumes one exists: `router()` alone builds a complete, |
| 20 | //! in-process `tower::Service` a caller can drive via |
| 21 | //! `tower::ServiceExt::oneshot` with no network transport at all -- the |
| 22 | //! same shape an in-process webview embedding would drive a request |
| 23 | //! through. See [`identity::SigningIdentity`]'s own doc for the other half |
| 24 | //! of this requirement: the signing identity a mutation is signed with is |
| 25 | //! always injected by the composition root, never resolved by this crate |
| 26 | //! itself. |
| 27 | //! |
| 28 | //! # What this crate does not expose (`roots.local`) |
| 29 | //! |
| 30 | //! There is no `/info/refs`, no `git-upload-pack`/`git-receive-pack` |
| 31 | //! route, and no code path that shells to `git` as a smart-HTTP backend |
| 32 | //! anywhere in `router()`'s route table. `git ents serve`'s own doc |
| 33 | //! (`git-ents`'s `commands::serve`) states why: the local root's existing |
| 34 | //! wiring already serves git's own transport for the test-harness case |
| 35 | //! (`roots.worktree-update`); this crate adds only the web UI on top of |
| 36 | //! it, on loopback, never a second git-serving surface. |
| 37 | //! |
| 38 | //! # Spec coverage |
| 39 | //! |
| 40 | //! From `docs/spec/roots.adoc`: |
| 41 | //! |
| 42 | //! - `roots.local` -- this crate's route table carries no git |
| 43 | //! smart-HTTP surface; `git-ents`'s own `serve` command reuses |
| 44 | //! `LocalRoot`'s existing seams and binds loopback only (see that |
| 45 | //! crate's `commands::serve` module). |
| 46 | //! - `roots.web-signing`, `roots.web-agnostic` -- [`identity::SigningIdentity`]. |
| 47 | //! - `roots.web-session` -- [`session::SessionStore`], and |
| 48 | //! `pages::require_csrf` on every state-changing route. |
| 49 | //! |
| 50 | //! `roots.path-validation` and `roots.fetch-auth` are out of scope for |
| 51 | //! this crate: both describe `git-ents-server`'s multi-repository hosted |
| 52 | //! root (phase 8) -- "reject a path that would escape the data |
| 53 | //! directory, nest inside an existing repository, or collide with a |
| 54 | //! non-repository namespace directory" and "private-repository access... |
| 55 | //! out of scope for v1" both presuppose a data directory holding more |
| 56 | //! than one repository, which does not exist until that phase. This |
| 57 | //! crate's composition root always already has exactly one, already-open |
| 58 | //! repository. |
| 59 | //! |
| 60 | //! # Examples |
| 61 | //! |
| 62 | //! Driving a full request through this crate with no socket bound at all |
| 63 | //! (`roots.web-agnostic`'s in-process case) -- see `tests/router.rs` for |
| 64 | //! the full-fixture version of this same shape, wired against a real |
| 65 | //! signed member. |
| 66 | //! |
| 67 | //! ``` |
| 68 | //! use std::sync::Arc; |
| 69 | //! |
| 70 | //! use ents_web::identity::SigningIdentity; |
| 71 | //! use ents_web::state::AppState; |
| 72 | //! use ents_receive::{Mode, NullEventSink}; |
| 73 | //! use ents_testutil::ObjectStore; |
| 74 | //! use gix_ref_store::LooseRefStore; |
| 75 | //! use http_body_util::BodyExt as _; |
| 76 | //! use tower::ServiceExt as _; |
| 77 | //! |
| 78 | //! struct Fixture; |
| 79 | //! impl SigningIdentity for Fixture { |
| 80 | //! fn actor(&self) -> gix::actor::Signature { |
| 81 | //! gix::actor::Signature { |
| 82 | //! name: "fixture".into(), email: "fixture@ents.test".into(), |
| 83 | //! time: gix::date::Time { seconds: 0, offset: 0 }, |
| 84 | //! } |
| 85 | //! } |
| 86 | //! fn sign(&self, _payload: &[u8]) -> String { String::new() } |
| 87 | //! fn public_openssh(&self) -> String { "ssh-ed25519 AAAA... fixture".to_owned() } |
| 88 | //! } |
| 89 | //! |
| 90 | //! # let runtime = tokio::runtime::Runtime::new().expect("runtime"); |
| 91 | //! # runtime.block_on(async { |
| 92 | //! let dir = tempfile::tempdir().expect("tempdir"); |
| 93 | //! gix::init(dir.path()).expect("init"); |
| 94 | //! let refs = LooseRefStore::open(dir.path()).expect("opens"); |
| 95 | //! let objects = ObjectStore::default(); |
| 96 | //! let state = Arc::new(AppState::new( |
| 97 | //! Box::new(refs), objects, Box::new(NullEventSink), Mode::Advisory, |
| 98 | //! Box::new(Fixture), dir.path().to_owned(), |
| 99 | //! )); |
| 100 | //! let router = ents_web::router(state); |
| 101 | //! let response = router |
| 102 | //! .oneshot(axum::http::Request::get("/").body(axum::body::Body::empty()).expect("request")) |
| 103 | //! .await |
| 104 | //! .expect("in-process call"); |
| 105 | //! assert_eq!(response.status(), axum::http::StatusCode::OK); |
| 106 | //! # }); |
| 107 | //! ``` |
| 108 | |
| 109 | pub(crate) mod asciidoc; |
| 110 | pub(crate) mod assets; |
| 111 | pub mod auth; |
| 112 | pub(crate) mod editor; |
| 113 | pub mod error; |
| 114 | pub mod form; |
| 115 | pub mod identity; |
| 116 | pub(crate) mod markdown; |
| 117 | pub mod pages; |
| 118 | pub mod render; |
| 119 | pub mod router; |
| 120 | pub mod session; |
| 121 | pub mod state; |
| 122 | |
| 123 | pub use error::{Error, Result}; |
| 124 | pub use router::{bind, router, serve_on}; |