steid

@jamesgill /

14223e7feat: claim, sign in, and sign out through the browser1mo
1//! Request-scoped helpers.
2//!
3//! Functions taking `cx: &Cx`, not middleware or extractors — Topcoat's guidance, and
4//! the safer shape: a page that forgets to call `current_actor` gets no actor, whereas
5//! a route added without its middleware silently gets someone else's.
6
7use std::time::SystemTime;
8
9use sqlx::SqlitePool;
10use topcoat::{
11 Result,
12 context::{Cx, app_context},
285f5fdfeat: create and view repositories through the browser24d
13 router::{HeaderName, HeaderValue, error::internal_server_error, header::LOCATION},
14223e7feat: claim, sign in, and sign out through the browser1mo
14 session,
15};
16
17use crate::{
285f5fdfeat: create and view repositories through the browser24d
18 application::{AppConfig, Identity, describe_identity, is_claimed, resolve_actor},
14223e7feat: claim, sign in, and sign out through the browser1mo
19 domain::{Actor, SessionTokenHash},
285f5fdfeat: create and view repositories through the browser24d
20 infrastructure::{
21 git::DiskGitStorage,
22 repository::{
23 SqliteMembershipRepo, SqliteOrgRepo, SqliteRepoRepo, SqliteSessionRepo, SqliteUserRepo,
24 },
a69e380feat: public profile page at /{handle}1mo
25 },
14223e7feat: claim, sign in, and sign out through the browser1mo
26};
27
28/// The one-time token that authorises claiming an unclaimed instance.
29///
30/// Present in app context only while the installation is unclaimed.
31pub struct SetupState(pub crate::domain::SetupToken);
32
33pub fn pool(cx: &Cx) -> &SqlitePool {
34 app_context::<SqlitePool>(cx)
35}
36
37/// Anything below the web layer failing is a 500 — the visitor can't act on it, and
38/// the detail belongs in the log rather than the page.
a69e380feat: public profile page at /{handle}1mo
39pub fn server_error<E>(error: E) -> topcoat::Error
14223e7feat: claim, sign in, and sign out through the browser1mo
40where
41 E: std::error::Error + Send + Sync + 'static,
42{
43 eprintln!("steid: {error}");
44 internal_server_error(error).into()
45}
46
a69e380feat: public profile page at /{handle}1mo
47pub fn orgs(cx: &Cx) -> SqliteOrgRepo {
48 SqliteOrgRepo::new(pool(cx).clone())
49}
50
51pub fn memberships(cx: &Cx) -> SqliteMembershipRepo {
52 SqliteMembershipRepo::new(pool(cx).clone())
53}
54
285f5fdfeat: create and view repositories through the browser24d
55pub fn repos(cx: &Cx) -> SqliteRepoRepo {
56 SqliteRepoRepo::new(pool(cx).clone())
57}
58
59/// Bare repositories on disk, rooted at the configured data directory.
60pub fn storage(cx: &Cx) -> DiskGitStorage {
61 DiskGitStorage::new(app_context::<AppConfig>(cx).data_dir.clone())
62}
63
14223e7feat: claim, sign in, and sign out through the browser1mo
64/// Who is making this request.
65///
66/// Resolves to [`Actor::Anonymous`] when there is no session, the session is unknown,
67/// or it has expired.
68pub async fn current_actor(cx: &Cx) -> Result<Actor> {
69 let presented = session::token_hash(cx)
70 .await?
71 .map(|hash| SessionTokenHash::from_trusted(hex(&hash)));
72
73 let sessions = SqliteSessionRepo::new(pool(cx).clone());
74
75 resolve_actor(presented.as_ref(), SystemTime::now(), &sessions)
76 .await
77 .map_err(server_error)
78}
79
40ab5c7feat: /api/me and a shared identity read model1mo
80/// Who the current actor actually is, resolved for display.
81///
82/// `None` when anonymous, or when the session outlived the user it names.
83pub async fn identity(cx: &Cx) -> Result<Option<Identity>> {
84 let actor = current_actor(cx).await?;
85 let pool = pool(cx).clone();
86
87 describe_identity(
88 &actor,
89 &SqliteUserRepo::new(pool.clone()),
90 &SqliteOrgRepo::new(pool),
91 )
92 .await
93 .map_err(server_error)
94}
95
14223e7feat: claim, sign in, and sign out through the browser1mo
96/// Whether this installation has an owner yet.
97pub async fn claimed(cx: &Cx) -> Result<bool> {
98 let users = SqliteUserRepo::new(pool(cx).clone());
99
100 is_claimed(&users).await.map_err(server_error)
101}
102
103/// The setup token, when the instance is still unclaimed.
104pub fn setup_token(cx: &Cx) -> Option<&crate::domain::SetupToken> {
105 topcoat::context::try_app_context::<SetupState>(cx).map(|state| &state.0)
106}
107
285f5fdfeat: create and view repositories through the browser24d
108/// The `Location` header for a post-redirect-get reply.
109///
110/// Pair it with `StatusCode::SEE_OTHER` inside a `view!` to redirect from a page that
111/// otherwise renders a view:
112///
113/// ```ignore
114/// view! { (StatusCode::SEE_OTHER) (location("/somewhere")?) }
115/// ```
116///
117/// The awkward-looking route to a 303: `redirect()` is a **307**, which preserves the
118/// method, so a browser re-POSTs the form to its target instead of fetching it.
119/// `see_other()` is the right status but is a response type, and `#[page]` handlers
120/// must return a view so the layout can wrap the failure re-render. Setting the status
121/// and header inside `view!` is the documented way to get both from one handler.
122pub fn location(uri: &str) -> Result<(HeaderName, HeaderValue)> {
123 Ok((LOCATION, HeaderValue::try_from(uri).map_err(server_error)?))
124}
125
14223e7feat: claim, sign in, and sign out through the browser1mo
126/// Renders a `TokenHash` as lowercase hex for storage.
127pub fn hex(hash: &session::TokenHash) -> String {
128 hash.iter().map(|byte| format!("{byte:02x}")).collect()
129}