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]
|
||||
askama = "0.12.1"
|
||||
axum = "0.6"
|
||||
bb8 = "0.8.3"
|
||||
chrono = { version = "0.4.38", features = ["alloc", "serde"] }
|
||||
clap = { version = "4.5.13", features = ["derive"] }
|
||||
cookie = "0.18.1"
|
||||
dotenv = "0.15.0"
|
||||
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 = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"chrono",
|
||||
"macros",
|
||||
"postgres",
|
||||
|
|
|
|||
14
schema.sql
14
schema.sql
|
|
@ -1,9 +1,9 @@
|
|||
DO $$
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TYPE link_type as ENUM ('article', 'blog');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
|
|
@ -24,13 +24,3 @@ CREATE TABLE IF NOT EXISTS links (
|
|||
author varchar(50) not null,
|
||||
link_type link_type not null,
|
||||
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 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") {
|
||||
|
|
|
|||
|
|
@ -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 projects;
|
||||
pub mod books;
|
||||
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,
|
||||
}
|
||||
|
||||
#[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::{
|
||||
http::{Response, StatusCode},
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post, Router},
|
||||
routing::{get, Router},
|
||||
Extension,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rand_core::OsRng;
|
||||
use sqlx::PgPool;
|
||||
use std::error::Error;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::{
|
||||
auth::{auth, logout_response, post_login, post_signup},
|
||||
database::{
|
||||
link::{Link, LinkType},
|
||||
PsqlData,
|
||||
},
|
||||
use crate::database::{
|
||||
link::{Link, LinkType},
|
||||
PsqlData,
|
||||
};
|
||||
|
||||
use super::{
|
||||
admin, books,
|
||||
books,
|
||||
posts::{self, get_articles_date_sorted},
|
||||
projects,
|
||||
templates::{
|
||||
AboutTemplate, AiTemplate, BlogrollTemplate, ContactTemplate, CookingTemplate,
|
||||
CreationTemplate, GiftsTemplate, HomeTemplate, HtmlTemplate, InterestsTemplate,
|
||||
LinksPageTemplate, LoginTemplate, MoneyTemplate, NowTemplate, SignupTemplate,
|
||||
TechnologyTemplate, TimeTemplate, UsesTemplate,
|
||||
LinksPageTemplate, MoneyTemplate, NowTemplate, TechnologyTemplate, TimeTemplate,
|
||||
UsesTemplate,
|
||||
},
|
||||
};
|
||||
|
||||
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("/posts", posts::get_router())
|
||||
.nest("/projects", projects::get_router())
|
||||
.nest("/books", books::get_router())
|
||||
.nest("/admin", admin::get_router())
|
||||
.nest_service(
|
||||
"/assets",
|
||||
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("/technology", get(technology))
|
||||
.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(
|
||||
"/robots.txt",
|
||||
get(|| async { Redirect::permanent("/assets/robots.txt") }),
|
||||
|
|
@ -74,11 +58,7 @@ pub fn get_router(pool: PgPool) -> Router {
|
|||
"/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(Arc::new(Mutex::new(random))))
|
||||
}
|
||||
|
||||
async fn home(Extension(pool): Extension<PgPool>) -> impl IntoResponse {
|
||||
|
|
@ -155,21 +135,6 @@ pub async fn get_links_as_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 {
|
||||
let time_page = TimeTemplate {};
|
||||
HtmlTemplate(time_page)
|
||||
|
|
|
|||
|
|
@ -36,12 +36,6 @@ pub struct ArticleTemplate {
|
|||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "page.html")]
|
||||
pub struct PageTemplate {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "links.html")]
|
||||
pub struct LinksPageTemplate {
|
||||
|
|
@ -133,32 +127,10 @@ pub struct PostFooterTemplate {
|
|||
pub next: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "resume.html")]
|
||||
pub struct ResumeTemplate {}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "work.html")]
|
||||
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 {}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "money.html")]
|
||||
pub struct MoneyTemplate {}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<p>
|
||||
Last updated: 2025-04-08
|
||||
</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.
|
||||
Last updated: 2026-01-04
|
||||
</p>
|
||||
<p>
|
||||
Still keeping my eyes open for other opportunities.
|
||||
</p>
|
||||
<h2>Brazilian Jiu-Jitsu</h2>
|
||||
<p>
|
||||
The team here at GB Toronto has become an external family.
|
||||
I am so grateful for the community.
|
||||
A lot has changed in the last year,
|
||||
got married,
|
||||
welcomed 2 new people into the world, one from family and one from one of my best friends,
|
||||
traveled to a half dozen countries.
|
||||
</p>
|
||||
<p>
|
||||
Training has lightened up post competition.
|
||||
The competition did not go very well but that happens and I learned a lot.
|
||||
Now enjoying having a break from the stress of competition prep.
|
||||
</p>
|
||||
<h2>Learning</h2>
|
||||
<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.
|
||||
I am still training BJJ, thought not as intensely as before.
|
||||
Other priorities in life, it still keeps me sane and gives me a reason to stay in shape.
|
||||
But it has not been the centre of my attention that it was.
|
||||
Has been nice to transition from obsession to joy and comfort.
|
||||
I will try and get a couple competitions in this year as normal.
|
||||
</p>
|
||||
<p>
|
||||
Starting to learn <a href="go.dev">Go</a>.
|
||||
A very simple and concise language.
|
||||
Both similar in ways to Rust (focus on performance, strong type system), and completely different (still a runtime, much simpler).
|
||||
Enjoying it so far, still very early days.
|
||||
My own projects have been languishing for a while.
|
||||
I don't have that much of a drive after a day at work, hoping to change that this year.
|
||||
Currently setting up a used mini PC as a home server, learning about FreeBSD and virtualization.
|
||||
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>
|
||||
Joined the <a href="https://leanwebclub.com">Lean Web Club</a> course for web development.
|
||||
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.
|
||||
Figured it was time to learn all the stuff that browsers are capable of and how to make the best use of them.
|
||||
They have come a long way and there is so much that you can do with just plain JS and no external dependencies.
|
||||
</p>
|
||||
<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.
|
||||
Also have a long list of stuff to build around the apartment.
|
||||
More storage planned out to use the space more efficiently, get some more art and things mounted on the walls.
|
||||
In the design phase, sketching out desks, cabinets, shelving, and working out the amount of material that I am going to need.
|
||||
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.
|
||||
I have not built anything physical for a bit and am looking forward to it.
|
||||
</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>
|
||||
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