refactoring
This commit is contained in:
parent
5aee4f61bb
commit
1adcd8617a
17 changed files with 743 additions and 850 deletions
82
src/auth.rs
82
src/auth.rs
|
@ -1,39 +1,25 @@
|
||||||
use actix_web::{post, web, HttpResponse, Responder};
|
use actix_web::{post, web, HttpResponse, Responder};
|
||||||
|
use crate::models::{RegistrationData, LoginData, LoginResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||||
use serde::{Deserialize, Serialize};
|
use sqlx::PgPool;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct RegistrationData {
|
|
||||||
pub email: String,
|
|
||||||
pub haslo: String,
|
|
||||||
pub imie: String,
|
|
||||||
#[serde(rename = "confirmPassword")]
|
|
||||||
pub confirm_password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/rejestracja")]
|
#[post("/rejestracja")]
|
||||||
pub async fn rejestracja(
|
pub async fn register(
|
||||||
form: web::Json<RegistrationData>,
|
form: web::Json<RegistrationData>,
|
||||||
pool: web::Data<sqlx::PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> impl Responder {
|
) -> Result<HttpResponse, AppError> {
|
||||||
// Walidacja hasła
|
|
||||||
if form.haslo.len() < 8 {
|
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 {
|
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 = hash(&form.haslo, DEFAULT_COST).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?;
|
||||||
let hashed_password = match hash(&form.haslo, DEFAULT_COST) {
|
|
||||||
Ok(h) => h,
|
|
||||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zapisz do bazy danych
|
sqlx::query!(
|
||||||
match sqlx::query!(
|
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO uzytkownicy (email, haslo, imie)
|
INSERT INTO uzytkownicy (email, haslo, imie)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
|
@ -44,57 +30,39 @@ pub async fn rejestracja(
|
||||||
)
|
)
|
||||||
.execute(pool.get_ref())
|
.execute(pool.get_ref())
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|e| {
|
||||||
Ok(_) => HttpResponse::Created().body("Konto utworzone pomyślnie"),
|
|
||||||
Err(e) => {
|
|
||||||
if e.to_string().contains("duplicate key value") {
|
if e.to_string().contains("duplicate key value") {
|
||||||
HttpResponse::Conflict().body("Email jest już zarejestrowany")
|
AppError::BadRequest("Email jest już zarejestrowany".to_string())
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::InternalServerError().finish()
|
AppError::InternalServerError("Błąd serwera".to_string())
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
Ok(HttpResponse::Created().body("Konto utworzone pomyślnie"))
|
||||||
pub struct LoginData {
|
|
||||||
pub email: String,
|
|
||||||
pub haslo: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub token: String,
|
|
||||||
pub imie: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
form: web::Json<LoginData>,
|
form: web::Json<LoginData>,
|
||||||
pool: web::Data<sqlx::PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> impl Responder {
|
) -> Result<HttpResponse, AppError> {
|
||||||
let user = match sqlx::query!(
|
let user = sqlx::query!(
|
||||||
"SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1",
|
"SELECT id, haslo, imie FROM uzytkownicy WHERE email = $1",
|
||||||
form.email
|
form.email
|
||||||
)
|
)
|
||||||
.fetch_optional(pool.get_ref())
|
.fetch_optional(pool.get_ref())
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))?
|
||||||
Ok(Some(u)) => u,
|
.ok_or_else(|| AppError::Unauthorized("Nieprawidłowe dane".to_string()))?;
|
||||||
Ok(None) => return HttpResponse::Unauthorized().body("Nieprawidłowe dane"),
|
|
||||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match verify(&form.haslo, &user.haslo) {
|
if verify(&form.haslo, &user.haslo).map_err(|_| AppError::InternalServerError("Błąd serwera".to_string()))? {
|
||||||
Ok(true) => {
|
|
||||||
// W praktyce użyj JWT lub innego mechanizmu autentykacji
|
|
||||||
let dummy_token = format!("user-{}-token", user.id);
|
let dummy_token = format!("user-{}-token", user.id);
|
||||||
HttpResponse::Ok().json(LoginResponse {
|
Ok(HttpResponse::Ok().json(LoginResponse {
|
||||||
token: dummy_token,
|
token: dummy_token,
|
||||||
imie: user.imie,
|
imie: user.imie,
|
||||||
})
|
}))
|
||||||
},
|
} else {
|
||||||
_ => HttpResponse::Unauthorized().body("Nieprawidłowe hasło"),
|
Err(AppError::Unauthorized("Nieprawidłowe hasło".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
67
src/books.rs
Normal file
67
src/books.rs
Normal 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
97
src/cart.rs
Normal 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
30
src/error.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
590
src/main.rs
590
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_cors::Cors;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use env_logger::{Builder, Env};
|
use env_logger::Builder;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use serde::{Deserialize, Serialize};
|
use std::env;
|
||||||
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"})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
// Inicjalizacja loggera
|
Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
|
||||||
Builder::from_env(Env::default().default_filter_or("debug")).init();
|
|
||||||
|
|
||||||
// Ładowanie zmiennych środowiskowych
|
|
||||||
dotenv().ok();
|
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()
|
let pool = PgPoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.connect(&database_url)
|
.connect(&database_url)
|
||||||
|
@ -570,10 +30,9 @@ async fn main() -> std::io::Result<()> {
|
||||||
.allow_any_origin()
|
.allow_any_origin()
|
||||||
.allowed_methods(vec!["GET", "POST", "DELETE"])
|
.allowed_methods(vec!["GET", "POST", "DELETE"])
|
||||||
.allowed_headers(vec![
|
.allowed_headers(vec![
|
||||||
header::CONTENT_TYPE,
|
actix_web::http::header::CONTENT_TYPE,
|
||||||
header::AUTHORIZATION,
|
actix_web::http::header::AUTHORIZATION,
|
||||||
header::ACCEPT,
|
actix_web::http::header::ACCEPT,
|
||||||
header::HeaderName::from_static("content-type"),
|
|
||||||
])
|
])
|
||||||
.supports_credentials();
|
.supports_credentials();
|
||||||
|
|
||||||
|
@ -581,17 +40,14 @@ async fn main() -> std::io::Result<()> {
|
||||||
.app_data(web::Data::new(pool.clone()))
|
.app_data(web::Data::new(pool.clone()))
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.wrap(actix_web::middleware::Logger::default())
|
.wrap(actix_web::middleware::Logger::default())
|
||||||
.service(get_ksiazki)
|
.service(books::get_books)
|
||||||
.service(get_ksiazka)
|
.service(books::get_book)
|
||||||
.service(rejestracja)
|
.service(auth::register)
|
||||||
.service(login)
|
.service(auth::login)
|
||||||
.service(get_cart)
|
.service(cart::get_cart)
|
||||||
.service(add_to_cart) // Dodaj
|
.service(cart::add_to_cart)
|
||||||
.service(checkout) // Dodaj
|
.service(cart::remove_from_cart)
|
||||||
.service(check_auth)
|
.service(profile::get_order_history)
|
||||||
.service(remove_from_cart)
|
|
||||||
.service(decrease_cart_item)
|
|
||||||
.service(get_order_history)
|
|
||||||
.service(
|
.service(
|
||||||
Files::new("/images", "./static/images")
|
Files::new("/images", "./static/images")
|
||||||
.show_files_listing(),
|
.show_files_listing(),
|
||||||
|
|
51
src/models.rs
Normal file
51
src/models.rs
Normal 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
97
src/profile.rs
Normal 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))
|
||||||
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<p id="book-description"></p>
|
<p id="book-description"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn">Dodaj do koszyka</button>
|
<button class="btn btn-add-to-cart">Dodaj do koszyka</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -76,7 +76,8 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<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 type="module" src="/js/utils.js"></script>
|
||||||
<script src="/js/book.js"></script>
|
<script type="module" src="/js/auth.js"></script>
|
||||||
|
<script type="module" src="/js/book.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -72,6 +72,8 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -61,7 +61,6 @@ footer {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
color: var(--gold1);
|
color: var(--gold1);
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
66
static/js/auth.js
Normal file
66
static/js/auth.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,3 +1,7 @@
|
||||||
|
// static/js/book.js
|
||||||
|
|
||||||
|
import { getAuthHeaders, handleApiError } from './utils.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const bookId = urlParams.get('id');
|
const bookId = urlParams.get('id');
|
||||||
|
@ -15,9 +19,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ksiazki/${bookId}`);
|
const response = await fetch(`/api/ksiazki/${bookId}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error(`Status: ${response.status}`);
|
||||||
throw new Error(`Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
const book = await response.json();
|
const book = await response.json();
|
||||||
|
|
||||||
document.getElementById('book-title').textContent = book.tytul;
|
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-description').textContent = book.opis;
|
||||||
document.getElementById('book-cover').src = book.obraz_url;
|
document.getElementById('book-cover').src = book.obraz_url;
|
||||||
|
|
||||||
// Dodaj obsługę przycisku "Dodaj do koszyka"
|
// Dodaj do koszyka
|
||||||
document.querySelector('.btn').addEventListener('click', async () => {
|
document.querySelector('.btn-add-to-cart').addEventListener('click', async () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
alert('Musisz być zalogowany, aby dodać książkę do koszyka');
|
alert('Musisz być zalogowany, aby dodać książkę do koszyka');
|
||||||
|
@ -38,10 +40,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/add-to-cart', {
|
const response = await fetch('/api/add-to-cart', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
book_id: parseInt(bookId),
|
book_id: parseInt(bookId),
|
||||||
quantity: 1
|
quantity: 1
|
||||||
|
@ -51,16 +50,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert('Dodano do koszyka!');
|
alert('Dodano do koszyka!');
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
throw new Error('Wystąpił błąd');
|
||||||
alert(error.error || 'Wystąpił błąd');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Błąd:', error);
|
handleApiError(error);
|
||||||
alert('Wystąpił błąd podczas dodawania do koszyka');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Błąd:', error);
|
|
||||||
bookDetails.innerHTML = `
|
bookDetails.innerHTML = `
|
||||||
<div class="col-12 text-center py-5">
|
<div class="col-12 text-center py-5">
|
||||||
<h2 class="text-danger">Błąd ładowania książki</h2>
|
<h2 class="text-danger">Błąd ładowania książki</h2>
|
||||||
|
|
66
static/js/books.js
Normal file
66
static/js/books.js
Normal 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);
|
||||||
|
});
|
|
@ -1,3 +1,7 @@
|
||||||
|
// static/js/cart.js
|
||||||
|
|
||||||
|
import { getAuthHeaders, handleApiError } from './utils.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
@ -7,14 +11,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/cart', {
|
const response = await fetch('/api/cart', {
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Błąd ładowania koszyka');
|
if (!response.ok) throw new Error('Błąd ładowania koszyka');
|
||||||
|
|
||||||
const cartItems = await response.json();
|
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');
|
const container = document.getElementById('cart-items');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
@ -26,11 +35,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
let totalCartValue = 0;
|
let totalCartValue = 0;
|
||||||
|
|
||||||
cartItems.forEach(item => {
|
cartItems.forEach(item => {
|
||||||
// Formatowanie cen
|
|
||||||
const price = parseFloat(item.cena);
|
const price = parseFloat(item.cena);
|
||||||
const formattedPrice = price.toFixed(2);
|
|
||||||
const itemTotal = price * item.quantity;
|
const itemTotal = price * item.quantity;
|
||||||
const formattedTotal = itemTotal.toFixed(2);
|
|
||||||
totalCartValue += itemTotal;
|
totalCartValue += itemTotal;
|
||||||
|
|
||||||
const itemHTML = `
|
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-wrap gap-3 mb-2 cart-item-info mt-auto">
|
||||||
<div class="d-flex flex-column flex-grow-1">
|
<div class="d-flex flex-column flex-grow-1">
|
||||||
<span class="fw-bold cart-item-price">Cena jednostkowa</span>
|
<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>
|
||||||
|
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
|
@ -65,7 +71,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<span class="fw-bold cart-item-total">Suma</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -90,7 +96,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
container.insertAdjacentHTML('beforeend', itemHTML);
|
container.insertAdjacentHTML('beforeend', itemHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dodaj całkowitą sumę koszyka
|
// Suma całkowita
|
||||||
const totalHTML = `
|
const totalHTML = `
|
||||||
<div class="col-12 mt-4">
|
<div class="col-12 mt-4">
|
||||||
<div class="card dark-card p-4">
|
<div class="card dark-card p-4">
|
||||||
|
@ -103,27 +109,24 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
`;
|
`;
|
||||||
container.insertAdjacentHTML('beforeend', totalHTML);
|
container.insertAdjacentHTML('beforeend', totalHTML);
|
||||||
|
|
||||||
// Obsługa przycisku zmniejszania ilości
|
// Obsługa przycisków
|
||||||
document.querySelectorAll('.decrease-quantity').forEach(button => {
|
document.querySelectorAll('.decrease-quantity').forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
const bookId = this.getAttribute('data-book-id');
|
const bookId = this.dataset.bookId;
|
||||||
updateCartItemQuantity(bookId, 'decrease');
|
updateCartItemQuantity(bookId, 'decrease');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Obsługa przycisku usuwania
|
|
||||||
document.querySelectorAll('.remove-from-cart').forEach(button => {
|
document.querySelectorAll('.remove-from-cart').forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
const bookId = this.getAttribute('data-book-id');
|
const bookId = this.dataset.bookId;
|
||||||
updateCartItemQuantity(bookId, 'remove');
|
updateCartItemQuantity(bookId, 'remove');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
// Finalizacja zamówienia
|
||||||
console.error('Error:', error);
|
document.getElementById('checkoutBtn')?.addEventListener('click', handleCheckout);
|
||||||
alert('Nie udało się załadować koszyka');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
async function updateCartItemQuantity(bookId, action) {
|
async function updateCartItemQuantity(bookId, action) {
|
||||||
try {
|
try {
|
||||||
|
@ -133,36 +136,28 @@ async function updateCartItemQuantity(bookId, action) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let endpoint, method;
|
let method, endpoint;
|
||||||
|
|
||||||
if (action === 'decrease') {
|
if (action === 'decrease') {
|
||||||
endpoint = `/api/decrease-cart-item/${bookId}`;
|
endpoint = `/api/decrease-cart-item/${bookId}`;
|
||||||
method = 'POST';
|
method = 'POST';
|
||||||
} else if (action === 'remove') {
|
} else {
|
||||||
endpoint = `/api/remove-from-cart/${bookId}`;
|
endpoint = `/api/remove-from-cart/${bookId}`;
|
||||||
method = 'DELETE';
|
method = 'DELETE';
|
||||||
} else {
|
|
||||||
throw new Error('Nieznana akcja');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: method,
|
method,
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Błąd aktualizacji koszyka');
|
if (!response.ok) throw new Error('Błąd aktualizacji koszyka');
|
||||||
|
|
||||||
// Odśwież koszyk
|
|
||||||
location.reload();
|
location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
handleApiError(error);
|
||||||
alert('Nie udało się zaktualizować koszyka');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFromCart(bookId) {
|
async function handleCheckout() {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -170,64 +165,30 @@ async function removeFromCart(bookId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/remove-from-cart/${bookId}`, {
|
// Pobierz zawartość koszyka
|
||||||
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
|
|
||||||
const cartResponse = await fetch('/api/cart', {
|
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();
|
const cartItems = await cartResponse.json();
|
||||||
|
|
||||||
// Przygotuj dane do zamówienia
|
// Wyślij zamówienie
|
||||||
const checkoutData = {
|
const response = await fetch('/api/checkout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
items: cartItems.map(item => ({
|
items: cartItems.map(item => ({
|
||||||
book_id: item.book_id,
|
book_id: item.book_id,
|
||||||
quantity: item.quantity
|
quantity: item.quantity
|
||||||
})),
|
})),
|
||||||
total: cartItems.reduce((sum, item) =>
|
total: cartItems.reduce((sum, item) =>
|
||||||
sum + (parseFloat(item.cena) * item.quantity), 0)
|
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');
|
if (!response.ok) throw new Error('Błąd podczas składania zamówienia');
|
||||||
|
|
||||||
window.location.href = '/thankyou.html';
|
window.location.href = '/thankyou.html';
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
handleApiError(error, 'Nie udało się złożyć zamówienia');
|
||||||
alert('Nie udało się złożyć zamówienia: ' + error.message);
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
// static/js/profile.js
|
||||||
|
|
||||||
|
import { getAuthHeaders, handleApiError } from './utils.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
@ -7,19 +11,25 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/order-history', {
|
const response = await fetch('/api/order-history', {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: getAuthHeaders()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Błąd ładowania historii');
|
if (!response.ok) throw new Error('Błąd ładowania historii');
|
||||||
|
|
||||||
const orders = await response.json();
|
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');
|
const container = document.getElementById('order-history');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
orders.forEach((order, index) => {
|
orders.forEach((order, index) => {
|
||||||
const orderNumber = orders.length - index;
|
const orderNumber = orders.length - index;
|
||||||
|
|
||||||
const orderDate = new Date(order.data_zamowienia).toLocaleDateString();
|
const orderDate = new Date(order.data_zamowienia).toLocaleDateString();
|
||||||
|
|
||||||
const itemsList = order.items.map(item => `
|
const itemsList = order.items.map(item => `
|
||||||
<div class="order card mb-2">
|
<div class="order card mb-2">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -55,10 +65,4 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
`;
|
`;
|
||||||
container.insertAdjacentHTML('beforeend', orderHTML);
|
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
30
static/js/utils.js
Normal 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 = '/';
|
||||||
|
});
|
||||||
|
}
|
|
@ -67,6 +67,8 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Add table
Reference in a new issue