From 929be68e691c1c4015fc6874111b19b9f5d68c02 Mon Sep 17 00:00:00 2001 From: Daniel Hader Date: Sat, 30 May 2026 10:10:17 -0500 Subject: registration page, me endpoint, and minor content tweaks --- src/auth.rs | 56 ----------------------------------------- src/database/sql/initialize.sql | 2 +- src/main.rs | 7 +++--- src/routes/auth.rs | 4 ++- src/routes/user.rs | 16 +++++++++--- static/default.css | 8 +++--- static/index.html | 15 +++++++---- static/login.html | 6 ++--- static/main.js | 24 +++++++++++------- static/register.html | 39 ++++++++++++++++++++++++++++ static/register.js | 50 ++++++++++++++++++++++++++++++++++++ 11 files changed, 141 insertions(+), 86 deletions(-) delete mode 100644 src/auth.rs create mode 100644 static/register.html create mode 100644 static/register.js diff --git a/src/auth.rs b/src/auth.rs deleted file mode 100644 index a621756..0000000 --- a/src/auth.rs +++ /dev/null @@ -1,56 +0,0 @@ -use axum::{extract::FromRequestParts, http::{StatusCode, request::Parts}}; -use serde::Serialize; -use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::{ - Error, PasswordHasher, SaltString, rand_core::OsRng -}}; - -pub fn hash_password(password: &str) -> Result { - let argon2 = Argon2::default(); - let salt = SaltString::generate(&mut OsRng); - Ok(argon2.hash_password(password.as_bytes(), &salt)?.to_string()) -} - -pub fn check_password(password: &str, password_hash: &str) -> Result { - let argon2 = Argon2::default(); - let hash = PasswordHash::new(password_hash)?; - Ok(argon2.verify_password(password.as_bytes(), &hash).is_ok()) -} - -#[derive(Serialize)] -pub struct Claims { - pub sub: String, - pub exp: usize, - pub iat: usize, - pub is_admin: bool, -} - -//pub fn create_jwt(email: &str, is_admin: bool) -> Result - -pub struct AuthUser(pub Claims); - -impl FromRequestParts for AuthUser { - type Rejection = StatusCode; - - async fn from_request_parts( - parts: &mut Parts, - state: &S, - ) -> Result { - todo!(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_password_hashing() { - - let passwords = vec!["password", "test1", "random"]; - - for password in &passwords { - let hash = hash_password(password).unwrap(); - assert!(check_password(password, &hash).unwrap()); - } - } -} diff --git a/src/database/sql/initialize.sql b/src/database/sql/initialize.sql index d5fb9e0..1c2a40f 100644 --- a/src/database/sql/initialize.sql +++ b/src/database/sql/initialize.sql @@ -47,7 +47,7 @@ INSERT INTO problem (title, description) VALUES ( INSERT INTO problem (title, description) VALUES ( 'As Big a Number as Possible', - 'This challenge is a bit different from the others. Instead of trying to make as small a program as possible, you are trying to make a program which prints out as big a number as possible. Your source code should be no longer than 512 bytes.' + 'This challenge is a bit different from the others. Instead of trying to make as small a program as possible, you are trying to make a program which prints out as big a number as possible in a fixed amount of code. Your source code should be no longer than 512 bytes and numbers should be printed in base 10. Before you get any funny ideas, keep in mind that your program has to finish running at some point; you can''t just print "9" in an infinite loop.' ); INSERT INTO problem (title, description) VALUES ( diff --git a/src/main.rs b/src/main.rs index 5629578..0fdd2fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,16 +2,14 @@ use std::env; mod database; mod routes; -mod auth; use axum::{ routing::{get, post}, Router, }; -use jsonwebtoken::EncodingKey; use routes::problem::{get_problems, create_problem}; -use routes::user::create_user; +use routes::user::{create_user, me}; use routes::auth::login; use tower_http::services::ServeDir; @@ -42,10 +40,11 @@ async fn main() { let static_files = ServeDir::new("./static"); let app = Router::new() + .route("/login", post(login)) .route("/problem", get(get_problems)) .route("/problem", post(create_problem)) .route("/user", post(create_user)) - .route("/login", post(login)) + .route("/me", get(me)) .nest_service("/static", static_files) .with_state(state); diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 4fb9124..979e617 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -2,7 +2,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use axum::extract::{Json, State, FromRequestParts}; use axum::response::IntoResponse; -use axum::http::{StatusCode, request::Parts}; +use axum::http::{request::Parts}; use axum_extra::extract::CookieJar; use axum_extra::extract::cookie::{Cookie, SameSite}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; @@ -21,6 +21,7 @@ pub struct Claims { pub sub: String, pub exp: usize, pub iat: usize, + pub username: String, pub is_admin: bool, } @@ -92,6 +93,7 @@ pub async fn login( sub: user.email().to_string(), iat: now, exp: now + 60 * 60 * 24, + username: user.username().to_string(), is_admin: false }; diff --git a/src/routes/user.rs b/src/routes/user.rs index f64cb39..157cf09 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -1,14 +1,14 @@ use axum::extract::{Json, State}; use axum::http::StatusCode; use axum::response::IntoResponse; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::AppState; -use crate::auth::hash_password; +use crate::routes::auth::{AuthUser, hash_password}; use super::errors::RouteError; #[derive(Deserialize)] -pub struct CreateUserRequest { +pub(crate) struct CreateUserRequest { email: String, username: String, password: String, @@ -42,3 +42,13 @@ pub async fn create_user( return Ok((StatusCode::CREATED, Json(user))); } +#[derive(Serialize)] +struct MeResponse { + username: String, +} + +pub async fn me( + AuthUser(claims): AuthUser, +) -> Result { + Ok(Json(MeResponse { username: claims.username })) +} diff --git a/static/default.css b/static/default.css index c1670c3..1cc3249 100644 --- a/static/default.css +++ b/static/default.css @@ -4,15 +4,15 @@ body { } #container { - width: 800px; + width: 640px; margin: auto; - padding: 20px; + padding: 30px; background: rgb(255,255,200); } .problem { border: 1px solid black; - padding: 0 8px; - margin: 8px; + padding: 0 10px; + margin: 8px 0; } diff --git a/static/index.html b/static/index.html index 3fd75e3..0cc6ec9 100644 --- a/static/index.html +++ b/static/index.html @@ -12,12 +12,17 @@
- Login + +

