diff --git a/src/data.rs b/src/data.rs index 85e695e..ef8d44c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,12 +1,13 @@ use std::{fmt::Display, sync::Arc, time::Duration}; -use cookie::{Cookie, Key}; +use cookie::{Cookie, CookieBuilder, Key, SameSite}; use dashmap::DashMap; use sea_orm::{error::DbErr, DatabaseConnection, EntityTrait, QuerySelect}; use serde::{Deserialize, Serialize}; use tokio::time::Instant; use entity::{link, user}; +use tracing::warn; use uuid::Uuid; use crate::utils; @@ -61,16 +62,6 @@ impl AppState { }); Ok(()) } - - pub async fn is_user_admin(&self, id: i32) -> Result { - Ok(user::Entity::find_by_id(id) - .select_only() - .column(user::Column::IsAdmin) - .one(&self.db) - .await? - .map(|u| u.is_admin) - .unwrap_or(false)) - } } #[derive(Clone)] @@ -137,9 +128,32 @@ impl UserSession { self.created.elapsed() > self.expiry } - pub fn get_decrypted_cookie(&self) -> String { + pub fn get_cookie_value(&self) -> String { self.decrypted_session_cookie.to_string() } + + pub fn into_cookie(self, opts: AppOptions) -> Cookie<'static> { + if opts.use_secure_cookie() { + if opts.domain.is_none() { + warn!("configured to use secure cookie, but no domain is set! falling back to insecure cookie"); + return self.into_insecure_cookie().finish(); + } + let domain = opts.domain.expect("math broke"); + self.into_insecure_cookie() + .secure(true) + .domain(domain) + .same_site(SameSite::Strict) + .finish() + } else { + self.into_insecure_cookie().finish() + } + } + + fn into_insecure_cookie(self) -> CookieBuilder<'static> { + Cookie::build("_session", self.get_cookie_value()) + .http_only(true) + .max_age(cookie::time::Duration::days(7)) + } } impl Display for UserSession { diff --git a/src/main.rs b/src/main.rs index 5c09de9..21e37d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,7 @@ 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, }; @@ -13,7 +10,7 @@ use miette::IntoDiagnostic; use sea_orm::Database; use time::macros::format_description; use tower::ServiceBuilder; -use tower_http::trace::TraceLayer; +use tower_http::{services::ServeDir, trace::TraceLayer}; use tracing::info; use tracing_subscriber::{fmt::time::UtcTime, EnvFilter, FmtSubscriber}; @@ -38,6 +35,7 @@ async fn main() -> miette::Result<()> { Err(_) => "sqlx=warn", }; let domain = env::var("BASE_DOMAIN").ok(); + let static_file_path = env::var("STATIC_FILE_PATH").unwrap_or("static".to_string()); let charset: Vec = env::var("V_ID_CHARSET") .unwrap_or("abcdefghijkmnpqrstuwxyz123467890".to_string()) .chars() @@ -62,7 +60,9 @@ async fn main() -> miette::Result<()> { 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()?; + utils::insert_dev_test_data(&db, charset.clone(), id_len) + .await + .into_diagnostic()?; info!("V_DEV_TEST_DATA is enabled. credentials: dev:a-password"); } @@ -79,7 +79,9 @@ async fn main() -> miette::Result<()> { let app = Router::new() .route("/", get(|| async { "landing page with login form" })) + .route("/404", get(|| async { "404 not found " })) .route("/gay/login", post(views::auth::login)) // this one's excempt from the login requirement + .nest_service("/gay/static", ServeDir::new(static_file_path)) .nest( "/gay", // everything is under this namespace to avoid blocking possible link uris as much as possible Router::new() diff --git a/src/utils.rs b/src/utils.rs index 70b2d11..58bef17 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,8 +12,8 @@ use entity::prelude::*; use entity::user; use migration::Migrator; use migration::MigratorTrait; -use sea_orm::{DatabaseConnection, DbErr}; use sea_orm::entity::prelude::*; +use sea_orm::{DatabaseConnection, DbErr}; use serde_json::json; use tera::{Context, Tera}; @@ -39,32 +39,49 @@ pub async fn migrate_and_seed_db(db: &DatabaseConnection) -> Result<(), DbErr> { } } -pub async fn insert_dev_test_data(db: &DatabaseConnection, charset: Vec, len: usize) -> Result<(), DbErr> { - if User::find().filter(user::Column::Name.eq("dev")).one(db).await?.is_none() { - +pub async fn insert_dev_test_data( + db: &DatabaseConnection, + charset: Vec, + len: usize, +) -> Result<(), DbErr> { + if User::find() + .filter(user::Column::Name.eq("dev")) + .one(db) + .await? + .is_none() + { let dev_password = hash_password("a-password".as_bytes()); let dev_uid = User::insert(user::ActiveModel::from_json(json!({ "name": "dev", "is_admin": false, "password": dev_password, - }))?).exec(db).await?.last_insert_id; + }))?) + .exec(db) + .await? + .last_insert_id; info!("created dev user"); let dev_link1 = Link::insert(link::ActiveModel::from_json(json!({ "source": "example", "target": "http://example.com", "user_id": dev_uid, - }))?).exec(db).await?.last_insert_id; - + }))?) + .exec(db) + .await? + .last_insert_id; + let dev_link2 = Link::insert(link::ActiveModel::from_json(json!({ "source": gen_id(len, charset), "target": "https://random.org", "user_id": dev_uid, - }))?).exec(db).await?.last_insert_id; - + }))?) + .exec(db) + .await? + .last_insert_id; + info!("created two links for dev user"); } - + Ok(()) } diff --git a/src/views/auth.rs b/src/views/auth.rs index d8ea1a5..889b4f5 100644 --- a/src/views/auth.rs +++ b/src/views/auth.rs @@ -4,7 +4,6 @@ use axum::{ response::{IntoResponse, Redirect, Response}, Extension, Form, }; -use cookie::Cookie; use entity::user; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use std::sync::Arc; @@ -23,17 +22,17 @@ pub async fn login( .map_err(|_| Redirect::to("/?redirect_reason=login_failed"))? { let session = UserSession::new(u.id, u.is_admin); - let cookie_dec = Cookie::new("_session", session.get_decrypted_cookie()); + let cookie_dec = session.clone().into_cookie(state.opts.clone()); let cookie_enc = state.encrypt_cookie(cookie_dec); state .sessions - .insert(session.get_decrypted_cookie(), session) + .insert(session.get_cookie_value(), session) .map(|old_session| { warn!( "sound the alarms! same session cookie for user id={} generated twice: '{}'", - old_session.get_decrypted_cookie(), - session.get_decrypted_cookie() + old_session.get_cookie_value(), + session.get_cookie_value() ) }); diff --git a/src/views/link.rs b/src/views/link.rs index 432b0a2..b931442 100644 --- a/src/views/link.rs +++ b/src/views/link.rs @@ -18,12 +18,12 @@ use std::{collections::HashMap, sync::Arc}; /// crud routes for pub fn get_routes() -> Router> { - Router::new() - .route("/", get(list).post(create)) - .route( - "/:name", - get(|| async { "filled out form with link data" }).patch(update).delete(delete), - ) + Router::new().route("/", get(list).post(create)).route( + "/:name", + get(|| async { "filled out form with link data" }) + .patch(update) + .delete(delete), + ) // admins can change/delete all, users their own } @@ -31,15 +31,19 @@ pub async fn detail( State(state): State>, Extension(user): Extension, Path(source): Path, - ) -> String { - let link = match link::Entity::find().filter(link::Column::Source.eq(source)).one(&state.db).await { +) -> String { + let link = match link::Entity::find() + .filter(link::Column::Source.eq(source)) + .one(&state.db) + .await + { Err(_) => return utils::render_status_code(StatusCode::INTERNAL_SERVER_ERROR).await, Ok(None) => return utils::render_status_code(StatusCode::NOT_FOUND).await, Ok(Some(l)) => l, }; if (!user.is_admin) && link.user_id != user.id { - return utils::render_status_code(StatusCode::UNAUTHORIZED).await + return utils::render_status_code(StatusCode::UNAUTHORIZED).await; } let mut context = Context::new(); @@ -187,12 +191,12 @@ pub async fn delete( Err(utils::render_status_code(StatusCode::UNAUTHORIZED).await)? } l - }, + } }; - + match link.delete(&state.db).await { Err(_) => Err(utils::render_status_code(StatusCode::INTERNAL_SERVER_ERROR).await)?, - Ok(_) => () + Ok(_) => (), }; Ok(Redirect::to("/gay/link"))