refactoring

This commit is contained in:
Lheorvine 2025-05-30 18:00:52 +02:00
parent 5aee4f61bb
commit 1adcd8617a
17 changed files with 743 additions and 850 deletions

View file

@ -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<RegistrationData>,
pool: web::Data<sqlx::PgPool>,
) -> impl Responder {
// Walidacja hasła
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
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) => {
.map_err(|e| {
if e.to_string().contains("duplicate key value") {
HttpResponse::Conflict().body("Email jest już zarejestrowany")
AppError::BadRequest("Email jest już zarejestrowany".to_string())
} else {
HttpResponse::InternalServerError().finish()
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<LoginData>,
pool: web::Data<sqlx::PgPool>,
) -> impl Responder {
let user = match sqlx::query!(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
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
if verify(&form.haslo, &user.haslo).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))? {
let dummy_token = format!("user-{}-token", user.id);
HttpResponse::Ok().json(LoginResponse {
Ok(HttpResponse::Ok().json(LoginResponse {
token: dummy_token,
imie: user.imie,
})
},
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
}))
} else {
Err(AppError::Unauthorized("Nieprawidłowe hasło".to_string()))
}
}

67
src/books.rs Normal file
View file

@ -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<PgPool>,
query: web::Query<HashMap<String, String>>,
) -> 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<PgPool>,
path: web::Path<i32>,
) -> 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"),
}
}

97
src/cart.rs Normal file
View file

@ -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<i32, AppError> {
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<PgPool>,
) -> Result<HttpResponse, AppError> {
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<CartItem>,
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
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<PgPool>,
book_id: web::Path<i32>,
) -> Result<HttpResponse, AppError> {
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"})))
}

30
src/error.rs Normal file
View file

@ -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()),
}
}
}

View file

@ -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<String>,
opis: Option<String>,
}
#[derive(Deserialize)]
struct CartItem {
book_id: i32,
quantity: i32,
}
#[derive(Serialize)]
struct OrderWithItems {
id: i32,
data_zamowienia: NaiveDateTime,
suma_totalna: BigDecimal,
status: Option<String>,
items: Vec<OrderItem>,
}
#[derive(sqlx::FromRow, Serialize)]
struct OrderItem {
tytul: String,
autor: String,
ilosc: i32,
cena: BigDecimal,
}
#[derive(Deserialize)]
struct CheckoutRequest {
items: Vec<CartItem>,
total: f64,
}
async fn validate_token(token: Option<&str>) -> Result<i32, actix_web::Error> {
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<RegistrationData>,
pool: web::Data<sqlx::PgPool>,
) -> 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<LoginData>,
pool: web::Data<sqlx::PgPool>,
) -> 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<sqlx::PgPool>,
web::Query(params): web::Query<HashMap<String, String>>,
) -> 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<sqlx::PgPool>,
path: web::Path<i32>,
) -> 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<sqlx::PgPool>, // 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<String> na String
}
#[get("/api/cart")]
async fn get_cart(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
) -> Result<HttpResponse, Error> {
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<CartItem>,
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
) -> Result<HttpResponse, actix_web::Error> {
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<sqlx::PgPool>,
book_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
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<sqlx::PgPool>,
book_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
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<sqlx::PgPool>,
) -> Result<HttpResponse, Error> {
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<OrderWithItems> = grouped_orders.into_values().collect();
Ok(HttpResponse::Ok().json(result))
}
#[post("/api/checkout")]
async fn checkout(
req: HttpRequest,
pool: web::Data<sqlx::PgPool>,
data: web::Json<CheckoutRequest>,
) -> Result<HttpResponse, actix_web::Error> {
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(),

51
src/models.rs Normal file
View file

@ -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<String>,
pub opis: Option<String>,
}
#[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,
}

97
src/profile.rs Normal file
View file

@ -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<String>,
pub items: Vec<OrderItem>,
}
// Funkcja pomocnicza do walidacji tokenu
async fn validate_token(token: Option<&str>) -> Result<i32, AppError> {
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<PgPool>,
) -> Result<HttpResponse, AppError> {
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<i32, OrderWithItems> = 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<OrderWithItems> = grouped_orders.into_values().collect();
Ok(HttpResponse::Ok().json(result))
}

View file

@ -54,7 +54,7 @@
<p id="book-description"></p>
</div>
</div>
<button class="btn">Dodaj do koszyka</button>
<button class="btn btn-add-to-cart">Dodaj do koszyka</button>
</div>
</div>
</main>
@ -76,7 +76,8 @@
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/main.js"></script>
<script src="/js/book.js"></script>
<script type="module" src="/js/utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/book.js"></script>
</body>
</html>

