diff options
| author | Daniel Hader <[email protected]> | 2026-05-20 22:04:16 -0500 |
|---|---|---|
| committer | Daniel Hader <[email protected]> | 2026-05-20 22:04:16 -0500 |
| commit | 92436c8bb9eafcc56219e784f8b374edfb1907a3 (patch) | |
| tree | 45c9ebc50220414c20700e653cb17dd0c21ec00f | |
| parent | 74bc939843ae5c35fbd367c1ef0144b6074cfefe (diff) | |
basic login route with JWT
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | src/main.rs | 15 | ||||
| -rw-r--r-- | src/routes.rs | 1 | ||||
| -rw-r--r-- | src/routes/auth.rs | 66 | ||||
| -rw-r--r-- | src/routes/errors.rs | 8 | ||||
| -rw-r--r-- | static/index.html | 13 |
6 files changed, 103 insertions, 2 deletions
@@ -1 +1,3 @@ /target + +*.pem
\ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c1209bf..4daba48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::{env, fs}; + mod database; mod routes; mod auth; @@ -7,6 +9,7 @@ use axum::{ Router, }; +use jsonwebtoken::EncodingKey; use routes::problem::{get_problems, create_problem}; use routes::user::create_user; use tower_http::services::ServeDir; @@ -15,16 +18,28 @@ use crate::database::Database; #[derive(Clone)] struct AppState { + api_key: EncodingKey, database: Database, } #[tokio::main] async fn main() { + let Ok(api_key_string) = fs::read_to_string("api-key.pem") else { + eprintln!("failed to read api-key.pem"); + return; + }; + + let Ok(api_key) = EncodingKey::from_ec_pem(api_key_string.as_bytes()) else { + eprintln!("failed to decode key from api-key.pem"); + return; + }; + let database = Database::new_in_memory().unwrap(); database.initialize().unwrap(); let state = AppState { + api_key: api_key.to_owned(), database: database, }; diff --git a/src/routes.rs b/src/routes.rs index 6d7ac77..e0adba6 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,3 +1,4 @@ pub mod problem; pub mod user; +pub mod auth; mod errors; diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..0b17ef4 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,66 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::extract::{Json, State}; +use axum::response::IntoResponse; +use jsonwebtoken::{encode, Header}; +use serde::{Deserialize, Serialize}; + +use crate::AppState; +use crate::auth::{check_password}; +use super::errors::RouteError; + +#[derive(Deserialize)] +pub struct LoginRequest { + email: String, + password: String, +} + +#[derive(Serialize)] +struct LoginResponse { + token: String, +} + +#[derive(Serialize)] +struct JWTClaims { + sub: String, + exp: usize, + iat: usize, + is_admin: bool, +} + +pub async fn login( + State(state): State<AppState>, + Json(request): Json<LoginRequest> +) -> Result<impl IntoResponse, RouteError> { + + let user = match state.database.fetch_user_by_email(&request.email) { + Err(_) => return Err(RouteError::Internal("database action failed".into())), + Ok(None) => return Err(RouteError::UnregisteredEmail(request.email)), + Ok(Some(user)) => user + }; + + match check_password(&request.password, user.password_hash()) { + Err(_) => return Err(RouteError::Internal("failed to check password".into())), + Ok(false) => return Err(RouteError::AuthorizationFailure()), + Ok(true) => {}, + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| RouteError::Internal("failed to access system clock".into()))? + .as_secs() as usize; + + let claims = JWTClaims { + sub: user.email().to_owned(), + iat: now, + exp: now + 60 * 60 * 24, + is_admin: false + }; + + let token= encode(&Header::default(), &claims, &state.api_key) + .map_err(|_| RouteError::Internal("failed to encode jwt".into()))?; + + Ok(Json(LoginResponse { token })) +} + + diff --git a/src/routes/errors.rs b/src/routes/errors.rs index 6261d75..78b4e3a 100644 --- a/src/routes/errors.rs +++ b/src/routes/errors.rs @@ -5,6 +5,8 @@ pub enum RouteError { Internal(String), UserCreateEmailExists(String), UserCreateUsernameExists(String), + UnregisteredEmail(String), + AuthorizationFailure(), } impl IntoResponse for RouteError { @@ -18,6 +20,12 @@ impl IntoResponse for RouteError { }, RouteError::UserCreateUsernameExists(username) => { (StatusCode::BAD_REQUEST, format!("user with username \"{}\" already exists", username)) + }, + RouteError::UnregisteredEmail(email) => { + (StatusCode::BAD_REQUEST, format!("email \"{}\" is not registered", email)) + }, + RouteError::AuthorizationFailure() => { + (StatusCode::UNAUTHORIZED, format!("failed to authorize")) } }; diff --git a/static/index.html b/static/index.html index 8162ac9..69c19a5 100644 --- a/static/index.html +++ b/static/index.html @@ -34,14 +34,23 @@ problem_div.appendChild(description); } } + + async function on_load() { + const login_anchor = document.createElement("a"); + login_anchor.innerText = "Login / Register"; + login_anchor.href="google.com"; + document.getElementById("login-notice").appendChild(login_anchor); + + } </script> </head> - <body onload="fetch_problems()"> + <body onload="on_load()"> <div id="layout"> <div id="container"> <div id="content"> - + <span id="login-notice"></span> + <h1>C&! Code Golf Leaderboard</h1> <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> |
