use actix_web::{Error, post, get, web, delete, App, HttpResponse, HttpServer, Responder, HttpRequest}; use actix_cors::Cors; use actix_files::Files; use dotenv::dotenv; use env_logger::{Builder, Env}; use sqlx::postgres::PgPoolOptions; use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashMap; use bigdecimal::BigDecimal; use chrono::{TimeZone, DateTime, Utc, NaiveDateTime}; use sqlx::FromRow; use actix_web::http::header; use sqlx::Row; use bigdecimal::FromPrimitive; use std::convert::Infallible; mod auth; #[derive(Deserialize)] struct RegistrationData { email: String, haslo: String, imie: String, #[serde(rename = "confirmPassword")] confirm_password: String, } #[derive(Deserialize)] struct LoginData { email: String, haslo: String, } #[derive(Serialize)] struct LoginResponse { token: String, imie: String, } #[derive(sqlx::FromRow, serde::Serialize)] struct Book { id: i32, tytul: String, autor: String, cena: BigDecimal, obraz_url: Option, opis: Option, } #[derive(Deserialize)] struct CartItem { book_id: i32, quantity: i32, } #[derive(Serialize)] struct OrderWithItems { id: i32, data_zamowienia: NaiveDateTime, suma_totalna: BigDecimal, status: Option, items: Vec, } #[derive(sqlx::FromRow, Serialize)] struct OrderItem { tytul: String, autor: String, ilosc: i32, cena: BigDecimal, } #[derive(Deserialize)] struct CheckoutRequest { items: Vec, total: f64, } async fn validate_token(token: Option<&str>) -> Result { let raw_token = token.ok_or(actix_web::error::ErrorUnauthorized("Unauthorized"))?; // Usuń prefiks "Bearer " 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(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?; Ok(user_id) } else { Err(actix_web::error::ErrorUnauthorized("Unauthorized")) } } #[post("/rejestracja")] async fn rejestracja( form: web::Json, pool: web::Data, ) -> impl Responder { // Walidacja hasła if form.haslo.len() < 8 { return HttpResponse::BadRequest().body("Hasło musi mieć minimum 8 znaków"); } // Sprawdzenie, czy hasła się zgadzają if form.haslo != form.confirm_password { return HttpResponse::BadRequest().body("Hasła nie są identyczne"); } // Hashowanie hasła let hashed_password = match bcrypt::hash(&form.haslo, bcrypt::DEFAULT_COST) { Ok(h) => h, Err(_) => return HttpResponse::InternalServerError().finish(), }; // Zapisz do bazy danych match sqlx::query!( r#" INSERT INTO uzytkownicy (email, haslo, imie) VALUES ($1, $2, $3) "#, form.email, hashed_password, form.imie ) .execute(pool.get_ref()) .await { Ok(_) => HttpResponse::Created().body("Konto utworzone pomyślnie"), Err(e) => { if e.to_string().contains("duplicate key value") { HttpResponse::Conflict().body("Email jest już zarejestrowany") } else { HttpResponse::InternalServerError().finish() } } } } #[post("/login")] async fn login( form: web::Json, pool: web::Data, ) -> impl Responder { let user = match sqlx::query!( "SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1", form.email ) .fetch_optional(pool.get_ref()) .await { Ok(Some(u)) => u, Ok(None) => return HttpResponse::Unauthorized().body("Nieprawidłowe dane"), Err(_) => return HttpResponse::InternalServerError().finish(), }; match bcrypt::verify(&form.haslo, &user.haslo) { Ok(true) => { // W praktyce użyj JWT lub innego mechanizmu autentykacji let dummy_token = format!("user-{}-token", user.id); HttpResponse::Ok().json(LoginResponse { token: dummy_token, imie: user.imie, }) }, _ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"), } } #[get("/api/ksiazki")] async fn get_ksiazki( pool: web::Data, web::Query(params): web::Query>, ) -> impl Responder { let search_term = params.get("search").map(|s| s.as_str()).unwrap_or(""); let sort_by = params.get("sort").map(|s| s.as_str()).unwrap_or("default"); // Poprawione zapytanie bazowe let mut base_query = "SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis FROM ksiazki".to_string(); // Warunek WHERE let where_clause = if !search_term.is_empty() { " WHERE LOWER(tytul) LIKE LOWER($1) OR LOWER(autor) LIKE LOWER($1)" } else { "" }; // Poprawna kolejność klauzul let order_clause = match sort_by { "price_asc" => " ORDER BY cena ASC", "price_desc" => " ORDER BY cena DESC", "title_asc" => " ORDER BY tytul ASC", "author_asc" => " ORDER BY autor ASC", _ => " ORDER BY tytul ASC" // Domyślne sortowanie }; // Łączymy części zapytania w odpowiedniej kolejności let query = format!("{}{}{}", base_query, where_clause, order_clause); let mut query_builder = sqlx::query_as::<_, Book>(&query); if !search_term.is_empty() { query_builder = query_builder.bind(format!("%{}%", search_term)); } match query_builder.fetch_all(pool.get_ref()).await { Ok(books) => HttpResponse::Ok().json(books), Err(e) => { log::error!("Błąd bazy danych: {:?}", e); HttpResponse::InternalServerError().json(json!({"error": "Błąd serwera"})) } } } #[get("/api/ksiazki/{id}")] async fn get_ksiazka( pool: web::Data, path: web::Path, ) -> impl Responder { let id = path.into_inner(); match sqlx::query_as!( Book, r#" SELECT id, tytul, autor, cena, COALESCE('/images/' || obraz_url, '/images/placeholder.jpg') as obraz_url, COALESCE(opis, 'Brak opisu') as opis FROM ksiazki WHERE id = $1 "#, id ) .fetch_optional(pool.get_ref()) .await { Ok(Some(book)) => HttpResponse::Ok().json(book), Ok(None) => HttpResponse::NotFound().json(json!({"error": "Książka nie znaleziona"})), Err(e) => { log::error!("Błąd bazy danych: {}", e); HttpResponse::InternalServerError().json(json!({"error": "Błąd serwera"})) } } } #[derive(Serialize)] struct UserInfo { id: i32, imie: String, } #[get("/api/check-auth")] async fn check_auth( req: HttpRequest, pool: web::Data, // Dodajemy pool jako parametr ) -> impl Responder { let token = req.headers().get("Authorization") .and_then(|h| h.to_str().ok()); match validate_token(token).await { Ok(user_id) => { match sqlx::query!( "SELECT imie FROM uzytkownicy WHERE id = $1", user_id ) .fetch_one(pool.get_ref()) // Używamy pool z parametru .await { Ok(u) => HttpResponse::Ok().json(json!({ "authenticated": true, "user": { "id": user_id, "imie": u.imie } })), Err(_) => HttpResponse::Ok().json(json!({ "authenticated": false, "user": null })) } }, Err(_) => HttpResponse::Ok().json(json!({ "authenticated": false, "user": null })) } } #[derive(serde::Serialize)] struct CartItemResponse { book_id: i32, quantity: i32, tytul: String, cena: BigDecimal, #[serde(rename = "obraz_url")] obraz_url: String, // Zmiana z Option na String } #[get("/api/cart")] async fn get_cart( req: HttpRequest, pool: web::Data, ) -> Result { let user_id = validate_token(get_token(&req)).await?; let cart_items = sqlx::query_as!( CartItemResponse, r#"SELECT k.book_id as "book_id!", k.quantity as "quantity!", b.tytul as "tytul!", b.cena as "cena!", COALESCE('/images/' || NULLIF(b.obraz_url, ''), '/images/placeholder.jpg') as "obraz_url!" FROM koszyk k JOIN ksiazki b ON k.book_id = b.id WHERE k.user_id = $1 ORDER BY b.tytul"#, user_id ) .fetch_all(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; Ok(HttpResponse::Ok().json(cart_items)) } fn get_token(req: &HttpRequest) -> Option<&str> { req.headers() .get("Authorization") .and_then(|h| h.to_str().ok()) } #[post("/api/add-to-cart")] async fn add_to_cart( cart_item: web::Json, req: HttpRequest, pool: web::Data, ) -> Result { let token = req.headers().get("Authorization") .and_then(|h| h.to_str().ok()); let user_id = validate_token(token).await?; sqlx::query!( "INSERT INTO koszyk (user_id, book_id, quantity) VALUES ($1, $2, $3) ON CONFLICT (user_id, book_id) DO UPDATE SET quantity = koszyk.quantity + $3", user_id, cart_item.book_id, cart_item.quantity ) .execute(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; Ok(HttpResponse::Ok().json(json!({"status": "success"}))) } #[delete("/api/remove-from-cart/{book_id}")] async fn remove_from_cart( req: HttpRequest, pool: web::Data, book_id: web::Path, ) -> Result { let user_id = validate_token(get_token(&req)).await?; let book_id = book_id.into_inner(); sqlx::query!( "DELETE FROM koszyk WHERE user_id = $1 AND book_id = $2", user_id, book_id ) .execute(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Dodaj mapowanie błędu Ok(HttpResponse::Ok().json(json!({"status": "success"}))) } #[post("/api/decrease-cart-item/{book_id}")] async fn decrease_cart_item( req: HttpRequest, pool: web::Data, book_id: web::Path, ) -> Result { let user_id = validate_token(get_token(&req)).await?; let book_id = book_id.into_inner(); // Sprawdź aktualną ilość let current_quantity = sqlx::query!( "SELECT quantity FROM koszyk WHERE user_id = $1 AND book_id = $2", user_id, book_id ) .fetch_optional(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))? // Dodaj mapowanie błędu .map(|r| r.quantity); if let Some(qty) = current_quantity { if qty > 1 { // Zmniejsz ilość o 1 sqlx::query!( "UPDATE koszyk SET quantity = quantity - 1 WHERE user_id = $1 AND book_id = $2", user_id, book_id ) .execute(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Dodaj mapowanie błędu } else { // Usuń pozycję jeśli ilość = 1 sqlx::query!( "DELETE FROM koszyk WHERE user_id = $1 AND book_id = $2", user_id, book_id ) .execute(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Dodaj mapowanie błędu } } Ok(HttpResponse::Ok().json(json!({"status": "success"}))) } #[get("/api/order-history")] async fn get_order_history( req: HttpRequest, pool: web::Data, ) -> Result { let user_id = validate_token(get_token(&req)).await?; let orders = sqlx::query!( r#" SELECT z.id as "id!", z.data_zamowienia as "data_zamowienia!", z.suma_totalna as "suma_totalna!", z.status, pz.ilosc as "ilosc!", pz.cena as "item_price!", k.tytul as "tytul!", k.autor as "autor!" FROM zamowienia z JOIN pozycje_zamowienia pz ON z.id = pz.zamowienie_id JOIN ksiazki k ON pz.book_id = k.id WHERE z.user_id = $1 ORDER BY z.id DESC, k.tytul ASC "#, user_id ) .fetch_all(pool.get_ref()) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let mut grouped_orders = HashMap::new(); for record in orders { let entry = grouped_orders.entry(record.id).or_insert(OrderWithItems { id: record.id, data_zamowienia: record.data_zamowienia, suma_totalna: record.suma_totalna, status: record.status, items: Vec::new(), }); entry.items.push(OrderItem { tytul: record.tytul, autor: record.autor, ilosc: record.ilosc, cena: record.item_price, }); } let result: Vec = grouped_orders.into_values().collect(); Ok(HttpResponse::Ok().json(result)) } #[post("/api/checkout")] async fn checkout( req: HttpRequest, pool: web::Data, data: web::Json, ) -> Result { let user_id = validate_token(get_token(&req)).await?; let mut transaction = pool.begin().await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // 1. Utwórz zamówienie let order_id = sqlx::query!( "INSERT INTO zamowienia (user_id, suma_totalna) VALUES ($1, $2) RETURNING id", user_id, BigDecimal::from_f64(data.total).ok_or_else(|| actix_web::error::ErrorBadRequest("Invalid total value"))? ) .fetch_one(&mut *transaction) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))? .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| actix_web::error::ErrorInternalServerError(e))?; 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| actix_web::error::ErrorInternalServerError(e))?; } // 3. Wyczyść koszyk sqlx::query!( "DELETE FROM koszyk WHERE user_id = $1", user_id ) .execute(&mut *transaction) .await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; transaction.commit().await .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; Ok(HttpResponse::Ok().json(json!({"status": "success"}))) } #[actix_web::main] async fn main() -> std::io::Result<()> { // Inicjalizacja loggera Builder::from_env(Env::default().default_filter_or("debug")).init(); // Ładowanie zmiennych środowiskowych dotenv().ok(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set in .env"); // Utwórz pulę połączeń let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to create pool"); HttpServer::new(move || { let cors = Cors::default() .allow_any_origin() .allowed_methods(vec!["GET", "POST", "DELETE"]) .allowed_headers(vec![ header::CONTENT_TYPE, header::AUTHORIZATION, header::ACCEPT, header::HeaderName::from_static("content-type"), ]) .supports_credentials(); App::new() .app_data(web::Data::new(pool.clone())) .wrap(cors) .wrap(actix_web::middleware::Logger::default()) .service(get_ksiazki) .service(get_ksiazka) .service(rejestracja) .service(login) .service(get_cart) .service(add_to_cart) // Dodaj .service(checkout) // Dodaj .service(check_auth) .service(remove_from_cart) .service(decrease_cart_item) .service(get_order_history) .service( Files::new("/images", "./static/images") .show_files_listing(), ) .service(Files::new("/", "./static").index_file("index.html")) }) .bind("0.0.0.0:7999")? .run() .await }