steid

@jamesgill /

40ab5c7feat: /api/me and a shared identity read model1mo
1//! JSON API.
2//!
3//! Not a milestone of its own — every use case gets a second surface here where that
4//! makes sense. Having two consumers is what keeps the application layer honest about
5//! staying transport-neutral.
6
7use serde::Serialize;
8use topcoat::{
9 Result,
10 context::Cx,
11 router::{content::Json, error::unauthorized, route},
12};
13
14use super::context::identity;
15
16/// The authenticated user, as JSON.
17///
18/// A DTO rather than the domain type: serialisation is a wire concern, and deriving
19/// `Serialize` on domain entities lets a field leak into a response the moment someone
20/// adds one.
21#[derive(Debug, Serialize)]
22struct Me {
23 id: String,
24 email: String,
25 handle: String,
26}
27
28/// Who the caller is.
29///
30/// 401 when anonymous, matching the convention that `/me` describes an authenticated
31/// caller and has nothing to say without one.
32#[route(GET "/api/me")]
33async fn me(cx: &Cx) -> Result<Json<Me>> {
34 let identity = identity(cx).await?.ok_or_else(unauthorized)?;
35
36 Ok(Json(Me {
37 id: identity.user_id.to_string(),
38 email: identity.email.to_string(),
39 handle: identity.handle.to_string(),
40 }))
41}
a69e380feat: public profile page at /{handle}1mo
42
43/// A profile, as JSON, for anyone.
44///
45/// Mirrors `PublicProfile` exactly. There is no email field here and there must never
46/// be one — this is served to anonymous callers.
47#[derive(Debug, Serialize)]
48struct Profile {
49 handle: String,
50 label: String,
51 bio: Option<String>,
52 viewer_is_owner: bool,
53}
54
55#[route(GET "/api/users/{handle}")]
56async fn user(cx: &Cx) -> Result<Json<Profile>> {
57 let profile = super::profile::profile_for(cx).await?;
58
59 Ok(Json(Profile {
60 handle: profile.handle.to_string(),
61 label: profile.label,
62 bio: profile.bio,
63 viewer_is_owner: profile.viewer_is_owner,
64 }))
65}