View file

@ -72,6 +72,8 @@
</div>
</footer>
<script src="/js/cart.js"></script>
<script type="module" src="/js/utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/cart.js"></script>
</body>
</html>

View file

@ -61,7 +61,6 @@ footer {
font-size: 12px;
font-family: 'Inter', sans-serif;
color: var(--gold1);
position: fixed;
left: 0;
bottom: 0;
width: 100%;

66
static/js/auth.js Normal file
View file

@ -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);
}
});
});

View file

@ -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 = `
<div class="col-12 text-center py-5">
<h2 class="text-danger">Błąd ładowania książki</h2>

66
static/js/books.js Normal file
View file

@ -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 = '<div class="col-12 text-center py-5"><h3>Nie znaleziono książek</h3></div>';
return;
}
container.innerHTML = books.map(book => `
<div class="col">
<div class="book-card h-100">
<a href="/book.html?id=${book.id}">
<div class="cover-container">
<img src="${book.obraz_url}" class="book-cover" alt="${book.tytul}">
</div>
</a>
<div class="book-body p-3">
<h5 class="book-title">${book.tytul}</h5>
<div class="mt-auto">
<p class="book-author mb-1">${book.autor}</p>
<p class="book-price mb-0">${book.cena} PLN</p>
</div>
</div>
</div>
</div>
`).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);
});

View file

@ -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,14 +11,19 @@ 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();
renderCart(cartItems);
} catch (error) {
handleApiError(error, 'Nie udało się załadować koszyka');
}
});
function renderCart(cartItems) {
const container = document.getElementById('cart-items');
container.innerHTML = '';
@ -26,11 +35,8 @@ document.addEventListener('DOMContentLoaded', async () => {
let totalCartValue = 0;
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;
const itemHTML = `
@ -53,7 +59,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<div class="d-flex flex-wrap gap-3 mb-2 cart-item-info mt-auto">
<div class="d-flex flex-column flex-grow-1">
<span class="fw-bold cart-item-price">Cena jednostkowa</span>
<span class="fs-5">${formattedPrice} PLN</span>
<span class="fs-5">${price.toFixed(2)} PLN</span>
</div>
<div class="d-flex flex-column">
@ -65,7 +71,7 @@ document.addEventListener('DOMContentLoaded', async () => {
<div class="d-flex flex-column">
<span class="fw-bold cart-item-total">Suma</span>
<span class="fs-5 text-warning">${formattedTotal} PLN</span>
<span class="fs-5 text-warning">${itemTotal.toFixed(2)} PLN</span>
</div>
</div>
@ -90,7 +96,7 @@ document.addEventListener('DOMContentLoaded', async () => {
container.insertAdjacentHTML('beforeend', itemHTML);
});
// Dodaj całkowitą sumę koszyka
// Suma całkowita
const totalHTML = `
<div class="col-12 mt-4">
<div class="card dark-card p-4">
@ -103,27 +109,24 @@ document.addEventListener('DOMContentLoaded', async () => {
`;
container.insertAdjacentHTML('beforeend', totalHTML);
// Obsługa przycisku zmniejszania ilości
// Obsługa przycisków
document.querySelectorAll('.decrease-quantity').forEach(button => {
button.addEventListener('click', function() {
const bookId = this.getAttribute('data-book-id');
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');
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 = {
// Wyślij zamówienie
const response = await fetch('/api/checkout', {
method: 'POST',
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)
};
// Wyślij zamówienie
const response = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(checkoutData)
})
});
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');
}
});
}

View file

@ -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,19 +11,25 @@ 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();
renderOrderHistory(orders);
} catch (error) {
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 => `
<div class="order card mb-2">
<div class="card-body">
@ -55,10 +65,4 @@ document.addEventListener('DOMContentLoaded', async () => {
`;
container.insertAdjacentHTML('beforeend', orderHTML);
});
} catch (error) {
console.error('Error:', error);
alert('Nie udało się załadować historii zamówień');
}
});
}

30
static/js/utils.js Normal file
View file

@ -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 = '/';
});
}

View file

@ -67,6 +67,8 @@
</div>
</footer>
<script src="/js/profile.js"></script>
<script type="module" src="/js/utils.js"></script>
<script type="module" src="/js/auth.js"></script>
<script type="module" src="/js/profile.js"></script>
</body>
</html>