feat(backend): replace auth stub with real session, create users and sessions tables

This commit is contained in:
2026-05-26 02:34:23 +03:00
parent a54ec4ed34
commit e7eaa42064
11 changed files with 636 additions and 135 deletions

View File

@@ -1,41 +1,103 @@
use axum::{
Json, Router,
routing::{get, post},
use argon2::{
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
password_hash::{SaltString, rand_core::OsRng},
};
use axum::{Json, Router, extract::State, http::StatusCode, routing::post};
use rand::RngExt;
use serde::Deserialize;
use serde_json::{Value, json};
use sqlx::PgPool;
use tower_cookies::{Cookie, Cookies};
use crate::{error::LoftError, web::AUTH_TOKEN};
pub fn routes_login() -> Router {
use crate::{error::LoftError, model::UserRepository, web::AUTH_TOKEN};
pub fn routes_auth(user_repository: UserRepository) -> Router {
Router::new()
.route("/login", post(login))
.route("/register", get(register))
.route("/logout", post(logout))
.route("/register", post(register))
.with_state(user_repository)
}
async fn login(
State(user_repository): State<UserRepository>,
cookies: Cookies,
Json(payload): Json<LoginPayload>,
) -> Result<Json<Value>, LoftError> {
//TODO: real db/auth logic
if payload.username != "x" || payload.password != "y" {
) -> Result<StatusCode, LoftError> {
let user = user_repository.find_by_username(payload.username).await?;
// TODO: replace unwrap with ?
let parsed_hash = PasswordHash::new(&user.password_hash).unwrap();
if Argon2::default()
.verify_password(payload.password.as_bytes(), &parsed_hash)
.is_ok()
{
let expires_at = chrono::Utc::now() + chrono::Duration::days(1);
let cookie = create_cookie();
let auth_token = cookie.value().to_string();
cookies.add(cookie);
user_repository
.create_session(user.id, auth_token, expires_at)
.await?;
} else {
return Err(LoftError::LoginFail);
}
// FIXME: real auth-token generation-signature
cookies.add(Cookie::new(AUTH_TOKEN, "user-1.exp.sign"));
let body = Json(json!({
"result": {
"success": true
}
}));
Ok(body)
Ok(StatusCode::OK)
}
async fn register() -> &'static str {
"register"
async fn logout(
State(user_repository): State<UserRepository>,
cookies: Cookies,
) -> Result<StatusCode, LoftError> {
let auth_token = cookies.get(AUTH_TOKEN).map(|c| c.value().to_string());
if let Some(auth_token) = auth_token {
user_repository.delete_session(auth_token.clone()).await?;
cookies.remove(Cookie::build(AUTH_TOKEN).path("/").build());
}
Ok(StatusCode::OK)
}
async fn register(
State(user_repository): State<UserRepository>,
Json(payload): Json<LoginPayload>,
) -> Result<StatusCode, LoftError> {
if user_repository
.find_by_username(payload.username.clone())
.await
.is_ok()
{
return Err(LoftError::RegisterFail);
}
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
// TODO: replace unwrap
let password_hash = argon2
.hash_password(payload.password.as_bytes(), &salt)
.unwrap()
.to_string();
user_repository
.create_user(payload.username, password_hash)
.await?;
Ok(StatusCode::CREATED)
}
fn create_cookie() -> Cookie<'static> {
let auth_token: String = rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(64)
.map(char::from)
.collect();
Cookie::build((AUTH_TOKEN, auth_token))
.http_only(true)
.same_site(tower_cookies::cookie::SameSite::Lax)
.path("/")
.secure(std::env::var("ENVIRONMENT").unwrap_or_default() == "prod")
.build()
}
#[derive(Debug, Deserialize)]
@@ -46,23 +108,54 @@ struct LoginPayload {
#[cfg(test)]
mod tests {
use axum::{
Router,
routing::{get, post},
};
use axum::{Router, http::StatusCode};
use axum_test::TestServer;
use serde_json::json;
use sqlx::PgPool;
use crate::web::routes_login::{login, register};
use crate::{
model::UserRepository,
web::{AUTH_TOKEN, routes_login::routes_auth},
};
async fn truncate_users(pool: &PgPool) {
sqlx::query!("TRUNCATE TABLE users CASCADE")
.execute(pool)
.await
.unwrap();
}
async fn truncate_sessions(pool: &PgPool) {
sqlx::query!("TRUNCATE TABLE sessions")
.execute(pool)
.await
.unwrap();
}
async fn user_repository() -> UserRepository {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPool::connect(&database_url).await.unwrap();
let user_repository = UserRepository::new(pool).unwrap();
user_repository
}
async fn test_server() -> TestServer {
let user_repository = user_repository().await;
let routes_auth = routes_auth(user_repository);
let app = Router::new()
.nest("/api/auth", routes_auth)
.layer(tower_cookies::CookieManagerLayer::new());
TestServer::new(app)
}
#[tokio::test]
#[serial_test::serial]
async fn test_routes_login_wrong_credentials() {
let app = Router::new()
.route(&"/login", post(login))
.layer(tower_cookies::CookieManagerLayer::new());
let server = TestServer::new(app);
let server = test_server().await;
let response = server
.post("/login")
.post("/api/auth/login")
.json(&json!({
"username": "wrong",
"password": "wrong",
@@ -72,30 +165,74 @@ mod tests {
}
#[tokio::test]
#[serial_test::serial]
async fn test_routes_login() {
let app = Router::new()
.route(&"/login", post(login))
.layer(tower_cookies::CookieManagerLayer::new());
let server = TestServer::new(app);
let user_repository = user_repository().await;
truncate_sessions(&user_repository.pool).await;
truncate_users(&user_repository.pool).await;
let server = test_server().await;
server
.post("/api/auth/register")
.json(&json!({ "username": "picolo", "password": "picolo" }))
.await;
let response = server
.post("/login")
.post("/api/auth/login")
.json(&json!({
"username": "x",
"password": "y",
"username": "picolo",
"password": "picolo",
}))
.await;
response.assert_status_ok().assert_json(&json!({
"result": {
"success": true
}
}));
response.assert_status_ok();
}
#[tokio::test]
#[serial_test::serial]
async fn test_routes_logout() {
let user_repository = user_repository().await;
truncate_sessions(&user_repository.pool).await;
truncate_users(&user_repository.pool).await;
let server = test_server().await;
let register_response = server
.post("/api/auth/register")
.json(&json!({ "username": "picolo", "password": "picolo" }))
.await;
register_response.assert_status(StatusCode::CREATED);
let login_response = server
.post("/api/auth/login")
.json(&json!({
"username": "picolo",
"password": "picolo",
}))
.await;
login_response.assert_status_ok();
let cookie = login_response.cookie(AUTH_TOKEN);
let logout_response = server.post("/api/auth/logout").add_cookie(cookie).await;
logout_response.assert_status_ok();
}
#[tokio::test]
#[serial_test::serial]
async fn test_routes_register() {
let app = Router::new().route(&"/register", get(register));
let server = TestServer::new(app);
let response = server.get("/register").await;
response.assert_status_ok().assert_text("register");
let user_repository = user_repository().await;
truncate_sessions(&user_repository.pool).await;
truncate_users(&user_repository.pool).await;
let server = test_server().await;
let register_response = server
.post("/api/auth/register")
.json(&json!({ "username": "picolo", "password": "picolo" }))
.await;
register_response.assert_status(StatusCode::CREATED);
}
}