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},
acde6b5feat: show the clone URL on a repository page8d
13 router::{
14 HeaderName, HeaderValue,
15 error::internal_server_error,
16 header::{HOST, LOCATION},
17 headers,
18 },
14223e7feat: claim, sign in, and sign out through the browser1mo
19 session,
20};
21
22use crate::{
285f5fdfeat: create and view repositories through the browser24d
23 application::{AppConfig, Identity, describe_identity, is_claimed, resolve_actor},
14223e7feat: claim, sign in, and sign out through the browser1mo
24 domain::{Actor, SessionTokenHash},
285f5fdfeat: create and view repositories through the browser24d
25 infrastructure::{
b7a57dbfeat: a repository can be taken away as a file17h
26 git::{DiskGitArchive, DiskGitStorage, GitHttpBackend},
dce0bf3feat: browse a repository's files and history8d
27 git_query::DiskGitQuery,
285f5fdfeat: create and view repositories through the browser24d
28 repository::{
a814db5feat: push and clone private repositories with a token8d
29 SqliteMembershipRepo, SqliteOrgRepo, SqliteRepoRepo, SqliteSessionRepo,
30 SqliteTokenRepo, SqliteUserRepo,
285f5fdfeat: create and view repositories through the browser24d
31 },
a69e380feat: public profile page at /{handle}1mo
32 },
14223e7feat: claim, sign in, and sign out through the browser1mo
33};
34
35/// The one-time token that authorises claiming an unclaimed instance.
36///
37/// Present in app context only while the installation is unclaimed.
38pub struct SetupState(pub crate::domain::SetupToken);
39
40pub fn pool(cx: &Cx) -> &SqlitePool {
41 app_context::<SqlitePool>(cx)
42}
43
44/// Anything below the web layer failing is a 500 — the visitor can't act on it, and
45/// the detail belongs in the log rather than the page.
a69e380feat: public profile page at /{handle}1mo
46pub fn server_error<E>(error: E) -> topcoat::Error
14223e7feat: claim, sign in, and sign out through the browser1mo
47where
48 E: std::error::Error + Send + Sync + 'static,
49{
50 eprintln!("steid: {error}");
51 internal_server_error(error).into()
52}
53
a69e380feat: public profile page at /{handle}1mo
54pub fn orgs(cx: &Cx) -> SqliteOrgRepo {
55 SqliteOrgRepo::new(pool(cx).clone())
56}
57
58pub fn memberships(cx: &Cx) -> SqliteMembershipRepo {
59 SqliteMembershipRepo::new(pool(cx).clone())
60}
61
285f5fdfeat: create and view repositories through the browser24d
62pub fn repos(cx: &Cx) -> SqliteRepoRepo {
63 SqliteRepoRepo::new(pool(cx).clone())
64}
65
66/// Bare repositories on disk, rooted at the configured data directory.
67pub fn storage(cx: &Cx) -> DiskGitStorage {
68 DiskGitStorage::new(app_context::<AppConfig>(cx).data_dir.clone())
69}
70
8ed4e5afeat: the root is the owner's profile1d
71pub fn users(cx: &Cx) -> SqliteUserRepo {
72 SqliteUserRepo::new(pool(cx).clone())
73}
74
a814db5feat: push and clone private repositories with a token8d
75pub fn tokens(cx: &Cx) -> SqliteTokenRepo {
76 SqliteTokenRepo::new(pool(cx).clone())
77}
78
dce0bf3feat: browse a repository's files and history8d
79/// Repository contents, read from the same data directory as [`storage`].
80pub fn queries(cx: &Cx) -> DiskGitQuery {
81 DiskGitQuery::new(app_context::<AppConfig>(cx).data_dir.clone())
82}
83
b7a57dbfeat: a repository can be taken away as a file17h
84/// `git archive`, rooted at the same data directory as [`storage`].
85pub fn archives(cx: &Cx) -> DiskGitArchive {
86 DiskGitArchive::new(app_context::<AppConfig>(cx).data_dir.clone())
87}
88
e0856ebfeat: clone a public repository over HTTP8d
89/// The git smart-HTTP protocol, rooted at the same data directory as [`storage`].
90pub fn protocol(cx: &Cx) -> GitHttpBackend {
91 GitHttpBackend::new(app_context::<AppConfig>(cx).data_dir.clone())
92}
93
14223e7feat: claim, sign in, and sign out through the browser1mo
94/// Who is making this request.
95///
96/// Resolves to [`Actor::Anonymous`] when there is no session, the session is unknown,
97/// or it has expired.
98pub async fn current_actor(cx: &Cx) -> Result<Actor> {
99 let presented = session::token_hash(cx)
100 .await?
101 .map(|hash| SessionTokenHash::from_trusted(hex(&hash)));
102
103 let sessions = SqliteSessionRepo::new(pool(cx).clone());
104
105 resolve_actor(presented.as_ref(), SystemTime::now(), &sessions)
106 .await
107 .map_err(server_error)
108}
109
40ab5c7feat: /api/me and a shared identity read model1mo
110/// Who the current actor actually is, resolved for display.
111///
112/// `None` when anonymous, or when the session outlived the user it names.
113pub async fn identity(cx: &Cx) -> Result<Option<Identity>> {
114 let actor = current_actor(cx).await?;
115 let pool = pool(cx).clone();
116
117 describe_identity(
118 &actor,
119 &SqliteUserRepo::new(pool.clone()),
120 &SqliteOrgRepo::new(pool),
121 )
122 .await
123 .map_err(server_error)
124}
125
14223e7feat: claim, sign in, and sign out through the browser1mo
126/// Whether this installation has an owner yet.
127pub async fn claimed(cx: &Cx) -> Result<bool> {
128 let users = SqliteUserRepo::new(pool(cx).clone());
129
130 is_claimed(&users).await.map_err(server_error)
131}
132
133/// The setup token, when the instance is still unclaimed.
134pub fn setup_token(cx: &Cx) -> Option<&crate::domain::SetupToken> {
135 topcoat::context::try_app_context::<SetupState>(cx).map(|state| &state.0)
136}
137
285f5fdfeat: create and view repositories through the browser24d
138/// The `Location` header for a post-redirect-get reply.
139///
140/// Pair it with `StatusCode::SEE_OTHER` inside a `view!` to redirect from a page that
141/// otherwise renders a view:
142///
143/// ```ignore
144/// view! { (StatusCode::SEE_OTHER) (location("/somewhere")?) }
145/// ```
146///
147/// The awkward-looking route to a 303: `redirect()` is a **307**, which preserves the
148/// method, so a browser re-POSTs the form to its target instead of fetching it.
149/// `see_other()` is the right status but is a response type, and `#[page]` handlers
150/// must return a view so the layout can wrap the failure re-render. Setting the status
151/// and header inside `view!` is the documented way to get both from one handler.
152pub fn location(uri: &str) -> Result<(HeaderName, HeaderValue)> {
153 Ok((LOCATION, HeaderValue::try_from(uri).map_err(server_error)?))
154}
155
acde6b5feat: show the clone URL on a repository page8d
156/// The origin this instance is being reached on, e.g. `https://steid.example`.
157///
158/// Derived from the request rather than configured, so an instance is deployable
159/// anywhere without being told its own address — behind a proxy, on a platform
160/// subdomain, or on localhost, all with no configuration.
161///
162/// The scheme comes from `X-Forwarded-Proto` when a proxy sets it, since TLS is
163/// terminated in front of us and the request that arrives here is plain HTTP. Falling
164/// back to `http` is right for local development and wrong nowhere that matters: a
165/// deployment without a terminating proxy has no TLS to advertise anyway.
166pub fn public_origin(cx: &Cx) -> String {
167 let headers = headers(cx);
168 let value = |name: &str| headers.get(name).and_then(|value| value.to_str().ok());
169
170 let scheme = value("x-forwarded-proto").unwrap_or("http");
171 let host = headers
172 .get(HOST)
173 .and_then(|value| value.to_str().ok())
174 .unwrap_or("localhost");
175
176 format!("{scheme}://{host}")
177}
178
14223e7feat: claim, sign in, and sign out through the browser1mo
179/// Renders a `TokenHash` as lowercase hex for storage.
180pub fn hex(hash: &session::TokenHash) -> String {
181 hash.iter().map(|byte| format!("{byte:02x}")).collect()
182}