Clearing out unused login stuff
Updating Schema and Now
This commit is contained in:
parent
c3d16a0a5d
commit
2bb6ddfe8f
17 changed files with 609 additions and 1548 deletions
1349
Cargo.lock
generated
1349
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,18 +8,12 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = "0.12.1"
|
askama = "0.12.1"
|
||||||
axum = "0.6"
|
axum = "0.6"
|
||||||
bb8 = "0.8.3"
|
|
||||||
chrono = { version = "0.4.38", features = ["alloc", "serde"] }
|
chrono = { version = "0.4.38", features = ["alloc", "serde"] }
|
||||||
clap = { version = "4.5.13", features = ["derive"] }
|
clap = { version = "4.5.13", features = ["derive"] }
|
||||||
cookie = "0.18.1"
|
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
futures-util = "0.3.30"
|
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"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
sqlx = { version = "0.7.4", features = [
|
sqlx = { version = "0.8.6", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"macros",
|
"macros",
|
||||||
"postgres",
|
"postgres",
|
||||||
|
|
|
||||||
10
schema.sql
10
schema.sql
|
|
@ -24,13 +24,3 @@ CREATE TABLE IF NOT EXISTS links (
|
||||||
author varchar(50) not null,
|
author varchar(50) not null,
|
||||||
link_type link_type not null,
|
link_type link_type not null,
|
||||||
id serial primary key);
|
id serial primary key);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
|
||||||
username text NOT NULL UNIQUE,
|
|
||||||
password text NOT NULL,
|
|
||||||
admin boolean NOT NULL);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
session_token BYTEA PRIMARY KEY,
|
|
||||||
user_id integer REFERENCES users (id) ON DELETE CASCADE NOT NULL);
|
|
||||||
|
|
|
||||||
157
src/auth.rs
157
src/auth.rs
|
|
@ -1,157 +0,0 @@
|
||||||
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,8 +9,6 @@ use std::{
|
||||||
|
|
||||||
pub mod article;
|
pub mod article;
|
||||||
pub mod link;
|
pub mod link;
|
||||||
pub mod session;
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
pub async fn establish_connection() -> Result<PgPool, Box<dyn Error>> {
|
pub async fn establish_connection() -> Result<PgPool, Box<dyn Error>> {
|
||||||
let db_url = match env::var("ACHUBB_DATABASE_URL") {
|
let db_url = match env::var("ACHUBB_DATABASE_URL") {
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
use core::panic;
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
response::{IntoResponse, Redirect, Response},
|
|
||||||
routing::{get, post, Router},
|
|
||||||
Extension, Form,
|
|
||||||
};
|
|
||||||
use chrono::Local;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
auth::{AuthState, UserInfo},
|
|
||||||
database::{
|
|
||||||
link::{Link, LinkType},
|
|
||||||
PsqlData,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
templates::{AdminTemplate, HtmlTemplate},
|
|
||||||
NewLink,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_router() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/", get(get_admin))
|
|
||||||
.route("/new_link", post(new_link))
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_link(
|
|
||||||
Extension(mut current_user): Extension<AuthState>,
|
|
||||||
Extension(pool): Extension<PgPool>,
|
|
||||||
Form(new_item): Form<NewLink>,
|
|
||||||
) -> Response {
|
|
||||||
let user_info: UserInfo = current_user.get_user().await.unwrap().clone();
|
|
||||||
if !user_info.admin {
|
|
||||||
return Redirect::to("/").into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_link: Link = Link {
|
|
||||||
id: 0,
|
|
||||||
url: new_item.url,
|
|
||||||
title: new_item.title,
|
|
||||||
author: new_item.author,
|
|
||||||
link_type: match &*new_item.link_type {
|
|
||||||
"article" => LinkType::ARTICLE,
|
|
||||||
"blog" => LinkType::BLOG,
|
|
||||||
_ => panic!("Not a proper link type"),
|
|
||||||
},
|
|
||||||
description: get_trimmed_string(new_item.description),
|
|
||||||
date_added: Local::now().date_naive(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = new_link.insert(&pool).await;
|
|
||||||
|
|
||||||
Redirect::to("/admin").into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_trimmed_string(input: String) -> Option<String> {
|
|
||||||
if input.trim().is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(input.trim().to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +1,5 @@
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
pub mod admin;
|
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
pub mod books;
|
pub mod books;
|
||||||
pub mod root;
|
pub mod root;
|
||||||
pub mod templates;
|
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct NewLink {
|
|
||||||
pub url: String,
|
|
||||||
pub description: String,
|
|
||||||
pub title: String,
|
|
||||||
pub author: String,
|
|
||||||
pub link_type: String,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,36 @@
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
http::{Response, StatusCode},
|
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{get, post, Router},
|
routing::{get, Router},
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
use rand::{RngCore, SeedableRng};
|
|
||||||
use rand_chacha::ChaCha8Rng;
|
|
||||||
use rand_core::OsRng;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use crate::{
|
use crate::database::{
|
||||||
auth::{auth, logout_response, post_login, post_signup},
|
|
||||||
database::{
|
|
||||||
link::{Link, LinkType},
|
link::{Link, LinkType},
|
||||||
PsqlData,
|
PsqlData,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
admin, books,
|
books,
|
||||||
posts::{self, get_articles_date_sorted},
|
posts::{self, get_articles_date_sorted},
|
||||||
projects,
|
projects,
|
||||||
templates::{
|
templates::{
|
||||||
AboutTemplate, AiTemplate, BlogrollTemplate, ContactTemplate, CookingTemplate,
|
AboutTemplate, AiTemplate, BlogrollTemplate, ContactTemplate, CookingTemplate,
|
||||||
CreationTemplate, GiftsTemplate, HomeTemplate, HtmlTemplate, InterestsTemplate,
|
CreationTemplate, GiftsTemplate, HomeTemplate, HtmlTemplate, InterestsTemplate,
|
||||||
LinksPageTemplate, LoginTemplate, MoneyTemplate, NowTemplate, SignupTemplate,
|
LinksPageTemplate, MoneyTemplate, NowTemplate, TechnologyTemplate, TimeTemplate,
|
||||||
TechnologyTemplate, TimeTemplate, UsesTemplate,
|
UsesTemplate,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn get_router(pool: PgPool) -> Router {
|
pub fn get_router(pool: PgPool) -> Router {
|
||||||
let assets_path = std::env::current_dir().unwrap();
|
let assets_path = std::env::current_dir().unwrap();
|
||||||
|
|
||||||
let random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
|
|
||||||
let middleware_database = pool.clone();
|
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/posts", posts::get_router())
|
.nest("/posts", posts::get_router())
|
||||||
.nest("/projects", projects::get_router())
|
.nest("/projects", projects::get_router())
|
||||||
.nest("/books", books::get_router())
|
.nest("/books", books::get_router())
|
||||||
.nest("/admin", admin::get_router())
|
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/assets",
|
"/assets",
|
||||||
ServeDir::new(format!("{}/assets", assets_path.to_str().unwrap())),
|
ServeDir::new(format!("{}/assets", assets_path.to_str().unwrap())),
|
||||||
|
|
@ -63,9 +50,6 @@ pub fn get_router(pool: PgPool) -> Router {
|
||||||
.route("/creation", get(creation))
|
.route("/creation", get(creation))
|
||||||
.route("/technology", get(technology))
|
.route("/technology", get(technology))
|
||||||
.route("/money", get(money))
|
.route("/money", get(money))
|
||||||
.route("/login", get(get_login).post(post_login))
|
|
||||||
.route("/signup", get(get_signup).post(post_signup))
|
|
||||||
.route("/logout", post(logout_response))
|
|
||||||
.route(
|
.route(
|
||||||
"/robots.txt",
|
"/robots.txt",
|
||||||
get(|| async { Redirect::permanent("/assets/robots.txt") }),
|
get(|| async { Redirect::permanent("/assets/robots.txt") }),
|
||||||
|
|
@ -74,11 +58,7 @@ pub fn get_router(pool: PgPool) -> Router {
|
||||||
"/feed.xml",
|
"/feed.xml",
|
||||||
get(|| async { Redirect::permanent("/assets/feed.xml") }),
|
get(|| async { Redirect::permanent("/assets/feed.xml") }),
|
||||||
)
|
)
|
||||||
.layer(axum::middleware::from_fn(move |req, next| {
|
|
||||||
auth(req, next, middleware_database.clone())
|
|
||||||
}))
|
|
||||||
.layer(Extension(pool))
|
.layer(Extension(pool))
|
||||||
.layer(Extension(Arc::new(Mutex::new(random))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn home(Extension(pool): Extension<PgPool>) -> impl IntoResponse {
|
async fn home(Extension(pool): Extension<PgPool>) -> impl IntoResponse {
|
||||||
|
|
@ -155,21 +135,6 @@ pub async fn get_links_as_list(
|
||||||
Ok(list)
|
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 {})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn time() -> impl IntoResponse {
|
async fn time() -> impl IntoResponse {
|
||||||
let time_page = TimeTemplate {};
|
let time_page = TimeTemplate {};
|
||||||
HtmlTemplate(time_page)
|
HtmlTemplate(time_page)
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,6 @@ pub struct ArticleTemplate {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "page.html")]
|
|
||||||
pub struct PageTemplate {
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "links.html")]
|
#[template(path = "links.html")]
|
||||||
pub struct LinksPageTemplate {
|
pub struct LinksPageTemplate {
|
||||||
|
|
@ -133,32 +127,10 @@ pub struct PostFooterTemplate {
|
||||||
pub next: String,
|
pub next: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "resume.html")]
|
|
||||||
pub struct ResumeTemplate {}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "work.html")]
|
|
||||||
pub struct WorkTemplate {}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "technology.html")]
|
#[template(path = "technology.html")]
|
||||||
pub struct TechnologyTemplate {}
|
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 {}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "money.html")]
|
#[template(path = "money.html")]
|
||||||
pub struct MoneyTemplate {}
|
pub struct MoneyTemplate {}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ use std::error::Error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
mod auth;
|
|
||||||
pub mod database;
|
pub mod database;
|
||||||
mod errors;
|
|
||||||
mod html;
|
mod html;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<!-- prettier-ignore -->
|
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2>Admin</h2>
|
|
||||||
<form action="/logout" method="post">
|
|
||||||
<input type="submit" value="Logout">
|
|
||||||
</form>
|
|
||||||
<h3>New Link</h3>
|
|
||||||
<form action="/admin/new_link" method="post">
|
|
||||||
<p>
|
|
||||||
<label for="title">Title:</label>
|
|
||||||
<input type="text" name="title" id="title" required>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="author">Author:</label>
|
|
||||||
<input type="text" name="author" id="author" required>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="url">URL:</label>
|
|
||||||
<input type="text" name="url" id="url" required>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="link_type">Type:</label>
|
|
||||||
<select name="link_type" id="link_type">
|
|
||||||
<option value="blog">Blog</option>
|
|
||||||
<option value="article">Article</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label for="description">Description:</label>
|
|
||||||
<br>
|
|
||||||
<textarea name="description" id="description" rows="5" columns="50">
|
|
||||||
</textarea>
|
|
||||||
</p>
|
|
||||||
<input type="submit" value="Submit">
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -3,60 +3,36 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
Last updated: 2025-04-08
|
Last updated: 2026-01-04
|
||||||
</p>
|
|
||||||
<h2>Work</h2>
|
|
||||||
<p>
|
|
||||||
The project that I am focussed on we took too big of a bite from the apple all at once.
|
|
||||||
Trying to deliver something too large, and without being able to iterate quickly there were roadbumps.
|
|
||||||
We have re-prioritized on what will be minimally functional so that the new tool is used, any other features we can add after the fact.
|
|
||||||
That portion is almost done.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Still keeping my eyes open for other opportunities.
|
A lot has changed in the last year,
|
||||||
</p>
|
got married,
|
||||||
<h2>Brazilian Jiu-Jitsu</h2>
|
welcomed 2 new people into the world, one from family and one from one of my best friends,
|
||||||
<p>
|
traveled to a half dozen countries.
|
||||||
The team here at GB Toronto has become an external family.
|
|
||||||
I am so grateful for the community.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Training has lightened up post competition.
|
I am still training BJJ, thought not as intensely as before.
|
||||||
The competition did not go very well but that happens and I learned a lot.
|
Other priorities in life, it still keeps me sane and gives me a reason to stay in shape.
|
||||||
Now enjoying having a break from the stress of competition prep.
|
But it has not been the centre of my attention that it was.
|
||||||
</p>
|
Has been nice to transition from obsession to joy and comfort.
|
||||||
<h2>Learning</h2>
|
I will try and get a couple competitions in this year as normal.
|
||||||
<p>
|
|
||||||
I am working to learn japanese and portuguese on Duolingo.
|
|
||||||
I love the program and it is 100% worth paying for, but the fact that even when paying it keeps asking for ratings, putting widgets on my home screen, or turning on notifications bothers me.
|
|
||||||
You already have my money now please don't bother me.
|
|
||||||
Finding the lessons really helpful.
|
|
||||||
Coupling it with Anki flashcards for memory is working well.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Starting to learn <a href="go.dev">Go</a>.
|
My own projects have been languishing for a while.
|
||||||
A very simple and concise language.
|
I don't have that much of a drive after a day at work, hoping to change that this year.
|
||||||
Both similar in ways to Rust (focus on performance, strong type system), and completely different (still a runtime, much simpler).
|
Currently setting up a used mini PC as a home server, learning about FreeBSD and virtualization.
|
||||||
Enjoying it so far, still very early days.
|
Goal is to move most of my cloud hosted stuff locally, and build some new tools.
|
||||||
|
Have run into the wall of "I don't know as much about this as I thought I did", so it is simultaneously and exciting and frustrating learning experience.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Joined the <a href="https://leanwebclub.com">Lean Web Club</a> course for web development.
|
Also have a long list of stuff to build around the apartment.
|
||||||
Have been muddling my way through on my own up to this point trying to just use HTML and CSS with most of the actual logic in the backend where I am more comfortable.
|
More storage planned out to use the space more efficiently, get some more art and things mounted on the walls.
|
||||||
Figured it was time to learn all the stuff that browsers are capable of and how to make the best use of them.
|
In the design phase, sketching out desks, cabinets, shelving, and working out the amount of material that I am going to need.
|
||||||
They have come a long way and there is so much that you can do with just plain JS and no external dependencies.
|
I was looking at buying a bunch more tools but realized that is what the tool library is for, so once I have the materials I will borrow them one weekend at a time.
|
||||||
</p>
|
I have not built anything physical for a bit and am looking forward to it.
|
||||||
<h2>Tinkering</h2>
|
|
||||||
<p>
|
|
||||||
Have been working mostly on the website for our wedding.
|
|
||||||
It has been a lot of fun, added magic links, more interactiviy both from the guest and admin side.
|
|
||||||
Menu selections have been the most recent thing as well as linking the admin page to Mailgun so that we can send out customized emails to groups of guests.
|
|
||||||
For now that is private as it contains personal information.
|
|
||||||
I am starting to think that it would be fun to rewrite and generalize more to an event planning and coordination tool.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
There is a lot that I would do differently, and building and using it now has tought me a lot.
|
|
||||||
It is both a wonderful and terrible feeling looking back on past work and thinking that "I would do this completely differently".
|
|
||||||
It reminds me that I have learned a lot, but don't have the time currently to fully rewrite/redesign it.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Awstin
|
Awstin
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<!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="Signup">
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in a new issue