Magazi is a content distribution platform that gates access to files using ATProtocol (Bluesky) identity and cryptographic proofs.
download.ngerakines.me/
atprotocol
appview
atprotocol-attestations
1mod error;
2mod handlers;
3mod oauth_storage;
4mod resolvers;
5mod session;
6mod state;
7mod templates;
8
9use std::net::SocketAddr;
10use std::sync::Arc;
11
12use axum::{
13 Router,
14 http::{HeaderValue, header},
15 routing::{get, post},
16};
17use tower_http::{
18 limit::RequestBodyLimitLayer, set_header::SetResponseHeaderLayer, trace::TraceLayer,
19};
20use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
21
22use magazi::config::Config;
23use state::{AppState, InnerAppState};
24
25#[tokio::main]
26async fn main() -> anyhow::Result<()> {
27 tracing_subscriber::registry()
28 .with(
29 tracing_subscriber::EnvFilter::try_from_default_env()
30 .unwrap_or_else(|_| "magazi=warn".into()),
31 )
32 .with(tracing_subscriber::fmt::layer())
33 .init();
34
35 let config = Config::from_env()?;
36
37 // Verify all catalog files exist
38 for entry in &config.catalog {
39 let file_path = config.files_path.join(&entry.id);
40 if !file_path.exists() {
41 panic!(
42 "Catalog entry '{}' references missing file: {}",
43 entry.name,
44 file_path.display()
45 );
46 }
47 }
48
49 let http_port = config.http_port;
50 let inner_state = InnerAppState::new(config).await?;
51 let state = AppState(Arc::new(inner_state));
52
53 // Warm up the entitlement cache for anonymous visitors
54 handlers::util_entitlements::warmup_anonymous_cache(&state).await;
55
56 let app = Router::new()
57 .route("/", get(handlers::handler_home::home))
58 .route("/robots.txt", get(handlers::handler_robots::robots_txt))
59 .route(
60 "/oauth-client-metadata.json",
61 get(handlers::handler_oauth::client_metadata),
62 )
63 .route("/login", get(handlers::handler_auth::login_form))
64 .route("/login", post(handlers::handler_auth::login_start))
65 .route("/login/callback", get(handlers::handler_auth::login_callback))
66 .route("/download/{id}", get(handlers::handler_download::download_file))
67 .route(
68 "/xrpc/com.atproto.sync.getBlob",
69 get(handlers::handler_getblob::get_blob),
70 )
71 .route("/logout", get(handlers::handler_auth::logout))
72 // Security headers
73 .layer(SetResponseHeaderLayer::overriding(
74 header::X_CONTENT_TYPE_OPTIONS,
75 HeaderValue::from_static("nosniff"),
76 ))
77 .layer(SetResponseHeaderLayer::overriding(
78 header::X_FRAME_OPTIONS,
79 HeaderValue::from_static("DENY"),
80 ))
81 .layer(SetResponseHeaderLayer::overriding(
82 header::CONTENT_SECURITY_POLICY,
83 HeaderValue::from_static(
84 "default-src 'self'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src https://cdnjs.cloudflare.com",
85 ),
86 ))
87 .layer(SetResponseHeaderLayer::overriding(
88 header::STRICT_TRANSPORT_SECURITY,
89 HeaderValue::from_static("max-age=31536000; includeSubDomains"),
90 ))
91 // Request body size limit (1KB for form submissions)
92 .layer(RequestBodyLimitLayer::new(1024))
93 .layer(TraceLayer::new_for_http())
94 .with_state(state);
95
96 let addr = SocketAddr::from(([0, 0, 0, 0], http_port));
97 tracing::info!("listening on {}", addr);
98
99 let listener = tokio::net::TcpListener::bind(addr).await?;
100 axum::serve(listener, app).await?;
101
102 Ok(())
103}