summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Hader <[email protected]>2026-05-29 18:14:31 -0500
committerDaniel Hader <[email protected]>2026-05-29 18:14:31 -0500
commit772c7844c4ca1de632f64eb9428e8e97eea64ac1 (patch)
tree2b038268f0d15a02830f4c4e465a323f43ce3c35 /src
parent334867ba0732f85a48ad88ef8f3201c10bc1da4e (diff)
login page interaction with server
Diffstat (limited to 'src')
-rw-r--r--src/auth.rs25
-rw-r--r--src/main.rs9
-rw-r--r--src/routes/auth.rs104
3 files changed, 110 insertions, 28 deletions
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());
+ }
+ }
+}