feat(backend): replace auth stub with real session, create users and sessions tables
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user