diff options
| author | Daniel Hader <[email protected]> | 2026-05-29 18:14:31 -0500 |
|---|---|---|
| committer | Daniel Hader <[email protected]> | 2026-05-29 18:14:31 -0500 |
| commit | 772c7844c4ca1de632f64eb9428e8e97eea64ac1 (patch) | |
| tree | 2b038268f0d15a02830f4c4e465a323f43ce3c35 | |
| parent | 334867ba0732f85a48ad88ef8f3201c10bc1da4e (diff) | |
login page interaction with server
| -rw-r--r-- | Cargo.lock | 35 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/auth.rs | 25 | ||||
| -rw-r--r-- | src/main.rs | 9 | ||||
| -rw-r--r-- | src/routes/auth.rs | 104 | ||||
| -rw-r--r-- | static/login.html | 18 | ||||
| -rw-r--r-- | static/login.js | 38 | ||||
| -rw-r--r-- | static/main.js | 3 |
8 files changed, 196 insertions, 38 deletions
@@ -85,6 +85,28 @@ dependencies = [ ] [[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -171,11 +193,13 @@ version = "0.1.0" dependencies = [ "argon2", "axum", + "axum-extra", "jsonwebtoken", "r2d2", "r2d2_sqlite", "serde", "serde_json", + "time", "tokio", "tower-http", ] @@ -187,6 +211,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,10 +6,12 @@ edition = "2024" [dependencies] argon2 = { version = "0.5.3", features = ["std"] } axum = "0.8.9" +axum-extra = { version = "0.12.6", features = ["cookie"] } jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } r2d2 = "0.8.10" r2d2_sqlite = "0.34.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" +time = "0.3.47" tokio = { version = "1.52.2", features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.6.10", features = ["fs"] } diff --git a/src/auth.rs b/src/auth.rs index 90ce4a5..a621756 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,3 +1,5 @@ +use axum::{extract::FromRequestParts, http::{StatusCode, request::Parts}}; +use serde::Serialize; use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::{ Error, PasswordHasher, SaltString, rand_core::OsRng }}; @@ -14,6 +16,29 @@ pub fn check_password(password: &str, password_hash: &str) -> Result<bool, Error 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::*; diff --git a/src/main.rs b/src/main.rs index 511a8f2..5629578 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{env, fs}; +use std::env; mod database; mod routes; @@ -19,7 +19,7 @@ use crate::database::Database; #[derive(Clone)] struct AppState { - api_key: EncodingKey, + secret: String, database: Database, } @@ -31,14 +31,11 @@ async fn main() { return; }; - let api_key = EncodingKey::from_secret(secret.as_ref()); - - let database = Database::new_in_memory().unwrap(); database.initialize().unwrap(); let state = AppState { - api_key: api_key.to_owned(), + secret: secret, database: database, }; diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 64ada2b..4fb9124 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,34 +1,72 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use axum::extract::{Json, State}; +use axum::extract::{Json, State, FromRequestParts}; use axum::response::IntoResponse; -use jsonwebtoken::{encode, Header}; +use axum::http::{StatusCode, request::Parts}; +use axum_extra::extract::CookieJar; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::{Deserialize, Serialize}; +use time::Duration; + +use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::{ + Error, PasswordHasher, SaltString, rand_core::OsRng +}}; use crate::AppState; -use crate::auth::{check_password}; -use super::errors::RouteError; +use crate::routes::errors::RouteError; -#[derive(Deserialize)] -pub struct LoginRequest { - email: String, - password: String, +#[derive(Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, + pub iat: usize, + pub is_admin: bool, } -#[derive(Serialize)] -struct LoginResponse { - token: String, +pub struct AuthUser(pub Claims); + +impl FromRequestParts<AppState> for AuthUser { + type Rejection = RouteError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result<Self, Self::Rejection> { + let jar = CookieJar::from_request_parts(parts, state).await.unwrap(); + + let token = jar.get("token") + .map(|c| c.value().to_string()) + .ok_or(RouteError::AuthorizationFailure())?; + + let key = DecodingKey::from_secret(state.secret.as_ref()); + let data = decode::<Claims>(token, &key, &Validation::default()) + .map_err(|_| RouteError::AuthorizationFailure())?; + + Ok(AuthUser(data.claims)) + } } -#[derive(Serialize)] -struct JWTClaims { - sub: String, - exp: usize, - iat: usize, - is_admin: bool, +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(Deserialize)] +pub struct LoginRequest { + email: String, + password: String, } pub async fn login( + jar: CookieJar, State(state): State<AppState>, Json(request): Json<LoginRequest> ) -> Result<impl IntoResponse, RouteError> { @@ -50,17 +88,39 @@ pub async fn login( .map_err(|_| RouteError::Internal("failed to access system clock".into()))? .as_secs() as usize; - let claims = JWTClaims { - sub: user.email().to_owned(), + let claims = Claims { + sub: user.email().to_string(), iat: now, exp: now + 60 * 60 * 24, is_admin: false }; - - let token= encode(&Header::default(), &claims, &state.api_key) + + let key = EncodingKey::from_secret(state.secret.as_ref()); + let token = encode(&Header::default(), &claims, &key) .map_err(|e| RouteError::Internal(format!("failed to encode jwt: {e}")))?; - Ok(Json(LoginResponse { token })) + let cookie = Cookie::build(("token", token)) + .path("/") + .max_age(Duration::hours(24)) + .same_site(SameSite::Strict) + .http_only(true) + .build(); + + Ok(jar.add(cookie)) } +#[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/static/login.html b/static/login.html index 37035dc..c6cd992 100644 --- a/static/login.html +++ b/static/login.html @@ -5,21 +5,25 @@ <meta charset="utf-8" /> <title>Code Golf Login</title> <link rel="stylesheet" href="default.css"> - <script type="text/javascript" src="main.js"></script> + <script type="text/javascript" src="login.js"></script> </head> - <body> + <body onload="init()"> <div id="layout"> <div id="container"> <div id="content"> <h1>C&! Code Golf Leaderboard</h1> <h2>Login</h2> - <form id="login-form" action="" method="get" onsubmit="login()"> - <label for="login-email">Email</label><br> - <input type="text" id="login-email" name="login-email"> + <form id="login-form" method="POST" action="/login"> + <div id="error" hidden> + <span id="error-message" style="color: red"></span> + <br><br> + </div> + <label for="email">Email</label><br> + <input type="text" id="login-email" name="email"> <br><br> - <label for="login-password">Password</label><br> - <input type="password" id="login-password" name="login-password"> + <label for="password">Password</label><br> + <input type="password" id="login-password" name="password"> <br><br> <input type="submit" value="Login"> </form> diff --git a/static/login.js b/static/login.js new file mode 100644 index 0000000..ee135f8 --- /dev/null +++ b/static/login.js @@ -0,0 +1,38 @@ + +function display_error(message) { + document.getElementById("error-message").innerHTML = `Error: ${message}`; + document.getElementById("error").hidden = false; +} + +function init() { + const form = document.getElementById("login-form"); + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const body = {} + new FormData(form).forEach((value, key) => body[key] = value); + + try { + console.log(); + + const res = await fetch("/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(body) + }); + + if (!res.ok) { + const error = await res.json(); + display_error(error.error); + return; + } + + //const result = await res.json(); + + } catch (err) { + console.log(err); + //display_error("network error"); + } + }); +} diff --git a/static/main.js b/static/main.js index 2bde100..f18cbef 100644 --- a/static/main.js +++ b/static/main.js @@ -41,6 +41,3 @@ async function on_load() { } -async function login() { - console.log("login pressed"); -} |
