add: create user api
This commit is contained in:
parent
6dff656583
commit
cda879f6df
11 changed files with 187 additions and 25 deletions
34
Cargo.lock
generated
34
Cargo.lock
generated
|
@ -165,6 +165,28 @@ dependencies = [
|
||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "axum-macros"
|
name = "axum-macros"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -415,6 +437,17 @@ dependencies = [
|
||||||
"xdg",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
@ -2006,6 +2039,7 @@ name = "tetratto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"rainbeam-shared",
|
"rainbeam-shared",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|
|
@ -16,3 +16,4 @@ axum = { version = "0.8.1", features = ["macros"] }
|
||||||
tokio = { version = "1.44.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.44.0", features = ["macros", "rt-multi-thread"] }
|
||||||
rainbeam-shared = "1.0.1"
|
rainbeam-shared = "1.0.1"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
|
axum-extra = { version = "0.10.0", features = ["cookie"] }
|
||||||
|
|
|
@ -6,6 +6,8 @@ pub const ATTO_JS: &str = include_str!("../public/js/atto.js");
|
||||||
|
|
||||||
// html
|
// html
|
||||||
pub const ROOT: &str = include_str!("../public/html/root.html");
|
pub const ROOT: &str = include_str!("../public/html/root.html");
|
||||||
|
pub const REDIRECT_TO_AUTH: &str =
|
||||||
|
"<head><meta http-equiv=\"refresh\" content=\"0; url=/_atto/login\" /></head>";
|
||||||
|
|
||||||
pub const AUTH_BASE: &str = include_str!("../public/html/auth/base.html");
|
pub const AUTH_BASE: &str = include_str!("../public/html/auth/base.html");
|
||||||
pub const LOGIN: &str = include_str!("../public/html/auth/login.html");
|
pub const LOGIN: &str = include_str!("../public/html/auth/login.html");
|
||||||
|
|
|
@ -1,29 +1,15 @@
|
||||||
use super::model::{Error, Result, User};
|
use super::model::{Error, Result, User};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::write_template;
|
||||||
|
|
||||||
use pathbufd::PathBufD as PathBuf;
|
use pathbufd::PathBufD as PathBuf;
|
||||||
use rainbeam_shared::hash::hash_salted;
|
use rainbeam_shared::hash::hash_salted;
|
||||||
use rusqlite::{Connection, Result as SqlResult, Row};
|
use rusqlite::{Connection, Result as SqlResult, Row};
|
||||||
use std::fs::{create_dir, exists, write};
|
use std::fs::{create_dir, exists};
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
pub struct DataManager(pub(crate) Config, pub 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 {
|
impl DataManager {
|
||||||
/// Obtain a connection to the staging database.
|
/// Obtain a connection to the staging database.
|
||||||
pub(crate) fn connect(name: &str) -> SqlResult<Connection> {
|
pub(crate) fn connect(name: &str) -> SqlResult<Connection> {
|
||||||
|
|
|
@ -12,6 +12,8 @@ pub enum Error {
|
||||||
RegistrationDisabled,
|
RegistrationDisabled,
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
IncorrectPassword,
|
IncorrectPassword,
|
||||||
|
AlreadyAuthenticated,
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Error {
|
impl ToString for Error {
|
||||||
|
@ -21,6 +23,7 @@ impl ToString for Error {
|
||||||
Error::UserNotFound => "Unable to find user with given parameters".to_string(),
|
Error::UserNotFound => "Unable to find user with given parameters".to_string(),
|
||||||
Error::RegistrationDisabled => "Registration is disabled".to_string(),
|
Error::RegistrationDisabled => "Registration is disabled".to_string(),
|
||||||
Error::IncorrectPassword => "The given password is invalid".to_string(),
|
Error::IncorrectPassword => "The given password is invalid".to_string(),
|
||||||
|
Error::AlreadyAuthenticated => "Already authenticated".to_string(),
|
||||||
_ => format!("An unknown error as occurred ({:?})", self),
|
_ => format!("An unknown error as occurred ({:?})", self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
src/macros.rs
Normal file
40
src/macros.rs
Normal file
|
@ -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) <optional>) => {{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod data;
|
mod data;
|
||||||
|
mod macros;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
use data::DataManager;
|
use data::DataManager;
|
||||||
|
|
1
src/routes/api/mod.rs
Normal file
1
src/routes/api/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod v1;
|
41
src/routes/api/v1/auth.rs
Normal file
41
src/routes/api/v1/auth.rs
Normal file
|
@ -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<State>,
|
||||||
|
Json(props): Json<AuthProps>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = data.read().await;
|
||||||
|
let user = get_user_from_token!((jar, data) <optional>);
|
||||||
|
|
||||||
|
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: (),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
32
src/routes/api/v1/mod.rs
Normal file
32
src/routes/api/v1/mod.rs
Normal file
|
@ -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<T>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
pub ok: bool,
|
||||||
|
pub message: String,
|
||||||
|
pub payload: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ApiReturn<T>
|
||||||
|
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,
|
||||||
|
}
|
|
@ -1,39 +1,58 @@
|
||||||
|
pub mod api;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
||||||
use crate::State;
|
use crate::{State, get_user_from_token};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Router,
|
Extension, Router,
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse, Redirect},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
/// `/`
|
/// `/`
|
||||||
pub async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
pub async fn index_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
|
let user = get_user_from_token!((jar, data) <optional>);
|
||||||
|
|
||||||
let mut context = data.initial_context();
|
let mut context = data.initial_context();
|
||||||
Html(data.1.render("index.html", &mut context).unwrap())
|
Html(data.1.render("index.html", &mut context).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/_atto/login`
|
/// `/_atto/login`
|
||||||
pub async fn login_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
|
let user = get_user_from_token!((jar, data) <optional>);
|
||||||
|
|
||||||
|
if user.is_some() {
|
||||||
|
return Err(Redirect::to("/"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut context = data.initial_context();
|
let mut context = data.initial_context();
|
||||||
Html(
|
Ok(Html(
|
||||||
data.1
|
data.1
|
||||||
.render("_atto/auth/login.html", &mut context)
|
.render("_atto/auth/login.html", &mut context)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/_atto/register`
|
/// `/_atto/register`
|
||||||
pub async fn register_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
pub async fn register_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
|
let user = get_user_from_token!((jar, data) <optional>);
|
||||||
|
|
||||||
|
if user.is_some() {
|
||||||
|
return Err(Redirect::to("/"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut context = data.initial_context();
|
let mut context = data.initial_context();
|
||||||
Html(
|
Ok(Html(
|
||||||
data.1
|
data.1
|
||||||
.render("_atto/auth/register.html", &mut context)
|
.render("_atto/auth/register.html", &mut context)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
|
@ -41,6 +60,8 @@ pub fn routes() -> Router {
|
||||||
// assets
|
// assets
|
||||||
.route("/css/style.css", get(assets::style_css_request))
|
.route("/css/style.css", get(assets::style_css_request))
|
||||||
.route("/js/atto.js", get(assets::atto_js_request))
|
.route("/js/atto.js", get(assets::atto_js_request))
|
||||||
|
// api
|
||||||
|
.nest("/api/v1", api::v1::routes())
|
||||||
// pages
|
// pages
|
||||||
.route("/", get(index_request))
|
.route("/", get(index_request))
|
||||||
.route("/_atto/login", get(login_request))
|
.route("/_atto/login", get(login_request))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue