diff --git a/src/auth.rs b/src/auth.rs index 34ea8e0..ed702dd 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,39 +1,25 @@ use actix_web::{post, web, HttpResponse, Responder}; +use crate::models::{RegistrationData, LoginData, LoginResponse}; +use crate::error::AppError; use bcrypt::{hash, verify, DEFAULT_COST}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize)] -pub struct RegistrationData { - pub email: String, - pub haslo: String, - pub imie: String, - #[serde(rename = "confirmPassword")] - pub confirm_password: String, -} +use sqlx::PgPool; #[post("/rejestracja")] -pub async fn rejestracja( +pub async fn register( form: web::Json, - pool: web::Data, -) -> impl Responder { - // Walidacja hasła + pool: web::Data, +) -> Result { if form.haslo.len() < 8 { - return HttpResponse::BadRequest().body("Hasło musi mieć minimum 8 znaków"); + return Err(AppError::BadRequest("Hasło musi mieć minimum 8 znaków".to_string())); } - // Sprawdzenie, czy hasła się zgadzają if form.haslo != form.confirm_password { - return HttpResponse::BadRequest().body("Hasła nie są identyczne"); + return Err(AppError::BadRequest("Hasła nie są identyczne".to_string())); } - // Hashowanie hasła - let hashed_password = match hash(&form.haslo, DEFAULT_COST) { - Ok(h) => h, - Err(_) => return HttpResponse::InternalServerError().finish(), - }; + let hashed_password = hash(&form.haslo, DEFAULT_COST).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; - // Zapisz do bazy danych - match sqlx::query!( + sqlx::query!( r#" INSERT INTO uzytkownicy (email, haslo, imie) VALUES ($1, $2, $3) @@ -44,57 +30,39 @@ pub async fn rejestracja( ) .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() - } + .map_err(|e| { + if e.to_string().contains("duplicate key value") { + AppError::BadRequest("Email jest już zarejestrowany".to_string()) + } else { + AppError::InternalServerError("Błąd serwera".to_string()) } - } -} + })?; -#[derive(Deserialize)] -pub struct LoginData { - pub email: String, - pub haslo: String, -} - -#[derive(Serialize)] -pub struct LoginResponse { - pub token: String, - pub imie: String, + Ok(HttpResponse::Created().body("Konto utworzone pomyślnie")) } #[post("/login")] pub async fn login( form: web::Json, - pool: web::Data, -) -> impl Responder { - let user = match sqlx::query!( + pool: web::Data, +) -> Result { + let user = 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(), - }; + .map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))? + .ok_or_else(|| AppError::Unauthorized("Nieprawidłowe dane".to_string()))?; - match 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"), + 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, + })) + } else { + Err(AppError::Unauthorized("Nieprawidłowe hasło".to_string())) } } diff --git a/src/books.rs b/src/books.rs new file mode 100644 index 0000000..dd4aab9 --- /dev/null +++ b/src/books.rs @@ -0,0 +1,67 @@ +use actix_web::{get, web, HttpResponse, Responder}; +use crate::models::Book; +use sqlx::PgPool; +use std::collections::HashMap; + +#[get("/api/ksiazki")] +pub async fn get_books( + pool: web::Data, + query: web::Query>, +) -> impl Responder { + let search_term = query.get("search").map(|s| s.as_str()).unwrap_or(""); + let sort_by = query.get("sort").map(|s| s.as_str()).unwrap_or("default"); + + 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(); + + let where_clause = if !search_term.is_empty() { + " WHERE LOWER(tytul) LIKE LOWER($1) OR LOWER(autor) LIKE LOWER($1)" + } else { + "" + }; + + 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", + }; + + let query_str = format!("{}{}{}", base_query, where_clause, order_clause); + + let mut query_builder = sqlx::query_as::<_, Book>(&query_str); + + 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(_) => HttpResponse::InternalServerError().body("Błąd serwera"), + } +} + +#[get("/api/ksiazki/{id}")] +pub async fn get_book( + 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().body("Książka nie znaleziona"), + Err(_) => HttpResponse::InternalServerError().body("Błąd serwera"), + } +} + diff --git a/src/cart.rs b/src/cart.rs new file mode 100644 index 0000000..c20a4af --- /dev/null +++ b/src/cart.rs @@ -0,0 +1,97 @@ +use actix_web::{get, post, delete, web, HttpResponse, HttpRequest, Responder}; +use crate::models::{CartItem, CartItemResponse}; +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()) +} + +#[get("/api/cart")] +pub 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(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + + Ok(HttpResponse::Ok().json(cart_items)) +} + +#[post("/api/add-to-cart")] +pub async fn add_to_cart( + cart_item: web::Json, + req: HttpRequest, + pool: web::Data, +) -> Result { + let token = get_token(&req); + 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(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + + Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))) +} + +#[delete("/api/remove-from-cart/{book_id}")] +pub 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(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + + Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))) +} + diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b121e44 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,30 @@ +use actix_web::{HttpResponse, ResponseError}; +use std::fmt; + +#[derive(Debug)] +pub enum AppError { + BadRequest(String), + Unauthorized(String), + InternalServerError(String), +} + +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AppError::BadRequest(msg) => write!(f, "BadRequest: {}", msg), + AppError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg), + AppError::InternalServerError(msg) => write!(f, "InternalServerError: {}", msg), + } + } +} + +impl ResponseError for AppError { + fn error_response(&self) -> HttpResponse { + match self { + AppError::BadRequest(msg) => HttpResponse::BadRequest().body(msg.clone()), + AppError::Unauthorized(msg) => HttpResponse::Unauthorized().body(msg.clone()), + AppError::InternalServerError(msg) => HttpResponse::InternalServerError().body(msg.clone()), + } + } +} + diff --git a/src/main.rs b/src/main.rs index a9d28a7..bfdcfc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,564 +1,24 @@ -use actix_web::{Error, post, get, web, delete, App, HttpResponse, HttpServer, Responder, HttpRequest}; +mod models; +mod auth; +mod books; +mod cart; +mod profile; +mod error; + +use actix_web::{web, App, HttpServer}; use actix_cors::Cors; use actix_files::Files; use dotenv::dotenv; -use env_logger::{Builder, Env}; +use env_logger::Builder; 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"}))) -} +use std::env; #[actix_web::main] async fn main() -> std::io::Result<()> { - // Inicjalizacja loggera - Builder::from_env(Env::default().default_filter_or("debug")).init(); - - // Ładowanie zmiennych środowiskowych + Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); dotenv().ok(); - let database_url = std::env::var("DATABASE_URL") - .expect("DATABASE_URL must be set in .env"); - // Utwórz pulę połączeń + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set in .env"); let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) @@ -570,10 +30,9 @@ async fn main() -> std::io::Result<()> { .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"), + actix_web::http::header::CONTENT_TYPE, + actix_web::http::header::AUTHORIZATION, + actix_web::http::header::ACCEPT, ]) .supports_credentials(); @@ -581,17 +40,14 @@ async fn main() -> std::io::Result<()> { .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(books::get_books) + .service(books::get_book) + .service(auth::register) + .service(auth::login) + .service(cart::get_cart) + .service(cart::add_to_cart) + .service(cart::remove_from_cart) + .service(profile::get_order_history) .service( Files::new("/images", "./static/images") .show_files_listing(), diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..8ff57d5 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; + +#[derive(Deserialize)] +pub struct RegistrationData { + pub email: String, + pub haslo: String, + pub imie: String, + #[serde(rename = "confirmPassword")] + pub confirm_password: String, +} + +#[derive(Deserialize)] +pub struct LoginData { + pub email: String, + pub haslo: String, +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub token: String, + pub imie: String, +} + +#[derive(sqlx::FromRow, Serialize)] +pub struct Book { + pub id: i32, + pub tytul: String, + pub autor: String, + pub cena: BigDecimal, + pub obraz_url: Option, + pub opis: Option, +} + +#[derive(Deserialize)] +pub struct CartItem { + pub book_id: i32, + pub quantity: i32, +} + +#[derive(Serialize)] +pub struct CartItemResponse { + pub book_id: i32, + pub quantity: i32, + pub tytul: String, + pub cena: BigDecimal, + #[serde(rename = "obraz_url")] + pub obraz_url: String, +} + diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 0000000..8ef4619 --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,97 @@ +use actix_web::{get, web, HttpResponse, HttpRequest}; +use serde::Serialize; +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()) +} + +#[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 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(|_| AppError::InternalServerError("Błąd serwera".to_string()))?; + + let mut grouped_orders: std::collections::HashMap = std::collections::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)) +} + diff --git a/static/book.html b/static/book.html index cf652ab..b25bf00 100644 --- a/static/book.html +++ b/static/book.html @@ -54,7 +54,7 @@

- + @@ -76,7 +76,8 @@ - - + + + diff --git a/static/cart.html b/static/cart.html index 5c5a711..2c64674 100644 --- a/static/cart.html +++ b/static/cart.html @@ -72,6 +72,8 @@ - + + + diff --git a/static/css/styles.css b/static/css/styles.css index 5a77e78..f1e92b1 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -61,7 +61,6 @@ footer { font-size: 12px; font-family: 'Inter', sans-serif; color: var(--gold1); - position: fixed; left: 0; bottom: 0; width: 100%; diff --git a/static/js/auth.js b/static/js/auth.js new file mode 100644 index 0000000..f742e45 --- /dev/null +++ b/static/js/auth.js @@ -0,0 +1,66 @@ +// static/js/auth.js + +import { handleApiError, updateAuthUI, setupLogout } from './utils.js'; + +document.addEventListener('DOMContentLoaded', () => { + updateAuthUI(); + setupLogout(); + + // 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 + }) + }); + + 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); + } + }); + + // Rejestracja + document.getElementById('registerForm')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const name = document.getElementById('registerName').value; + const email = document.getElementById('registerEmail').value; + const password = document.getElementById('registerPassword').value; + const confirmPassword = document.getElementById('registerConfirmPassword').value; + + if (password !== confirmPassword) { + alert('Hasła nie są identyczne'); + return; + } + + try { + const response = await fetch('/rejestracja', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ imie: name, email, haslo: password, confirmPassword }), + }); + + if (response.ok) { + alert('Rejestracja udana! Możesz się teraz zalogować.'); + window.location.href = '/login.html'; + } else { + throw new Error('Rejestracja nieudana'); + } + } catch (error) { + handleApiError(error); + } + }); +}); diff --git a/static/js/book.js b/static/js/book.js index 58490d0..e7210a4 100644 --- a/static/js/book.js +++ b/static/js/book.js @@ -1,3 +1,7 @@ +// static/js/book.js + +import { getAuthHeaders, handleApiError } from './utils.js'; + document.addEventListener('DOMContentLoaded', async () => { const urlParams = new URLSearchParams(window.location.search); const bookId = urlParams.get('id'); @@ -15,9 +19,7 @@ document.addEventListener('DOMContentLoaded', async () => { try { const response = await fetch(`/api/ksiazki/${bookId}`); - if (!response.ok) { - throw new Error(`Status: ${response.status}`); - } + if (!response.ok) throw new Error(`Status: ${response.status}`); const book = await response.json(); document.getElementById('book-title').textContent = book.tytul; @@ -26,8 +28,8 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('book-description').textContent = book.opis; document.getElementById('book-cover').src = book.obraz_url; - // Dodaj obsługę przycisku "Dodaj do koszyka" - document.querySelector('.btn').addEventListener('click', async () => { + // Dodaj do koszyka + document.querySelector('.btn-add-to-cart').addEventListener('click', async () => { const token = localStorage.getItem('token'); if (!token) { alert('Musisz być zalogowany, aby dodać książkę do koszyka'); @@ -38,10 +40,7 @@ document.addEventListener('DOMContentLoaded', async () => { try { const response = await fetch('/api/add-to-cart', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, + headers: getAuthHeaders(), body: JSON.stringify({ book_id: parseInt(bookId), quantity: 1 @@ -51,16 +50,13 @@ document.addEventListener('DOMContentLoaded', async () => { if (response.ok) { alert('Dodano do koszyka!'); } else { - const error = await response.json(); - alert(error.error || 'Wystąpił błąd'); + throw new Error('Wystąpił błąd'); } } catch (error) { - console.error('Błąd:', error); - alert('Wystąpił błąd podczas dodawania do koszyka'); + handleApiError(error); } }); } catch (error) { - console.error('Błąd:', error); bookDetails.innerHTML = `

Błąd ładowania książki

diff --git a/static/js/books.js b/static/js/books.js new file mode 100644 index 0000000..65a9bd2 --- /dev/null +++ b/static/js/books.js @@ -0,0 +1,66 @@ +// static/js/books.js + +import { getAuthHeaders, handleApiError } from './utils.js'; + +export async function loadBooks(searchTerm = '', sortBy = 'default') { + try { + const response = await fetch(`/api/ksiazki?search=${encodeURIComponent(searchTerm)}&sort=${sortBy}`); + if (!response.ok) throw new Error(`Błąd HTTP: ${response.status}`); + return await response.json(); + } catch (error) { + handleApiError(error, 'Wystąpił błąd podczas ładowania książek'); + return []; + } +} + +export function renderBooks(books, containerId) { + const container = document.getElementById(containerId); + if (!container) return; + + if (books.length === 0) { + container.innerHTML = '

Nie znaleziono książek

'; + return; + } + + container.innerHTML = books.map(book => ` +
+
+ +
+ ${book.tytul} +
+
+
+
${book.tytul}
+
+

${book.autor}

+

${book.cena} PLN

+
+
+
+
+ `).join(''); +} + +// Inicjalizacja na stronie głównej +document.addEventListener('DOMContentLoaded', async () => { + if (!document.getElementById('books-container')) return; + + const books = await loadBooks(); + renderBooks(books, 'books-container'); + + // Obsługa wyszukiwania i sortowania + const searchInput = document.getElementById('searchInput'); + const sortSelect = document.getElementById('sortSelect'); + + const reloadBooks = async () => { + const books = await loadBooks( + searchInput?.value || '', + sortSelect?.value || 'default' + ); + renderBooks(books, 'books-container'); + }; + + searchInput?.addEventListener('input', reloadBooks); + sortSelect?.addEventListener('change', reloadBooks); +}); diff --git a/static/js/cart.js b/static/js/cart.js index 71b5192..8ba1487 100644 --- a/static/js/cart.js +++ b/static/js/cart.js @@ -1,3 +1,7 @@ +// static/js/cart.js + +import { getAuthHeaders, handleApiError } from './utils.js'; + document.addEventListener('DOMContentLoaded', async () => { try { const token = localStorage.getItem('token'); @@ -7,123 +11,122 @@ document.addEventListener('DOMContentLoaded', async () => { } const response = await fetch('/api/cart', { - headers: { - 'Authorization': `Bearer ${token}` - } + headers: getAuthHeaders() }); - + if (!response.ok) throw new Error('Błąd ładowania koszyka'); - const cartItems = await response.json(); - const container = document.getElementById('cart-items'); - container.innerHTML = ''; + renderCart(cartItems); - if (cartItems.length === 0) { - container.innerHTML = '

Twój koszyk jest pusty

'; - return; - } + } catch (error) { + handleApiError(error, 'Nie udało się załadować koszyka'); + } +}); - let totalCartValue = 0; +function renderCart(cartItems) { + const container = document.getElementById('cart-items'); + container.innerHTML = ''; - cartItems.forEach(item => { - // Formatowanie cen - const price = parseFloat(item.cena); - const formattedPrice = price.toFixed(2); - const itemTotal = price * item.quantity; - const formattedTotal = itemTotal.toFixed(2); - totalCartValue += itemTotal; + if (cartItems.length === 0) { + container.innerHTML = '

Twój koszyk jest pusty

'; + return; + } - const itemHTML = ` -
-
-
-
- ${item.tytul} -
-
-
-
-
${item.tytul}
-

${item.autor}

+ let totalCartValue = 0; + + cartItems.forEach(item => { + const price = parseFloat(item.cena); + const itemTotal = price * item.quantity; + totalCartValue += itemTotal; + + const itemHTML = ` +
+
+
+
+ ${item.tytul} +
+
+
+
+
${item.tytul}
+

${item.autor}

+
+ +
+
+ Cena jednostkowa + ${price.toFixed(2)} PLN
-
-
- Cena jednostkowa - ${formattedPrice} PLN -
- -
- Ilość -
- ${item.quantity} -
-
- -
- Suma - ${formattedTotal} PLN +
+ Ilość +
+ ${item.quantity}
-
-
- - -
+
+ Suma + ${itemTotal.toFixed(2)} PLN +
+
+ +
+
+ +
- `; - container.insertAdjacentHTML('beforeend', itemHTML); - }); - - // Dodaj całkowitą sumę koszyka - const totalHTML = ` -
-
-
-

Suma całkowita

-

${totalCartValue.toFixed(2)} PLN

-
-
`; - container.insertAdjacentHTML('beforeend', totalHTML); + container.insertAdjacentHTML('beforeend', itemHTML); + }); - // Obsługa przycisku zmniejszania ilości - document.querySelectorAll('.decrease-quantity').forEach(button => { - button.addEventListener('click', function() { - const bookId = this.getAttribute('data-book-id'); - updateCartItemQuantity(bookId, 'decrease'); - }); + // Suma całkowita + const totalHTML = ` +
+
+
+

Suma całkowita

+

${totalCartValue.toFixed(2)} PLN

+
+
+
+ `; + container.insertAdjacentHTML('beforeend', totalHTML); + + // Obsługa przycisków + document.querySelectorAll('.decrease-quantity').forEach(button => { + button.addEventListener('click', function() { + const bookId = this.dataset.bookId; + updateCartItemQuantity(bookId, 'decrease'); }); + }); - // Obsługa przycisku usuwania - document.querySelectorAll('.remove-from-cart').forEach(button => { - button.addEventListener('click', function() { - const bookId = this.getAttribute('data-book-id'); - updateCartItemQuantity(bookId, 'remove'); - }); + document.querySelectorAll('.remove-from-cart').forEach(button => { + button.addEventListener('click', function() { + const bookId = this.dataset.bookId; + updateCartItemQuantity(bookId, 'remove'); }); + }); - } catch (error) { - console.error('Error:', error); - alert('Nie udało się załadować koszyka'); - } -}); + // Finalizacja zamówienia + document.getElementById('checkoutBtn')?.addEventListener('click', handleCheckout); +} async function updateCartItemQuantity(bookId, action) { try { @@ -133,36 +136,28 @@ async function updateCartItemQuantity(bookId, action) { return; } - let endpoint, method; - + let method, endpoint; if (action === 'decrease') { endpoint = `/api/decrease-cart-item/${bookId}`; method = 'POST'; - } else if (action === 'remove') { + } else { endpoint = `/api/remove-from-cart/${bookId}`; method = 'DELETE'; - } else { - throw new Error('Nieznana akcja'); } const response = await fetch(endpoint, { - method: method, - headers: { - 'Authorization': `Bearer ${token}` - } + method, + headers: getAuthHeaders() }); if (!response.ok) throw new Error('Błąd aktualizacji koszyka'); - - // Odśwież koszyk location.reload(); } catch (error) { - console.error('Error:', error); - alert('Nie udało się zaktualizować koszyka'); + handleApiError(error); } } -async function removeFromCart(bookId) { +async function handleCheckout() { try { const token = localStorage.getItem('token'); if (!token) { @@ -170,64 +165,30 @@ async function removeFromCart(bookId) { return; } - const response = await fetch(`/api/remove-from-cart/${bookId}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) throw new Error('Błąd usuwania z koszyka'); - - // Odśwież koszyk - location.reload(); - } catch (error) { - console.error('Error:', error); - alert('Nie udało się usunąć z koszyka'); - } -} - - -document.getElementById('checkoutBtn').addEventListener('click', async () => { - try { - const token = localStorage.getItem('token'); - if (!token) { - window.location.href = '/login.html'; - return; - } - - // Pobierz aktualną zawartość koszyka + // Pobierz zawartość koszyka const cartResponse = await fetch('/api/cart', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); + if (!cartResponse.ok) throw new Error('Błąd pobierania koszyka'); const cartItems = await cartResponse.json(); - // Przygotuj dane do zamówienia - const checkoutData = { - items: cartItems.map(item => ({ - book_id: item.book_id, - quantity: item.quantity - })), - total: cartItems.reduce((sum, item) => - sum + (parseFloat(item.cena) * item.quantity), 0) - }; - // Wyślij zamówienie const response = await fetch('/api/checkout', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify(checkoutData) + headers: getAuthHeaders(), + body: JSON.stringify({ + items: cartItems.map(item => ({ + book_id: item.book_id, + quantity: item.quantity + })), + total: cartItems.reduce((sum, item) => + sum + (parseFloat(item.cena) * item.quantity), 0) + }) }); if (!response.ok) throw new Error('Błąd podczas składania zamówienia'); - window.location.href = '/thankyou.html'; - } catch (error) { - console.error('Error:', error); - alert('Nie udało się złożyć zamówienia: ' + error.message); + handleApiError(error, 'Nie udało się złożyć zamówienia'); } -}); +} diff --git a/static/js/profile.js b/static/js/profile.js index fae967a..d2a8329 100644 --- a/static/js/profile.js +++ b/static/js/profile.js @@ -1,3 +1,7 @@ +// static/js/profile.js + +import { getAuthHeaders, handleApiError } from './utils.js'; + document.addEventListener('DOMContentLoaded', async () => { try { const token = localStorage.getItem('token'); @@ -7,58 +11,58 @@ document.addEventListener('DOMContentLoaded', async () => { } const response = await fetch('/api/order-history', { - headers: { 'Authorization': `Bearer ${token}` } + headers: getAuthHeaders() }); if (!response.ok) throw new Error('Błąd ładowania historii'); - const orders = await response.json(); - const container = document.getElementById('order-history'); - container.innerHTML = ''; - - orders.forEach((order, index) => { - const orderNumber = orders.length - index; - - const orderDate = new Date(order.data_zamowienia).toLocaleDateString(); - const itemsList = order.items.map(item => ` -
-
-
${item.tytul}
-

Autor: ${item.autor}

-

Ilość: ${item.ilosc} × ${item.cena} PLN

-
-
- `).join(''); - - const orderHTML = ` -
-
-
-
Zamówienie #${orderNumber}
-

Data: ${orderDate}

-
-
-
Pozycje:
- ${itemsList} -
-
-

Suma całkowita:

-

${order.suma_totalna} PLN

-
-
-

Status:

-

${order.status || 'Przyjęto do realizacji'}

-
-
-
-
- `; - container.insertAdjacentHTML('beforeend', orderHTML); - }); - + renderOrderHistory(orders); } catch (error) { - console.error('Error:', error); - alert('Nie udało się załadować historii zamówień'); + handleApiError(error, 'Nie udało się załadować historii zamówień'); } }); +function renderOrderHistory(orders) { + const container = document.getElementById('order-history'); + container.innerHTML = ''; + + orders.forEach((order, index) => { + const orderNumber = orders.length - index; + const orderDate = new Date(order.data_zamowienia).toLocaleDateString(); + + const itemsList = order.items.map(item => ` +
+
+
${item.tytul}
+

Autor: ${item.autor}

+

Ilość: ${item.ilosc} × ${item.cena} PLN

+
+
+ `).join(''); + + const orderHTML = ` +
+
+
+
Zamówienie #${orderNumber}
+

Data: ${orderDate}

+
+
+
Pozycje:
+ ${itemsList} +
+
+

Suma całkowita:

+

${order.suma_totalna} PLN

+
+
+

Status:

+

${order.status || 'Przyjęto do realizacji'}

+
+
+
+
+ `; + container.insertAdjacentHTML('beforeend', orderHTML); + }); +} diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..fc804db --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,30 @@ +// static/js/utils.js + +export function getAuthHeaders() { + const token = localStorage.getItem('token'); + return { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; +} + +export function handleApiError(error, fallbackMsg = 'Wystąpił błąd') { + console.error('Error:', error); + 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'; + }); +} + +export function setupLogout() { + document.getElementById('logoutLink')?.addEventListener('click', (e) => { + e.preventDefault(); + localStorage.removeItem('token'); + window.location.href = '/'; + }); +} diff --git a/static/profile.html b/static/profile.html index ca746a9..dc57710 100644 --- a/static/profile.html +++ b/static/profile.html @@ -67,6 +67,8 @@
- + + +