diff --git a/src/auth.rs b/src/auth.rs index ed702dd..e629ea6 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,4 +1,5 @@ -use actix_web::{post, web, HttpResponse, Responder}; +use actix_web::{get, post, web, HttpResponse, Responder, HttpRequest, cookie::{Cookie, SameSite}}; +use serde_json::json; use crate::models::{RegistrationData, LoginData, LoginResponse}; use crate::error::AppError; use bcrypt::{hash, verify, DEFAULT_COST}; @@ -56,13 +57,100 @@ pub async fn login( .ok_or_else(|| AppError::Unauthorized("Nieprawidłowe dane".to_string()))?; if verify(&form.haslo, &user.haslo).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))? { - let dummy_token = format!("user-{}-token", user.id); - Ok(HttpResponse::Ok().json(LoginResponse { - token: dummy_token, - imie: user.imie, - })) + let token = format!("user-{}-token", user.id); + + // Utwórz ciasteczko + let cookie = Cookie::build("auth_token", &token) + .path("/") + .max_age(actix_web::cookie::time::Duration::days(7)) + .http_only(true) + .same_site(SameSite::Lax) + .finish(); + + let mut response = HttpResponse::Ok() + .json(LoginResponse { + token: token.clone(), + imie: user.imie, + }); + + // Poprawiona obsługa błędów dla add_cookie + response.add_cookie(&cookie) + .map_err(|e| { + log::error!("Błąd ustawiania ciasteczka: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; + + Ok(response) } else { Err(AppError::Unauthorized("Nieprawidłowe hasło".to_string())) } } +#[post("/logout")] +pub async fn logout() -> impl Responder { + // Utwórz ciasteczko z datą wygaśnięcia w przeszłości + let cookie = Cookie::build("auth_token", "") + .path("/") + .max_age(actix_web::cookie::time::Duration::seconds(0)) + .http_only(true) + .finish(); + + let mut response = HttpResponse::Ok().json(json!({"status": "success"})); + + if let Err(e) = response.add_cookie(&cookie) { + log::error!("Błąd usuwania ciasteczka: {}", e); + } + + response +} + +pub async fn validate_token(req: &HttpRequest) -> Result { + // Pobierz ciasteczko + let cookie = req.cookie("auth_token") + .ok_or_else(|| AppError::Unauthorized("Unauthorized".to_string()))?; + + let token = cookie.value(); + + if token.starts_with("user-") { + let user_id = token.replace("user-", "").replace("-token", "").parse() + .map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?; + Ok(user_id) + } else { + Err(AppError::Unauthorized("Unauthorized".to_string())) + } +} + +#[get("/api/check-auth")] +pub async fn check_auth( + req: HttpRequest, + pool: web::Data, +) -> impl Responder { + let user_id = validate_token(&req).await; + + match user_id { + Ok(user_id) => { + match sqlx::query!( + "SELECT imie FROM uzytkownicy WHERE id = $1", + user_id + ) + .fetch_one(pool.get_ref()) + .await { + Ok(user) => HttpResponse::Ok().json(json!({ + "authenticated": true, + "user": { + "id": user_id, + "imie": user.imie + } + })), + Err(_) => HttpResponse::Ok().json(json!({ + "authenticated": false, + "user": null + })) + } + }, + Err(_) => HttpResponse::Ok().json(json!({ + "authenticated": false, + "user": null + })) + } +} diff --git a/src/cart.rs b/src/cart.rs index c20a4af..7692f4c 100644 --- a/src/cart.rs +++ b/src/cart.rs @@ -1,31 +1,19 @@ use actix_web::{get, post, delete, web, HttpResponse, HttpRequest, Responder}; -use crate::models::{CartItem, CartItemResponse}; +use crate::models::{CartItem, CartItemResponse, CheckoutRequest}; use crate::error::AppError; use sqlx::PgPool; - -async fn validate_token(token: Option<&str>) -> Result { - let raw_token = token.ok_or(AppError::Unauthorized("Unauthorized".to_string()))?; - let token = raw_token.trim_start_matches("Bearer ").trim(); - - if token.starts_with("user-") { - let user_id = token.replace("user-", "").replace("-token", "").parse() - .map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?; - Ok(user_id) - } else { - Err(AppError::Unauthorized("Unauthorized".to_string())) - } -} - -fn get_token(req: &HttpRequest) -> Option<&str> { - req.headers().get("Authorization").and_then(|h| h.to_str().ok()) -} +use crate::auth::validate_token; +use bigdecimal::BigDecimal; +use serde_json::json; +use log; +use std::str::FromStr; // Dodane #[get("/api/cart")] pub async fn get_cart( req: HttpRequest, pool: web::Data, ) -> Result { - let user_id = validate_token(get_token(&req)).await?; + let user_id = validate_token(&req).await?; let cart_items = sqlx::query_as!( CartItemResponse, @@ -44,7 +32,10 @@ pub async fn get_cart( ) .fetch_all(pool.get_ref()) .await - .map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + .map_err(|e| { + log::error!("Błąd bazy danych: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; Ok(HttpResponse::Ok().json(cart_items)) } @@ -55,8 +46,7 @@ pub async fn add_to_cart( req: HttpRequest, pool: web::Data, ) -> Result { - let token = get_token(&req); - let user_id = validate_token(token).await?; + let user_id = validate_token(&req).await?; sqlx::query!( "INSERT INTO koszyk (user_id, book_id, quantity) @@ -69,9 +59,12 @@ pub async fn add_to_cart( ) .execute(pool.get_ref()) .await - .map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + .map_err(|e| { + log::error!("Błąd bazy danych: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; - Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))) + Ok(HttpResponse::Ok().json(json!({"status": "success"}))) } #[delete("/api/remove-from-cart/{book_id}")] @@ -80,7 +73,7 @@ pub async fn remove_from_cart( pool: web::Data, book_id: web::Path, ) -> Result { - let user_id = validate_token(get_token(&req)).await?; + let user_id = validate_token(&req).await?; let book_id = book_id.into_inner(); sqlx::query!( @@ -90,8 +83,93 @@ pub async fn remove_from_cart( ) .execute(pool.get_ref()) .await - .map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + .map_err(|e| { + log::error!("Błąd bazy danych: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; - Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))) + Ok(HttpResponse::Ok().json(json!({"status": "success"}))) } +#[post("/api/checkout")] +pub async fn checkout( + req: HttpRequest, + pool: web::Data, + data: web::Json, +) -> Result { + let user_id = validate_token(&req).await?; + + let mut transaction = pool.begin().await + .map_err(|e| { + log::error!("Błąd rozpoczynania transakcji: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; + + // Konwersja f64 na BigDecimal + let total_str = format!("{:.2}", data.total); + let total_bigdecimal = BigDecimal::from_str(&total_str) + .map_err(|_| AppError::BadRequest("Invalid total value".to_string()))?; + + // 1. Utwórz zamówienie + let order_id = sqlx::query!( + "INSERT INTO zamowienia (user_id, suma_totalna) + VALUES ($1, $2) RETURNING id", + user_id, + total_bigdecimal + ) + .fetch_one(&mut *transaction) + .await + .map_err(|e| { + log::error!("Błąd tworzenia zamówienia: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })? + .id; + + // 2. Dodaj pozycje zamówienia + for item in &data.items { + let book = sqlx::query!( + "SELECT cena FROM ksiazki WHERE id = $1", + item.book_id + ) + .fetch_one(&mut *transaction) + .await + .map_err(|e| { + log::error!("Błąd wyszukiwania książki: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; + + sqlx::query!( + "INSERT INTO pozycje_zamowienia (zamowienie_id, book_id, ilosc, cena) + VALUES ($1, $2, $3, $4)", + order_id, + item.book_id, + item.quantity, + book.cena + ) + .execute(&mut *transaction) + .await + .map_err(|e| { + log::error!("Błąd dodawania pozycji zamówienia: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; + } + + // 3. Wyczyść koszyk + sqlx::query!( + "DELETE FROM koszyk WHERE user_id = $1", + user_id + ) + .execute(&mut *transaction) + .await + .map_err(|e| { + log::error!("Błąd czyszczenia koszyka: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; + + transaction.commit().await.map_err(|e| { + log::error!("Błąd zatwierdzania transakcji: {}", e); + AppError::InternalServerError("Błąd serwera".to_string()) + })?; + + Ok(HttpResponse::Ok().json(json!({"status": "success"}))) +} diff --git a/src/main.rs b/src/main.rs index bfdcfc2..a05808c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use dotenv::dotenv; use env_logger::Builder; use sqlx::postgres::PgPoolOptions; use std::env; +use log; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -44,9 +45,12 @@ async fn main() -> std::io::Result<()> { .service(books::get_book) .service(auth::register) .service(auth::login) + .service(auth::logout) + .service(auth::check_auth) .service(cart::get_cart) .service(cart::add_to_cart) .service(cart::remove_from_cart) + .service(cart::checkout) .service(profile::get_order_history) .service( Files::new("/images", "./static/images") diff --git a/src/models.rs b/src/models.rs index 8ff57d5..310464b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use bigdecimal::BigDecimal; use chrono::NaiveDateTime; +use sqlx::FromRow; #[derive(Deserialize)] pub struct RegistrationData { @@ -49,3 +50,26 @@ pub struct CartItemResponse { pub obraz_url: String, } +#[derive(Serialize, FromRow)] +pub struct OrderItem { + pub tytul: String, + pub autor: String, + pub ilosc: i32, + pub cena: BigDecimal, +} + +#[derive(Serialize)] +pub struct OrderWithItems { + pub id: i32, + pub data_zamowienia: NaiveDateTime, + pub suma_totalna: BigDecimal, + pub status: Option, + pub items: Vec, +} + +#[derive(Deserialize)] +pub struct CheckoutRequest { + pub items: Vec, + pub total: f64, +} + diff --git a/src/profile.rs b/src/profile.rs index 8ef4619..d61beca 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -4,51 +4,15 @@ use sqlx::PgPool; use crate::error::AppError; use bigdecimal::BigDecimal; use chrono::NaiveDateTime; - -// Struktura reprezentująca pojedynczą pozycję zamówienia -#[derive(Serialize)] -pub struct OrderItem { - pub tytul: String, - pub autor: String, - pub ilosc: i32, - pub cena: BigDecimal, -} - -// Struktura reprezentująca zamówienie z pozycjami -#[derive(Serialize)] -pub struct OrderWithItems { - pub id: i32, - pub data_zamowienia: NaiveDateTime, - pub suma_totalna: BigDecimal, - pub status: Option, - pub items: Vec, -} - -// Funkcja pomocnicza do walidacji tokenu -async fn validate_token(token: Option<&str>) -> Result { - let raw_token = token.ok_or(AppError::Unauthorized("Unauthorized".to_string()))?; - let token = raw_token.trim_start_matches("Bearer ").trim(); - - if token.starts_with("user-") { - let user_id = token.replace("user-", "").replace("-token", "").parse() - .map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?; - Ok(user_id) - } else { - Err(AppError::Unauthorized("Unauthorized".to_string())) - } -} - -// Funkcja pomocnicza do pobierania tokenu z nagłówka -fn get_token(req: &HttpRequest) -> Option<&str> { - req.headers().get("Authorization").and_then(|h| h.to_str().ok()) -} +use crate::auth::validate_token; +use crate::models::{OrderItem, OrderWithItems}; #[get("/api/order-history")] pub async fn get_order_history( req: HttpRequest, pool: web::Data, ) -> Result { - let user_id = validate_token(get_token(&req)).await?; + let user_id = validate_token(&req).await?; let orders = sqlx::query!( r#" diff --git a/static/js/auth.js b/static/js/auth.js index f742e45..804451a 100644 --- a/static/js/auth.js +++ b/static/js/auth.js @@ -1,37 +1,28 @@ -// static/js/auth.js - import { handleApiError, updateAuthUI, setupLogout } from './utils.js'; -document.addEventListener('DOMContentLoaded', () => { - updateAuthUI(); - setupLogout(); +document.getElementById('loginForm')?.addEventListener('submit', async (e) => { + e.preventDefault(); - // Logowanie - document.getElementById('loginForm')?.addEventListener('submit', async (e) => { - e.preventDefault(); - - try { - const response = await fetch('/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: document.getElementById('loginEmail').value, - haslo: document.getElementById('loginPassword').value - }) - }); + try { + const response = await fetch('/login', { + method: 'POST', + credentials: 'include', // dołącz ciasteczka + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: document.getElementById('loginEmail').value, + haslo: document.getElementById('loginPassword').value + }) + }); - if (response.ok) { - const { token, imie } = await response.json(); - localStorage.setItem('token', token); - localStorage.setItem('imie', imie); - window.location.href = '/'; - } else { - throw new Error('Błąd logowania!'); - } - } catch (error) { - handleApiError(error); + if (response.ok) { + window.location.href = '/'; + } else { + alert('Błąd logowania!'); } - }); + } catch (error) { + console.error('Błąd:', error); + } +}); // Rejestracja document.getElementById('registerForm')?.addEventListener('submit', async (e) => { @@ -63,4 +54,31 @@ document.addEventListener('DOMContentLoaded', () => { handleApiError(error); } }); + +async function checkAuthStatus() { + try { + const response = await fetch('/api/check-auth', { + credentials: 'include' // ważne - dołącz ciasteczka + }); + + if (!response.ok) return { authenticated: false }; + + const data = await response.json(); + return data; + } catch (error) { + return { authenticated: false }; + } +} + +document.getElementById('logoutLink')?.addEventListener('click', async (e) => { + e.preventDefault(); + try { + await fetch('/api/logout', { + method: 'POST', + credentials: 'include' // ważne - dołącz ciasteczka + }); + window.location.href = '/'; + } catch (error) { + console.error('Logout error:', error); + } }); diff --git a/static/js/utils.js b/static/js/utils.js index fc804db..b0edd99 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -1,5 +1,3 @@ -// static/js/utils.js - export function getAuthHeaders() { const token = localStorage.getItem('token'); return { @@ -13,12 +11,32 @@ export function handleApiError(error, fallbackMsg = 'Wystąpił błąd') { alert(error.message || fallbackMsg); } -export function updateAuthUI() { - const token = localStorage.getItem('token'); - document.querySelectorAll('.auth-links').forEach(container => { - container.querySelector('.anonymous-links').style.display = token ? 'none' : 'flex'; - container.querySelector('.user-links').style.display = token ? 'flex' : 'none'; - }); +async function updateAuthUI() { + try { + const response = await fetch('/api/check-auth', { + credentials: 'include' + }); + + if (!response.ok) return; + + const data = await response.json(); + const authContainers = document.querySelectorAll('.auth-links'); + + authContainers.forEach(container => { + const anonymousLinks = container.querySelector('.anonymous-links'); + const userLinks = container.querySelector('.user-links'); + + if (data.authenticated) { + if (anonymousLinks) anonymousLinks.style.display = 'none'; + if (userLinks) userLinks.style.display = 'flex'; + } else { + if (anonymousLinks) anonymousLinks.style.display = 'flex'; + if (userLinks) userLinks.style.display = 'none'; + } + }); + } catch (error) { + console.error('Błąd aktualizacji UI:', error); + } } export function setupLogout() { @@ -28,3 +46,5 @@ export function setupLogout() { window.location.href = '/'; }); } + +document.addEventListener('DOMContentLoaded', updateAuthUI);