You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

122 lines
4.0 KiB

use std::{env, sync::Arc};
use axum::{
extract::Path,
http::StatusCode,
middleware::{from_fn, from_fn_with_state},
response::{IntoResponse, Redirect},
routing::{get, post},
Router,
};
use dotenv::dotenv;
use miette::IntoDiagnostic;
use sea_orm::Database;
use time::macros::format_description;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use tracing::info;
use tracing_subscriber::{fmt::time::UtcTime, EnvFilter, FmtSubscriber};
mod data;
mod middleware;
mod utils;
mod views;
use data::{AppOptions, AppState};
#[tokio::main]
async fn main() -> miette::Result<()> {
dotenv().ok();
let listen_addr = env::var("LISTEN_ADDR")
.unwrap_or("127.0.0.1:8080".to_string())
.parse()
.expect("LISTEN_ADDR should be valid");
let db_conn = env::var("DATABASE_URL").expect("DATABASE_URL should be set");
let sqlx_filter = match env::var("V_LOG_SQL") {
Ok(_) => "sqlx=info",
Err(_) => "sqlx=warn",
};
let domain = env::var("BASE_DOMAIN").ok();
let charset: Vec<char> = env::var("V_ID_CHARSET")
.unwrap_or("abcdefghijkmnpqrstuwxyz123467890".to_string())
.chars()
.collect();
let id_len = env::var("V_ID_GEN_LENGTH")
.map(|len| {
len.parse()
.expect("V_ID_GEN_LENGTH should parse into an integer")
})
.unwrap_or(6 as usize);
let test_data = env::var("V_DEV_TEST_DATA").is_ok();
FmtSubscriber::builder()
.with_env_filter(
EnvFilter::from_default_env().add_directive(sqlx_filter.parse().into_diagnostic()?),
)
.with_timer(UtcTime::new(format_description!(
"[year]:[month]:[day]T[hour]:[minute]:[second]"
)))
.init();
let db = Database::connect(db_conn.clone()).await.into_diagnostic()?;
utils::migrate_and_seed_db(&db).await.into_diagnostic()?;
if test_data {
utils::insert_dev_test_data(&db, charset.clone(), id_len).await.into_diagnostic()?;
info!("V_DEV_TEST_DATA is enabled. credentials: dev:a-password");
}
let state = Arc::new(AppState::new(
AppOptions {
domain,
charset,
id_len,
},
db,
None,
));
state.seed_cache().await.into_diagnostic()?;
let app = Router::new()
.route("/", get(|| async { "landing page with login form" }))
.route("/gay/login", post(views::auth::login)) // this one's excempt from the login requirement
.nest(
"/gay", // everything is under this namespace to avoid blocking possible link uris as much as possible
Router::new()
.layer(
ServiceBuilder::new()
.layer(from_fn_with_state(state.clone(), middleware::fetch_user))
.layer(from_fn(middleware::reject_unauthenticated)),
)
.route("/", get(|| async { "logged in dashboard" }))
.route("/logout", get(views::auth::logout))
.nest("/account", views::user::get_selfservice_routes())
.nest("/link", views::link::get_routes())
.nest(
"/admin",
views::admin::get_routes()
.layer(from_fn(middleware::admin_only))
.nest("/user", views::user::get_admin_routes()),
),
)
.route("/:source", get(views::link::access))
.layer(
ServiceBuilder::new() // all the middleware here is very cheap.
.layer(TraceLayer::new_for_http())
.layer(from_fn(middleware::errors_for_humans))
.layer(from_fn_with_state(
state.clone(),
middleware::try_get_user_session,
)),
)
.with_state(state.clone());
info!("listening on {}", &listen_addr);
axum::Server::bind(&listen_addr)
.serve(app.into_make_service())
.await
.into_diagnostic()?;
Ok(())
}