C&! Code Golf Leaderboard

+ +

In normal golf, the goal is to get a ball into a hole in as few swings as possible. Code golf is similar, but instead of swings your success is measured in bytes of source code. The following page contains a list of programming challenges. Your task is, first and foremost, to try to solve them, but secondly you should try to do so with as short a program as possible.

+

There are no strict rules, choose whatever language you'd like and have fun with it. The goal is to get you coding and thinking. Some of the problems are challenging, especially the last few. Good luck!o

-

In golf, the goal is to get a ball into a hole in as few swings as possible. The goal of code golf is similarly to solve a problem in as few bytes (of source code) as possible. The following is a list of programming challenges. Your task is to try and solve them in Python with as little code as possible measured in bytes.

- +

Problems

@@ -26,8 +31,8 @@
- - +

This leaderboard is lovingly handcrafted by Daniel Hader. The source code is available here and I encourage you to explore it. There's certainly all sorts of ways to break it and it's hosted on a server that I pay for. Please be respectful to me and the other campers using it. Thanks!

+
© 2026 Daniel Hader
diff --git a/static/login.html b/static/login.html index c6cd992..ba7e127 100644 --- a/static/login.html +++ b/static/login.html @@ -14,15 +14,15 @@

C&! Code Golf Leaderboard

Login

-
+ -
+


-
+


diff --git a/static/main.js b/static/main.js index f18cbef..da3a9d3 100644 --- a/static/main.js +++ b/static/main.js @@ -1,5 +1,17 @@ -async function login() { - +async function me() { + const response = await fetch("/me"); + if (response.ok) { + const result = await response.json(); + const span = document.getElementById("logged-in"); + // this is technically a XSS risk (TODO: deal with it) + // in principle it only affects the person who chose their username, but... + span.innerHTML = `Logged in as ${result.username}`; + span.hidden = false; + } else { + document.getElementById("login-links").hidden = false; + console.log("not logged in? No cookie"); + console.log(response); + } } async function fetch_problems() { @@ -9,7 +21,6 @@ async function fetch_problems() { } const result = await response.json(); - console.log(result); const problems_div = document.getElementById("problems"); while (problems_div.firstChild) { @@ -32,12 +43,7 @@ async function fetch_problems() { } async function on_load() { + await me(); await fetch_problems(); - - const login_anchor = document.createElement("a"); - login_anchor.innerText = "Login / Register"; - login_anchor.href="google.com"; - document.getElementById("login-notice").appendChild(login_anchor); - } diff --git a/static/register.html b/static/register.html new file mode 100644 index 0000000..5d769a2 --- /dev/null +++ b/static/register.html @@ -0,0 +1,39 @@ + + + + + + Code Golf Login + + + + + +
+
+
+

C&! Code Golf Leaderboard

+

Register

+ + +
+ +

+
+ +

+
+ +

+ + + +
© 2026 Daniel Hader
+
+
+
+ + diff --git a/static/register.js b/static/register.js new file mode 100644 index 0000000..09175d6 --- /dev/null +++ b/static/register.js @@ -0,0 +1,50 @@ + +function display_error(message) { + document.getElementById("error-message").innerHTML = `Error: ${message}`; + document.getElementById("error").hidden = false; +} + +function init() { + const form = document.getElementById("register-form"); + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const body = {} + new FormData(form).forEach((value, key) => body[key] = value); + + try { + console.log(); + + const reg_res = await fetch("/user", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + + if (!reg_res.ok) { + const error = await reg_res.json(); + display_error(error.error); + return; + } + + const log_res = await fetch("/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(body) + }); + + if (!log_res.ok) { + const error = await log_res.json(); + display_error(error.error); + return; + } + + window.location.href = "index.html" + + } catch (err) { + console.log(err); + display_error("network error"); + } + }); +} -- cgit v1.2.3