Clearing out unused login stuff

Updating Schema and Now
This commit is contained in:
Awstin 2026-01-04 10:16:01 -05:00 committed by awstin
parent c3d16a0a5d
commit 2bb6ddfe8f
17 changed files with 609 additions and 1548 deletions

1349
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

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

View file

@ -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()
}

View file

@ -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") {

View file

@ -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(())
}

View file

@ -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);
}
}
}

View file

@ -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,
}

View file

@ -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())
}
}

View file

@ -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,
}

View file

@ -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}, link::{Link, LinkType},
database::{ PsqlData,
link::{Link, LinkType},
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)

View file

@ -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 {}

View file

@ -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;

View file

@ -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 %}

View file

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

View file

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

View file

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