diff options
| -rw-r--r-- | src/auth.rs | 56 | ||||
| -rw-r--r-- | src/database/sql/initialize.sql | 2 | ||||
| -rw-r--r-- | src/main.rs | 7 | ||||
| -rw-r--r-- | src/routes/auth.rs | 4 | ||||
| -rw-r--r-- | src/routes/user.rs | 16 | ||||
| -rw-r--r-- | static/default.css | 8 | ||||
| -rw-r--r-- | static/index.html | 15 | ||||
| -rw-r--r-- | static/login.html | 6 | ||||
| -rw-r--r-- | static/main.js | 24 | ||||
| -rw-r--r-- | static/register.html | 39 | ||||
| -rw-r--r-- | static/register.js | 50 |
11 files changed, 141 insertions, 86 deletions
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<String, Error> { - 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<bool, Error> { - 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<S: Send + Sync> FromRequestParts<S> for AuthUser { - type Rejection = StatusCode; - - async fn from_request_parts( - parts: &mut Parts, - state: &S, - ) -> Result<Self, Self::Rejection> { - 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<impl IntoResponse, RouteError> { + 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 @@ <div id="layout"> <div id="container"> <div id="content"> - <a href="login.html">Login</a> + <div id="login-links" hidden> + <a href="login.html">Login</a> / <a href="register.html">Register</a> + </div> + <span id="logged-in" hidden></span> <h1>C&! Code Golf Leaderboard</h1> + + <p>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.</p> + <p>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</p> - <p>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.</p> - + <h2>Problems</h2> <div id="problems"> <div class="problem"> @@ -26,8 +31,8 @@ </div> </div> - <button onclick="fetch_problems()">Refresh Problems</button> - + <p>This leaderboard is lovingly handcrafted by Daniel Hader. The source code is available <a href="https://git.danielhader.net/code_golf.git">here</a> 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!</p> + <center>© 2026 Daniel Hader</center> </div> </div> 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 @@ <div id="content"> <h1>C&! Code Golf Leaderboard</h1> <h2>Login</h2> - <form id="login-form" method="POST" action="/login"> + <form id="login-form"> <div id="error" hidden> <span id="error-message" style="color: red"></span> <br><br> </div> - <label for="email">Email</label><br> + <label for="login-email">Email</label><br> <input type="text" id="login-email" name="email"> <br><br> - <label for="password">Password</label><br> + <label for="login-password">Password</label><br> <input type="password" id="login-password" name="password"> <br><br> <input type="submit" value="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 @@ +<!doctype html> + +<html> + <head> + <meta charset="utf-8" /> + <title>Code Golf Login</title> + <link rel="stylesheet" href="default.css"> + <script type="text/javascript" src="register.js"></script> + </head> + + <body onload="init()"> + <div id="layout"> + <div id="container"> + <div id="content"> + <h1>C&! Code Golf Leaderboard</h1> + <h2>Register</h2> + <form id="register-form"> + <div id="error" hidden> + <span id="error-message" style="color: red"></span> + <br><br> + </div> + <label for="register-email">Email</label><br> + <input type="text" id="register-email" name="email"> + <br><br> + <label for="register-username">Username</label><br> + <input type="text" id="register-username" name="username"> + <br><br> + <label for="register-password">Password</label><br> + <input type="password" id="register-password" name="password"> + <br><br> + <input type="submit" value="Register"> + </form> + + <center>© 2026 Daniel Hader</center> + </div> + </div> + </div> + </body> +</html> 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"); + } + }); +} |
