summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Hader <[email protected]>2026-05-20 22:04:16 -0500
committerDaniel Hader <[email protected]>2026-05-20 22:04:16 -0500
commit92436c8bb9eafcc56219e784f8b374edfb1907a3 (patch)
tree45c9ebc50220414c20700e653cb17dd0c21ec00f /src
parent74bc939843ae5c35fbd367c1ef0144b6074cfefe (diff)
basic login route with JWT
Diffstat (limited to 'src')
-rw-r--r--src/main.rs15
-rw-r--r--src/routes.rs1
-rw-r--r--src/routes/auth.rs66
-rw-r--r--src/routes/errors.rs8
4 files changed, 90 insertions, 0 deletions
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"))
}
};