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},
13 router::error::internal_server_error,
14 session,
15};
16
17use crate::{
18 application::{is_claimed, resolve_actor},
19 domain::{Actor, SessionTokenHash},
20 infrastructure::repository::{SqliteSessionRepo, SqliteUserRepo},
21};
22
23/// The one-time token that authorises claiming an unclaimed instance.
24///
25/// Present in app context only while the installation is unclaimed.
26pub struct SetupState(pub crate::domain::SetupToken);
27
28pub fn pool(cx: &Cx) -> &SqlitePool {
29 app_context::<SqlitePool>(cx)
30}
31
32/// Anything below the web layer failing is a 500 — the visitor can't act on it, and
33/// the detail belongs in the log rather than the page.
34fn server_error<E>(error: E) -> topcoat::Error
35where
36 E: std::error::Error + Send + Sync + 'static,
37{
38 eprintln!("steid: {error}");
39 internal_server_error(error).into()
40}
41
42/// Who is making this request.
43///
44/// Resolves to [`Actor::Anonymous`] when there is no session, the session is unknown,
45/// or it has expired.
46pub async fn current_actor(cx: &Cx) -> Result<Actor> {
47 let presented = session::token_hash(cx)
48 .await?
49 .map(|hash| SessionTokenHash::from_trusted(hex(&hash)));
50
51 let sessions = SqliteSessionRepo::new(pool(cx).clone());
52
53 resolve_actor(presented.as_ref(), SystemTime::now(), &sessions)
54 .await
55 .map_err(server_error)
56}
57
58/// Whether this installation has an owner yet.
59pub async fn claimed(cx: &Cx) -> Result<bool> {
60 let users = SqliteUserRepo::new(pool(cx).clone());
61
62 is_claimed(&users).await.map_err(server_error)
63}
64
65/// The setup token, when the instance is still unclaimed.
66pub fn setup_token(cx: &Cx) -> Option<&crate::domain::SetupToken> {
67 topcoat::context::try_app_context::<SetupState>(cx).map(|state| &state.0)
68}
69
70/// Renders a `TokenHash` as lowercase hex for storage.
71pub fn hex(hash: &session::TokenHash) -> String {
72 hash.iter().map(|byte| format!("{byte:02x}")).collect()
73}