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 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
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_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
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>
|
||||
</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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
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 () => {
|
||||
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
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 () => {
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
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>
|
||||
</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>
|
||||
|
|
Loading…
Add table
Reference in a new issue