Adding auth and signup/login/logout
This commit is contained in:
parent
1404a0b618
commit
c4bffa0387
15 changed files with 674 additions and 15 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "achubb_website"
|
||||
|
|
@ -10,8 +10,12 @@ dependencies = [
|
|||
"axum",
|
||||
"bb8",
|
||||
"clap",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
"pbkdf2",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"time",
|
||||
|
|
@ -382,6 +386,16 @@ version = "0.9.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
|
|
@ -1102,12 +1116,35 @@ dependencies = [
|
|||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
"password-hash",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
|
|
|||
13
Cargo.toml
13
Cargo.toml
|
|
@ -10,11 +10,20 @@ askama = "0.12.1"
|
|||
axum = "0.6"
|
||||
bb8 = "0.8.3"
|
||||
clap = { version = "4.5.13", features = ["derive"] }
|
||||
cookie = "0.18.1"
|
||||
futures-util = "0.3.30"
|
||||
pbkdf2 = { version = "0.12.2", features = ["simple"] }
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rand_core = "0.6.4"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
sqlx = {version = "0.7.4", features = ["runtime-tokio-rustls", "postgres", "time", "macros"]}
|
||||
time = {version = "0.3.36", features = ["macros", "serde"]}
|
||||
sqlx = { version = "0.7.4", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"postgres",
|
||||
"time",
|
||||
"macros",
|
||||
] }
|
||||
time = { version = "0.3.36", features = ["macros", "serde"] }
|
||||
tokio = { version = "1.35.0", features = ["full"] }
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.4.4", features = ["fs"] }
|
||||
|
|
|
|||
158
src/auth.rs
Normal file
158
src/auth.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use axum::{
|
||||
body::Empty,
|
||||
http::{Request, Response, StatusCode},
|
||||
middleware,
|
||||
response::IntoResponse,
|
||||
Extension, Form,
|
||||
};
|
||||
use pbkdf2::{
|
||||
password_hash::{PasswordHash, PasswordVerifier},
|
||||
Pbkdf2,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
session::{new_session, Random},
|
||||
user::create_user,
|
||||
},
|
||||
errors::{LoginError, SignupError},
|
||||
html::{
|
||||
root::{error_page, get_login},
|
||||
Login, Signup,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserInfo {
|
||||
pub user_id: i32,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthState(Option<(u128, Option<UserInfo>, PgPool)>);
|
||||
|
||||
pub async fn auth<B>(
|
||||
mut req: Request<B>,
|
||||
next: middleware::Next<B>,
|
||||
pool: PgPool,
|
||||
) -> axum::response::Response {
|
||||
let session_token = req
|
||||
.headers()
|
||||
.get_all("Cookie")
|
||||
.iter()
|
||||
.filter_map(|cookie| {
|
||||
cookie
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|cookie| cookie.parse::<cookie::Cookie>().ok())
|
||||
})
|
||||
.find_map(|cookie| {
|
||||
(cookie.name() == "session_token").then(move || cookie.value().to_owned())
|
||||
})
|
||||
.and_then(|cookie_value| cookie_value.parse::<u128>().ok());
|
||||
|
||||
if session_token.is_none()
|
||||
&& req.uri().to_string().contains("/admin")
|
||||
&& !req.uri().to_string().contains("/login")
|
||||
{
|
||||
return get_login().await.into_response();
|
||||
}
|
||||
|
||||
req.extensions_mut()
|
||||
.insert(AuthState(session_token.map(|v| (v, None, pool))));
|
||||
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
impl AuthState {
|
||||
pub async fn get_user(&mut self) -> Option<&UserInfo> {
|
||||
let (session_token, store, pool) = self.0.as_mut()?;
|
||||
|
||||
if store.is_none() {
|
||||
const QUERY: &str =
|
||||
"SELECT id, admin FROM users JOIN sessions ON user_id = id WHERE session_token = $1;";
|
||||
let user: Option<(i32, bool)> = sqlx::query_as(QUERY)
|
||||
.bind(&session_token.to_le_bytes().to_vec())
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some((id, admin)) = user {
|
||||
*store = Some(UserInfo {
|
||||
user_id: id,
|
||||
admin
|
||||
});
|
||||
}
|
||||
}
|
||||
store.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cookie(session_token: &str) -> impl IntoResponse {
|
||||
Response::builder()
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", "/")
|
||||
.header(
|
||||
"Set-Cookie",
|
||||
format!("session_token={}; Max-Age=999999", session_token),
|
||||
)
|
||||
.body(Empty::new())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn post_login(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(random): Extension<Random>,
|
||||
Form(login): Form<Login>,
|
||||
) -> impl IntoResponse {
|
||||
const LOGIN_QUERY: &str = "SELECT id, password FROM users WHERE users.username = $1;";
|
||||
|
||||
let row: Option<(i32, String)> = sqlx::query_as(LOGIN_QUERY)
|
||||
.bind(login.username)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (user_id, hashed_password) = if let Some(row) = row {
|
||||
row
|
||||
} else {
|
||||
return Err(error_page(&LoginError::UserDoesNotExist));
|
||||
};
|
||||
|
||||
// Verify password against PHC string
|
||||
let parsed_hash = PasswordHash::new(&hashed_password).unwrap();
|
||||
|
||||
if let Err(_err) = Pbkdf2.verify_password(login.password.as_bytes(), &parsed_hash) {
|
||||
return Err(error_page(&LoginError::WrongPassword));
|
||||
}
|
||||
|
||||
let session_token = new_session(&pool, random, user_id).await;
|
||||
|
||||
Ok(set_cookie(&session_token))
|
||||
}
|
||||
|
||||
pub async fn post_signup(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(random): Extension<Random>,
|
||||
Form(signup): Form<Signup>,
|
||||
) -> impl IntoResponse {
|
||||
if signup.password != signup.confirm_password {
|
||||
return Err(error_page(&SignupError::PasswordsDoNotMatch));
|
||||
}
|
||||
|
||||
let user_id = create_user(&signup.username, &signup.password, &pool).await.unwrap();
|
||||
|
||||
let session_token = new_session(&pool, random, user_id).await;
|
||||
|
||||
Ok(set_cookie(&session_token))
|
||||
}
|
||||
|
||||
pub async fn logout_response() -> impl IntoResponse {
|
||||
Response::builder()
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", "/")
|
||||
.header("Set-Cookie", "session_token=_; Max-Age=0")
|
||||
.body(Empty::new())
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ use std::{
|
|||
|
||||
pub mod article;
|
||||
pub mod link;
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
|
||||
pub async fn establish_connection() -> Result<PgPool, Box<dyn Error>> {
|
||||
let db_url = match env::var("ACHUBB_DATABASE_URL") {
|
||||
|
|
|
|||
108
src/database/session.rs
Normal file
108
src/database/session.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use crate::{database::PsqlData, errors::DatabaseError};
|
||||
use futures_util::TryStreamExt;
|
||||
use rand::RngCore;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::{
|
||||
error::Error,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub type Random = Arc<Mutex<ChaCha8Rng>>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Session {
|
||||
pub session_token: Vec<u8>,
|
||||
pub user_id: i32,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub async fn read_by_token(
|
||||
pool: &PgPool,
|
||||
session_token: &String,
|
||||
) -> Result<Box<Self>, Box<dyn Error>> {
|
||||
let token: Vec<u8> = session_token.parse::<u128>()?.to_le_bytes().to_vec();
|
||||
let result = sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT * FROM sessions WHERE session_token = $1",
|
||||
token,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl PsqlData for Session {
|
||||
async fn read_all(pool: &PgPool) -> Result<Vec<Box<Self>>, Box<dyn Error>> {
|
||||
crate::psql_read_all!(Self, pool, "sessions")
|
||||
}
|
||||
|
||||
async fn read(pool: &PgPool, id: i32) -> Result<Box<Self>, Box<dyn Error>> {
|
||||
let result = sqlx::query_as!(Self, "SELECT * FROM sessions WHERE user_id = $1", id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
async fn insert(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO sessions (session_token, user_id) VALUES ($1, $2)",
|
||||
self.session_token,
|
||||
self.user_id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
sqlx::query!(
|
||||
"UPDATE sessions SET session_token=$1 WHERE user_id=$2",
|
||||
self.session_token,
|
||||
self.user_id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
let _result = sqlx::query!("DELETE FROM sessions WHERE user_id = $1", self.user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_session(pool: &PgPool, random: Random, user_id: i32) -> String {
|
||||
let mut u128_pool = [0u8; 16];
|
||||
random.lock().unwrap().fill_bytes(&mut u128_pool);
|
||||
|
||||
let session_token = u128::from_le_bytes(u128_pool);
|
||||
|
||||
let session = Session {
|
||||
user_id,
|
||||
session_token: session_token.to_le_bytes().to_vec(),
|
||||
};
|
||||
|
||||
session.insert(pool).await.unwrap();
|
||||
|
||||
session_token.to_string()
|
||||
}
|
||||
|
||||
pub async fn clear_sessions_for_user(pool: &PgPool, user_id: i32) -> Result<(), DatabaseError> {
|
||||
const QUERY: &str = "DELETE FROM sessions WHERE user_id=$1;";
|
||||
let _result = sqlx::query(QUERY)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
107
src/database/user.rs
Normal file
107
src/database/user.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
use crate::{database::PsqlData, errors::SignupError};
|
||||
use futures_util::TryStreamExt;
|
||||
use pbkdf2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||
Pbkdf2,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn read_by_name(
|
||||
pool: &PgPool,
|
||||
username: &String,
|
||||
) -> Result<Box<Self>, Box<dyn Error>> {
|
||||
let result = sqlx::query_as!(Self, "SELECT * FROM users WHERE username = $1", username,)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl PsqlData for User {
|
||||
async fn read_all(pool: &PgPool) -> Result<Vec<Box<Self>>, Box<dyn Error>> {
|
||||
crate::psql_read_all!(Self, pool, "users")
|
||||
}
|
||||
|
||||
async fn read(pool: &PgPool, id: i32) -> Result<Box<Self>, Box<dyn Error>> {
|
||||
crate::psql_read!(Self, pool, id, "users")
|
||||
}
|
||||
|
||||
async fn insert(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO users (username, password, admin) VALUES ($1, $2, $3)",
|
||||
self.username,
|
||||
self.password,
|
||||
self.admin,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET username=$1, password=$2 WHERE id=$3",
|
||||
self.username,
|
||||
self.password,
|
||||
self.id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
let id = &self.id;
|
||||
crate::psql_delete!(id, pool, "users")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
username: &str,
|
||||
password: &str,
|
||||
pool: &PgPool,
|
||||
) -> Result<i32, SignupError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
// Hash password to PHC string ($pbkdf2-sha256$...)
|
||||
let hashed_password = Pbkdf2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
const INSERT_QUERY: &str =
|
||||
"INSERT INTO users (username, password, admin) VALUES ($1, $2, $3) RETURNING id;";
|
||||
|
||||
let fetch_one = sqlx::query_as(INSERT_QUERY)
|
||||
.bind(username)
|
||||
.bind(hashed_password)
|
||||
.bind(false)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
match fetch_one {
|
||||
Ok((user_id,)) => Ok(user_id),
|
||||
Err(sqlx::Error::Database(database))
|
||||
if database.constraint() == Some("users_username_key") =>
|
||||
{
|
||||
return Err(SignupError::UsernameExists);
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(SignupError::InternalError);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/errors.rs
Normal file
70
src/errors.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use std::{error::Error, fmt::Display};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotLoggedIn;
|
||||
|
||||
impl Display for NotLoggedIn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NotLoggedIn {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SignupError {
|
||||
UsernameExists,
|
||||
InvalidUsername,
|
||||
PasswordsDoNotMatch,
|
||||
MissingDetails,
|
||||
InvalidPassword,
|
||||
InternalError,
|
||||
}
|
||||
|
||||
impl Display for SignupError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SignupError::InvalidUsername => f.write_str("Invalid username"),
|
||||
SignupError::UsernameExists => f.write_str("Username already exists"),
|
||||
SignupError::PasswordsDoNotMatch => f.write_str("Passwords do not match"),
|
||||
SignupError::MissingDetails => f.write_str("Missing Details"),
|
||||
SignupError::InvalidPassword => f.write_str("Invalid Password"),
|
||||
SignupError::InternalError => f.write_str("Internal Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SignupError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginError {
|
||||
UserDoesNotExist,
|
||||
WrongPassword,
|
||||
}
|
||||
|
||||
impl Display for LoginError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LoginError::UserDoesNotExist => f.write_str("User does not exist"),
|
||||
LoginError::WrongPassword => f.write_str("Wrong password"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for LoginError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoUser(pub String);
|
||||
|
||||
impl Display for NoUser {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("could not find user '{}'", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for NoUser {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DatabaseError {
|
||||
NoEntries,
|
||||
}
|
||||
31
src/html/admin.rs
Normal file
31
src/html/admin.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{get, Router},
|
||||
Extension,
|
||||
};
|
||||
|
||||
use crate::auth::{AuthState, UserInfo};
|
||||
|
||||
use super::templates::{AdminTemplate, HtmlTemplate};
|
||||
|
||||
pub fn get_router() -> Router {
|
||||
Router::new().route("/", get(get_admin))
|
||||
}
|
||||
|
||||
pub async fn get_admin(
|
||||
Extension(mut current_user): Extension<AuthState>,
|
||||
) -> Response {
|
||||
let user_info: UserInfo = current_user.get_user().await.unwrap().clone();
|
||||
if !user_info.admin {
|
||||
return Redirect::to("/").into_response();
|
||||
}
|
||||
HtmlTemplate(AdminTemplate {}).into_response()
|
||||
}
|
||||
|
||||
fn get_trimmed_string(input: String) -> Option<String> {
|
||||
if input.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(input.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,20 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
pub mod admin;
|
||||
pub mod blog;
|
||||
pub mod garden;
|
||||
pub mod root;
|
||||
pub mod templates;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Login {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Signup {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub confirm_password: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,42 @@
|
|||
use std:: sync::{Arc, Mutex};
|
||||
|
||||
use axum::{
|
||||
http::{Response, StatusCode},
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, Router},
|
||||
routing::{get, post, Router},
|
||||
Extension,
|
||||
};
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use rand::{seq::SliceRandom, thread_rng, RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rand_core::OsRng;
|
||||
use sqlx::PgPool;
|
||||
use std::error::Error;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::database::{
|
||||
link::{Link, LinkType},
|
||||
PsqlData,
|
||||
use crate::{
|
||||
auth::{auth, logout_response, post_login, post_signup},
|
||||
database::{
|
||||
link::{Link, LinkType},
|
||||
PsqlData,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
blog::{self, get_articles_date_sorted},
|
||||
garden,
|
||||
templates::{
|
||||
AboutTemplate, AiTemplate, BlogrollTemplate, ContactTemplate, GiftsTemplate, HomeTemplate,
|
||||
HtmlTemplate, InterestsTemplate, LinksPageTemplate, NowTemplate, ResumeTemplate,
|
||||
WorkTemplate, UsesTemplate,
|
||||
},
|
||||
admin, blog::{self, get_articles_date_sorted}, garden, templates::{
|
||||
AboutTemplate, AiTemplate, BlogrollTemplate, ContactTemplate, GiftsTemplate, HomeTemplate, HtmlTemplate, InterestsTemplate, LinksPageTemplate, LoginTemplate, NowTemplate, ResumeTemplate, SignupTemplate, UsesTemplate, WorkTemplate
|
||||
}
|
||||
};
|
||||
|
||||
pub fn get_router(pool: PgPool) -> Router {
|
||||
let assets_path = std::env::current_dir().unwrap();
|
||||
|
||||
let random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
|
||||
let middleware_database = pool.clone();
|
||||
|
||||
Router::new()
|
||||
.nest("/blog", blog::get_router())
|
||||
.nest("/garden", garden::get_router())
|
||||
.nest("/admin", admin::get_router())
|
||||
.nest_service(
|
||||
"/assets",
|
||||
ServeDir::new(format!("{}/assets", assets_path.to_str().unwrap())),
|
||||
|
|
@ -44,11 +53,18 @@ pub fn get_router(pool: PgPool) -> Router {
|
|||
.route("/resume", get(resume))
|
||||
.route("/gifts", get(gifts))
|
||||
.route("/hire", get(work))
|
||||
.route("/login", get(get_login).post(post_login))
|
||||
.route("/signup", get(get_signup).post(post_signup))
|
||||
.route("/logout", post(logout_response))
|
||||
.route(
|
||||
"/robots.txt",
|
||||
get(|| async { Redirect::permanent("/assets/robots.txt") }),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(move |req, next| {
|
||||
auth(req, next, middleware_database.clone())
|
||||
}))
|
||||
.layer(Extension(pool))
|
||||
.layer(Extension(Arc::new(Mutex::new(random))))
|
||||
}
|
||||
|
||||
async fn home(Extension(pool): Extension<PgPool>) -> impl IntoResponse {
|
||||
|
|
@ -133,3 +149,18 @@ pub async fn get_links_as_list(
|
|||
.collect();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
pub fn error_page(err: &dyn std::error::Error) -> impl IntoResponse {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(format!("Err: {}", err))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_login() -> impl IntoResponse {
|
||||
HtmlTemplate(LoginTemplate { username: None })
|
||||
}
|
||||
|
||||
pub async fn get_signup() -> impl IntoResponse {
|
||||
HtmlTemplate(SignupTemplate {})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,3 +144,17 @@ pub struct WorkTemplate {}
|
|||
#[derive(Template)]
|
||||
#[template(path = "technology.html")]
|
||||
pub struct TechnologyTemplate {}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login.html")]
|
||||
pub struct LoginTemplate {
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "signup.html")]
|
||||
pub struct SignupTemplate {}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin.html")]
|
||||
pub struct AdminTemplate {}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use std::error::Error;
|
|||
use tracing::info;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod auth;
|
||||
pub mod database;
|
||||
mod errors;
|
||||
mod html;
|
||||
mod macros;
|
||||
|
||||
|
|
|
|||
12
templates/admin.html
Normal file
12
templates/admin.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!-- prettier-ignore -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Admin</h2>
|
||||
<form action="/logout" method="post">
|
||||
<input type="submit" value="logout">
|
||||
</form>
|
||||
<p>
|
||||
My admin page
|
||||
</p>
|
||||
{% endblock %}
|
||||
32
templates/login.html
Normal file
32
templates/login.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<link href="/assets/main.css" rel="stylesheet" />
|
||||
<link rel="shortcut icon" href="/assets/favicon.ico">
|
||||
<title>Awstin</title>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="content">
|
||||
<form action="/login" method="post">
|
||||
<p>
|
||||
{% if let Some(username) = username %}
|
||||
<label for="username">{{username}}</label>
|
||||
<input type="hidden" name="username" id="username" value="{{ username }}" minlength="1" maxlength="20" pattern="[0-9a-z]+" required>
|
||||
{% else %}
|
||||
<label for="username">Username: </label>
|
||||
<input type="text" name="username" id="username" minlength="1" maxlength="20" pattern="[0-9a-z]+" required>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Password: </label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
</p>
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
31
templates/signup.html
Normal file
31
templates/signup.html
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<link href="/assets/main.css" rel="stylesheet" />
|
||||
<link rel="shortcut icon" href="/assets/favicon.ico">
|
||||
<title>Awstin</title>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="content">
|
||||
<form action="/signup" method="post">
|
||||
<p>
|
||||
<label for="username">Username: </label>
|
||||
<input type="text" name="username" id="username" minlength="1" maxlength="20" pattern="[0-9a-z]+" required>
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Password: </label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Confirm Password: </label>
|
||||
<input type="password" name="confirm_password" id="confirm_password" required>
|
||||
</p>
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in a new issue