diff --git a/Cargo.lock b/Cargo.lock index 1450f3e..f740a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-macros" version = "0.5.0" @@ -415,6 +437,17 @@ dependencies = [ "xdg", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2006,6 +2039,7 @@ name = "tetratto" version = "0.1.0" dependencies = [ "axum", + "axum-extra", "pathbufd", "rainbeam-shared", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index 803a47c..2d3a620 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ axum = { version = "0.8.1", features = ["macros"] } tokio = { version = "1.44.0", features = ["macros", "rt-multi-thread"] } rainbeam-shared = "1.0.1" serde_json = "1.0.140" +axum-extra = { version = "0.10.0", features = ["cookie"] } diff --git a/src/data/assets.rs b/src/data/assets.rs index d5324fe..3613d3f 100644 --- a/src/data/assets.rs +++ b/src/data/assets.rs @@ -6,6 +6,8 @@ pub const ATTO_JS: &str = include_str!("../public/js/atto.js"); // html pub const ROOT: &str = include_str!("../public/html/root.html"); +pub const REDIRECT_TO_AUTH: &str = + ""; pub const AUTH_BASE: &str = include_str!("../public/html/auth/base.html"); pub const LOGIN: &str = include_str!("../public/html/auth/login.html"); diff --git a/src/data/manager.rs b/src/data/manager.rs index 1e45733..b45fb26 100644 --- a/src/data/manager.rs +++ b/src/data/manager.rs @@ -1,29 +1,15 @@ use super::model::{Error, Result, User}; use crate::config::Config; +use crate::write_template; use pathbufd::PathBufD as PathBuf; use rainbeam_shared::hash::hash_salted; use rusqlite::{Connection, Result as SqlResult, Row}; -use std::fs::{create_dir, exists, write}; +use std::fs::{create_dir, exists}; use tera::{Context, Tera}; pub struct DataManager(pub(crate) Config, pub Tera); -macro_rules! write_template { - ($atto_dir:ident->$path:literal($as:expr)) => { - write($atto_dir.join($path), $as).unwrap(); - }; - - ($atto_dir:ident->$path:literal($as:expr) -d $dir_path:literal) => { - let dir = $atto_dir.join($dir_path); - if !exists(&dir).unwrap() { - create_dir(dir).unwrap(); - } - - write($atto_dir.join($path), $as).unwrap(); - }; -} - impl DataManager { /// Obtain a connection to the staging database. pub(crate) fn connect(name: &str) -> SqlResult { diff --git a/src/data/model.rs b/src/data/model.rs index 3bfccf9..8fbdc8f 100644 --- a/src/data/model.rs +++ b/src/data/model.rs @@ -12,6 +12,8 @@ pub enum Error { RegistrationDisabled, DatabaseError, IncorrectPassword, + AlreadyAuthenticated, + Unknown, } impl ToString for Error { @@ -21,6 +23,7 @@ impl ToString for Error { Error::UserNotFound => "Unable to find user with given parameters".to_string(), Error::RegistrationDisabled => "Registration is disabled".to_string(), Error::IncorrectPassword => "The given password is invalid".to_string(), + Error::AlreadyAuthenticated => "Already authenticated".to_string(), _ => format!("An unknown error as occurred ({:?})", self), } } diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..69bae99 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,40 @@ +#[macro_export] +macro_rules! write_template { + ($atto_dir:ident->$path:literal($as:expr)) => { + std::fs::write($atto_dir.join($path), $as).unwrap(); + }; + + ($atto_dir:ident->$path:literal($as:expr) -d $dir_path:literal) => { + let dir = $atto_dir.join($dir_path); + if !std::fs::exists(&dir).unwrap() { + std::fs::create_dir(dir).unwrap(); + } + + std::fs::write($atto_dir.join($path), $as).unwrap(); + }; +} + +#[macro_export] +macro_rules! get_user_from_token { + (($jar:ident, $db:ident) ) => {{ + if let Some(token) = $jar.get("__Secure-Atto-Token") { + match $db.get_user_by_token(&token.to_string()).await { + Ok(ua) => Some(ua), + Err(_) => None, + } + } else { + None + } + }}; + + ($jar:ident, $db:ident) => {{ + if let Some(token) = $jar.get("__Secure-Atto-Token") { + match $db.get_user_by_token(token) { + Ok(ua) => ua, + Err(_) => return axum::response::Html(crate::data::assets::REDIRECT_TO_AUTH), + } + } else { + None + } + }}; +} diff --git a/src/main.rs b/src/main.rs index 45efbc5..3891545 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod config; mod data; +mod macros; mod routes; use data::DataManager; diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs new file mode 100644 index 0000000..a3a6d96 --- /dev/null +++ b/src/routes/api/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/src/routes/api/v1/auth.rs b/src/routes/api/v1/auth.rs new file mode 100644 index 0000000..9256a3a --- /dev/null +++ b/src/routes/api/v1/auth.rs @@ -0,0 +1,41 @@ +use super::{ApiReturn, AuthProps}; +use crate::{ + State, + data::model::{Error, User}, + get_user_from_token, +}; +use axum::{Extension, Json, response::IntoResponse}; +use axum_extra::extract::CookieJar; + +pub async fn register_request( + jar: CookieJar, + Extension(data): Extension, + Json(props): Json, +) -> impl IntoResponse { + let data = data.read().await; + let user = get_user_from_token!((jar, data) ); + + if user.is_some() { + return Json(ApiReturn { + ok: false, + message: Error::AlreadyAuthenticated.to_string(), + payload: (), + }); + } + + match data + .create_user(User::new(props.username, props.password)) + .await + { + Ok(_) => Json(ApiReturn { + ok: true, + message: "User created".to_string(), + payload: (), + }), + Err(_) => Json(ApiReturn { + ok: false, + message: Error::Unknown.to_string(), + payload: (), + }), + } +} diff --git a/src/routes/api/v1/mod.rs b/src/routes/api/v1/mod.rs new file mode 100644 index 0000000..1b3f2f8 --- /dev/null +++ b/src/routes/api/v1/mod.rs @@ -0,0 +1,32 @@ +pub mod auth; +use axum::{Router, routing::post}; +use serde::{Deserialize, Serialize}; + +pub fn routes() -> Router { + Router::new().route("/auth/register", post(auth::register_request)) +} + +#[derive(Serialize, Deserialize)] +pub struct ApiReturn +where + T: Serialize, +{ + pub ok: bool, + pub message: String, + pub payload: T, +} + +impl ApiReturn +where + T: Serialize, +{ + pub fn to_json(&self) -> String { + serde_json::to_string(&self).unwrap() + } +} + +#[derive(Deserialize)] +pub struct AuthProps { + pub username: String, + pub password: String, +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 3e9371b..5dc5573 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,39 +1,58 @@ +pub mod api; pub mod assets; -use crate::State; +use crate::{State, get_user_from_token}; use axum::{ Extension, Router, - response::{Html, IntoResponse}, + response::{Html, IntoResponse, Redirect}, routing::get, }; +use axum_extra::extract::CookieJar; /// `/` -pub async fn index_request(Extension(data): Extension) -> impl IntoResponse { +pub async fn index_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = data.read().await; + let user = get_user_from_token!((jar, data) ); + let mut context = data.initial_context(); Html(data.1.render("index.html", &mut context).unwrap()) } /// `/_atto/login` -pub async fn login_request(Extension(data): Extension) -> impl IntoResponse { +pub async fn login_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = data.read().await; + let user = get_user_from_token!((jar, data) ); + + if user.is_some() { + return Err(Redirect::to("/")); + } + let mut context = data.initial_context(); - Html( + Ok(Html( data.1 .render("_atto/auth/login.html", &mut context) .unwrap(), - ) + )) } /// `/_atto/register` -pub async fn register_request(Extension(data): Extension) -> impl IntoResponse { +pub async fn register_request( + jar: CookieJar, + Extension(data): Extension, +) -> impl IntoResponse { let data = data.read().await; + let user = get_user_from_token!((jar, data) ); + + if user.is_some() { + return Err(Redirect::to("/")); + } + let mut context = data.initial_context(); - Html( + Ok(Html( data.1 .render("_atto/auth/register.html", &mut context) .unwrap(), - ) + )) } pub fn routes() -> Router { @@ -41,6 +60,8 @@ pub fn routes() -> Router { // assets .route("/css/style.css", get(assets::style_css_request)) .route("/js/atto.js", get(assets::atto_js_request)) + // api + .nest("/api/v1", api::v1::routes()) // pages .route("/", get(index_request)) .route("/_atto/login", get(login_request))