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::{
40ab5c7feat: /api/me and a shared identity read model1mo
18 application::{Identity, describe_identity, is_claimed, resolve_actor},
14223e7feat: claim, sign in, and sign out through the browser1mo
19 domain::{Actor, SessionTokenHash},
a69e380feat: public profile page at /{handle}1mo
20 infrastructure::repository::{
21 SqliteMembershipRepo, SqliteOrgRepo, SqliteSessionRepo, SqliteUserRepo,
22 },
14223e7feat: claim, sign in, and sign out through the browser1mo
23};
24
25/// The one-time token that authorises claiming an unclaimed instance.
26///
27/// Present in app context only while the installation is unclaimed.
28pub struct SetupState(pub crate::domain::SetupToken);
29
30pub fn pool(cx: &Cx) -> &SqlitePool {
31 app_context::<SqlitePool>(cx)
32}
33
34/// Anything below the web layer failing is a 500 — the visitor can't act on it, and
35/// the detail belongs in the log rather than the page.
a69e380feat: public profile page at /{handle}1mo
36pub fn server_error<E>(error: E) -> topcoat::Error
14223e7feat: claim, sign in, and sign out through the browser1mo
37where
38 E: std::error::Error + Send + Sync + 'static,
39{
40 eprintln!("steid: {error}");
41 internal_server_error(error).into()
42}
43
a69e380feat: public profile page at /{handle}1mo
44pub fn orgs(cx: &Cx) -> SqliteOrgRepo {
45 SqliteOrgRepo::new(pool(cx).clone())
46}
47
48pub fn memberships(cx: &Cx) -> SqliteMembershipRepo {
49 SqliteMembershipRepo::new(pool(cx).clone())
50}
51
14223e7feat: claim, sign in, and sign out through the browser1mo
52/// Who is making this request.
53///
54/// Resolves to [`Actor::Anonymous`] when there is no session, the session is unknown,
55/// or it has expired.
56pub async fn current_actor(cx: &Cx) -> Result<Actor> {
57 let presented = session::token_hash(cx)
58 .await?
59 .map(|hash| SessionTokenHash::from_trusted(hex(&hash)));
60
61 let sessions = SqliteSessionRepo::new(pool(cx).clone());
62
63 resolve_actor(presented.as_ref(), SystemTime::now(), &sessions)
64 .await
65 .map_err(server_error)
66}
67
40ab5c7feat: /api/me and a shared identity read model1mo
68/// Who the current actor actually is, resolved for display.
69///
70/// `None` when anonymous, or when the session outlived the user it names.
71pub async fn identity(cx: &Cx) -> Result<Option<Identity>> {
72 let actor = current_actor(cx).await?;
73 let pool = pool(cx).clone();
74
75 describe_identity(
76 &actor,
77 &SqliteUserRepo::new(pool.clone()),
78 &SqliteOrgRepo::new(pool),
79 )
80 .await
81 .map_err(server_error)
82}
83
14223e7feat: claim, sign in, and sign out through the browser1mo
84/// Whether this installation has an owner yet.
85pub async fn claimed(cx: &Cx) -> Result<bool> {
86 let users = SqliteUserRepo::new(pool(cx).clone());
87
88 is_claimed(&users).await.map_err(server_error)
89}
90
91/// The setup token, when the instance is still unclaimed.
92pub fn setup_token(cx: &Cx) -> Option<&crate::domain::SetupToken> {
93 topcoat::context::try_app_context::<SetupState>(cx).map(|state| &state.0)
94}
95
96/// Renders a `TokenHash` as lowercase hex for storage.
97pub fn hex(hash: &session::TokenHash) -> String {
98 hash.iter().map(|byte| format!("{byte:02x}")).collect()
99}