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))