Skip to main content

max / makenotwork

5.2 KB · 133 lines History Blame Raw
1 //! Public-facing page routes visible to all visitors.
2
3 pub(crate) mod content;
4 mod discover;
5 mod docs;
6 mod feed;
7 mod health;
8 pub(crate) mod join_wizard;
9 pub(crate) mod landing;
10 mod sitemap;
11 mod two_factor;
12
13 use axum::{
14 extract::State,
15 response::IntoResponse,
16 routing::get,
17 };
18 use tower_sessions::Session;
19
20 use crate::{
21 auth::MaybeUserUnverified,
22 constants,
23 csrf::{post_csrf_skip, with_csrf_skip, CsrfRouter},
24 db,
25 error::Result,
26 helpers::get_csrf_token,
27 templates::*,
28 types::*,
29 AppState,
30 };
31
32 use tower_governor::GovernorLayer;
33
34 /// Register public page routes.
35 pub fn public_routes() -> CsrfRouter<AppState> {
36 let twofa_rate_limit = crate::helpers::rate_limiter_ms(constants::TWO_FACTOR_RATE_LIMIT_MS, constants::TWO_FACTOR_RATE_LIMIT_BURST);
37 let join_rate_limit = crate::helpers::rate_limiter_ms(constants::AUTH_RATE_LIMIT_MS, constants::AUTH_RATE_LIMIT_BURST);
38
39 CsrfRouter::new()
40 .route_get("/", get(landing::index))
41 .route_get("/library", get(landing::library))
42 .route_get("/cart", get(landing::cart_page))
43 .route_get("/library/tabs/purchases", get(landing::library_tab_purchases))
44 .route_get("/library/tabs/feed", get(landing::library_tab_feed))
45 .route_get("/library/tabs/collections", get(landing::library_tab_collections))
46 .route_get("/library/tabs/contacts", get(landing::library_tab_contacts))
47 .route_get("/library/tabs/communities", get(landing::library_tab_communities))
48 .route_get("/health", get(health::health))
49 .route_get("/api/health", get(health::health_json))
50 .route_get("/robots.txt", get(sitemap::robots_txt))
51 .route_get("/sitemap.xml", get(sitemap::sitemap_xml))
52 // NOTE: GET /login is registered in auth_routes() alongside POST /login
53 // to avoid Axum merge conflicts that strip rate limiting layers.
54 // Join wizard
55 .route_get("/join", get(join_wizard::wizard_page))
56 .route(
57 "/join/step/account",
58 post_csrf_skip(
59 "join-wizard step 1: pre-auth signup",
60 join_wizard::step_account_create,
61 )
62 .layer(GovernorLayer { config: join_rate_limit }),
63 )
64 .route(
65 "/join/step/{step}",
66 with_csrf_skip(
67 "join-wizard: continuation of pre-auth flow",
68 get(join_wizard::step_load).post(join_wizard::step_save),
69 ),
70 )
71 .route_get("/discover", get(discover::discover))
72 .route_get("/discover/results", get(discover::discover_results))
73 .route_get("/discover/suggestions", get(discover::search_suggestions_handler))
74 .route_get("/discover/tags", get(discover::tag_tree))
75 .route_get("/feed", get(feed::feed_page))
76 .route_get("/u/{username}", get(content::user_page))
77 .route_get("/c/{username}/{slug}", get(content::collection_page))
78 .route_get("/p/{slug}", get(content::project_page))
79 .route_get("/i/{item_id}", get(content::item_page))
80 .route_get("/l/{item_id}", get(content::library_page))
81 .route_get("/purchase/{item_id}", get(content::purchase_page))
82 .route_get("/receipt/{transaction_id}", get(content::receipt_page))
83 .route_get("/buy/{item_id}", get(content::buy_page))
84 .route_get("/pricing", get(landing::pricing_page))
85 .route_get("/checkout/complete", get(landing::checkout_complete))
86 .route_get("/use-cases", get(landing::use_cases_page))
87 .route_get("/team", get(landing::team_page))
88 .route_get("/policy", get(landing::policy_page))
89 .route_get("/fan-plus", get(landing::fan_plus_page))
90 .route_get("/creators", get(creators_page))
91 .route_get("/docs", get(docs::docs_index))
92 .route_get("/docs/search.json", get(docs::docs_search_index))
93 .route_get("/docs/{slug}", get(docs::doc_page))
94 // Two-factor authentication
95 .route_get("/auth/2fa", get(two_factor::two_factor_page))
96 .route(
97 "/auth/verify-2fa",
98 post_csrf_skip(
99 "2FA verification: pre-promotion to full auth, no session yet",
100 two_factor::verify_two_factor,
101 )
102 .layer(GovernorLayer { config: twofa_rate_limit }),
103 )
104 }
105
106 /// Render the public creators page showing invite waves and waitlist stats.
107 #[tracing::instrument(skip_all, name = "pages::creators_page")]
108 async fn creators_page(
109 State(state): State<AppState>,
110 session: Session,
111 MaybeUserUnverified(maybe_user): MaybeUserUnverified,
112 ) -> Result<impl IntoResponse> {
113 let csrf_token = get_csrf_token(&session).await;
114
115 let waves = db::waitlist::get_all_waves(&state.db).await?;
116 let total_creators = db::waitlist::count_active_creators(&state.db).await? as u32;
117 let waitlist_pending = db::waitlist::count_waitlist_pending(&state.db).await? as u32;
118
119 let is_creator = maybe_user.as_ref().map(|u| u.can_create_projects).unwrap_or(false);
120
121 let wave_stats: Vec<WaveStats> = waves.iter().map(WaveStats::from).collect();
122
123 Ok(CreatorsTemplate {
124 csrf_token,
125 session_user: maybe_user,
126 waves: wave_stats,
127 total_creators,
128 waitlist_pending,
129 is_creator,
130 tier_prices: state.tier_prices.clone(),
131 })
132 }
133