added listings
This commit is contained in:
20
.devcontainer/devcontainer.json
Normal file
20
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Rust Dev Container",
|
||||
"dockerComposeFile": [
|
||||
"../../deploy/docker-compose.dev.yml"
|
||||
],
|
||||
"service": "api_rust", // Replace with your Rust service name in docker-compose.dev.yml
|
||||
"workspaceFolder": "/mnt/code/newenglandoilbio/api_rust",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["rust-lang.rust-analyzer"],
|
||||
"settings": {
|
||||
"rust-analyzer.server.path": null,
|
||||
"rust-analyzer.cargo.sysroot": null,
|
||||
"rust-analyzer.enable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"remoteUser": "root", // Adjust to match your container's user
|
||||
"shutdownAction": "stopCompose"
|
||||
}
|
||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -69,6 +69,7 @@ dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"dotenv",
|
||||
"hyper",
|
||||
"jsonwebtoken",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -126,6 +127,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
@@ -524,6 +526,30 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"headers-core",
|
||||
"http",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"sha1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers-core"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
|
||||
dependencies = [
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6.20"
|
||||
axum = { version = "0.6.20", features = ["headers"] }
|
||||
tokio = { version = "1.35.1", features = ["full"] }
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "chrono"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -14,3 +14,4 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
dotenv = "0.15"
|
||||
tower-http = { version = "0.4", features = ["cors"] }
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
hyper = "0.14"
|
||||
|
||||
1
alter_column.sql
Normal file
1
alter_column.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE listings ALTER COLUMN price_per_gallon TYPE DOUBLE PRECISION;
|
||||
1
drop_column.sql
Normal file
1
drop_column.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE listings DROP COLUMN company_id;
|
||||
52
schema.sql
52
schema.sql
@@ -2,8 +2,58 @@ CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL,
|
||||
created TIMESTAMPTZ,
|
||||
email VARCHAR(255),
|
||||
last_login TIMESTAMPTZ,
|
||||
owner INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE service_categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
clicks_total INTEGER DEFAULT 0,
|
||||
total_companies INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE companies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
active BOOLEAN DEFAULT true,
|
||||
created DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
address VARCHAR(255),
|
||||
town VARCHAR(255),
|
||||
state VARCHAR(2),
|
||||
phone VARCHAR(20),
|
||||
owner_name VARCHAR(255),
|
||||
owner_phone_number VARCHAR(20),
|
||||
email VARCHAR(255),
|
||||
user_id INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE listings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_name VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
price_per_gallon DOUBLE PRECISION NOT NULL,
|
||||
price_per_gallon_cash DOUBLE PRECISION,
|
||||
note TEXT,
|
||||
minimum_order INTEGER,
|
||||
service BOOLEAN DEFAULT false,
|
||||
bio_percent INTEGER NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
online_ordering VARCHAR(20) NOT NULL DEFAULT 'none',
|
||||
county_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
last_edited TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- If the table already exists, add the new columns
|
||||
-- ALTER TABLE listings ADD COLUMN price_per_gallon_cash DOUBLE PRECISION;
|
||||
-- ALTER TABLE listings ADD COLUMN note TEXT;
|
||||
-- ALTER TABLE listings ADD COLUMN minimum_order INTEGER;
|
||||
-- ALTER TABLE listings ADD COLUMN last_edited TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- If the table already exists, drop the company_id column
|
||||
-- ALTER TABLE listings DROP COLUMN company_id;
|
||||
|
||||
37
seed_categories.sql
Normal file
37
seed_categories.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Seed data for service_categories table
|
||||
INSERT INTO service_categories (name, description, clicks_total, total_companies) VALUES
|
||||
('Landscaping - Lawn care, tree trimming, and garden design', 'Professional landscaping services including lawn maintenance, tree trimming, garden design, and outdoor space enhancement.', 0, 0),
|
||||
('Snowplowing - Snow removal from driveways and walkways', 'Reliable snow removal services to clear driveways, walkways, and parking areas during winter storms.', 0, 0),
|
||||
('Roofing - Roof repairs, replacements, and inspections', 'Comprehensive roofing services including repairs, complete replacements, and routine inspections to maintain your roof.', 0, 0),
|
||||
('Plumbing - Fixing leaks, installing fixtures, and unclogging drains', 'Expert plumbing services for leak repairs, fixture installation, drain cleaning, and all plumbing needs.', 0, 0),
|
||||
('HVAC - Heating, ventilation, and air conditioning maintenance and repair', 'Complete HVAC services including heating repair, air conditioning service, ventilation system maintenance, and energy efficiency upgrades.', 0, 0),
|
||||
('Electrical - Wiring, lighting installation, and electrical repairs', 'Professional electrical services for wiring, lighting installation, outlet repairs, and electrical safety inspections.', 0, 0),
|
||||
('Pest Control - Extermination of insects, rodents, and other pests', 'Effective pest control services for eliminating insects, rodents, and other unwanted pests from your property.', 0, 0),
|
||||
('House Cleaning - Regular or deep cleaning services', 'Thorough house cleaning services including regular maintenance cleaning and deep cleaning for a fresh, organized home.', 0, 0),
|
||||
('Window Cleaning - Exterior and interior window washing', 'Professional window cleaning for both exterior and interior surfaces, ensuring crystal clear views and streak-free glass.', 0, 0),
|
||||
('Gutter Cleaning - Removing debris from gutters to prevent water damage', 'Gutter cleaning and maintenance to remove leaves, debris, and prevent water damage and foundation issues.', 0, 0),
|
||||
('Painting - Interior and exterior painting for aesthetic and protection', 'Quality painting services for interior rooms and exterior surfaces, enhancing both appearance and protection.', 0, 0),
|
||||
('Carpentry - Building or repairing decks, fences, and furniture', 'Skilled carpentry work including deck construction, fence repair, furniture building, and custom woodwork.', 0, 0),
|
||||
('Masonry - Brickwork, stonework, and chimney repairs', 'Expert masonry services for brickwork, stone installations, chimney repairs, and stone structure maintenance.', 0, 0),
|
||||
('Siding Installation/Repair - Maintaining or replacing exterior siding', 'Siding services including installation, repair, and replacement to protect and enhance your home exterior.', 0, 0),
|
||||
('Pressure Washing - Cleaning driveways, decks, and home exteriors', 'High-pressure cleaning services for driveways, decks, siding, and other exterior surfaces to restore appearance.', 0, 0),
|
||||
('Tree Services - Tree removal, pruning, and stump grinding', 'Professional tree care including removal of hazardous trees, pruning, trimming, and stump grinding services.', 0, 0),
|
||||
('Septic System Services - Pumping, maintenance, and repairs for septic tanks', 'Septic system maintenance including pumping, inspections, repairs, and regular servicing to prevent system failure.', 0, 0),
|
||||
('Well Water Services - Maintenance and testing for private wells', 'Well water services including maintenance, testing, filtration, and repair for private water well systems.', 0, 0),
|
||||
('Home Security Installation - Alarm systems, cameras, and smart locks', 'Home security installation including alarm systems, surveillance cameras, smart locks, and security monitoring.', 0, 0),
|
||||
('Locksmith Services - Lock repairs, replacements, and rekeying', 'Professional locksmith services for lock repair, replacement, rekeying, and emergency lockout assistance.', 0, 0),
|
||||
('Appliance Repair - Fixing refrigerators, washers, dryers, and more', 'Appliance repair services for all major household appliances including refrigerators, washers, dryers, and ovens.', 0, 0),
|
||||
('Garage Door Services - Installation, repair, and maintenance of garage doors', 'Garage door services including installation, repair, maintenance, and opener replacement for residential and commercial doors.', 0, 0),
|
||||
('Foundation Repair - Addressing cracks or structural issues in foundations', 'Foundation repair services to address cracks, settling, and structural issues in home and building foundations.', 0, 0),
|
||||
('Waterproofing - Basement or crawlspace waterproofing to prevent leaks', 'Waterproofing services for basements and crawlspaces to prevent water intrusion and moisture damage.', 0, 0),
|
||||
('Mold Remediation - Removing mold and addressing moisture issues', 'Professional mold remediation services including identification, removal, and moisture control to protect your health.', 0, 0),
|
||||
('Insulation Services - Installing or upgrading insulation for energy efficiency', 'Insulation installation and upgrading services to improve energy efficiency, comfort, and reduce heating/cooling costs.', 0, 0),
|
||||
('Drywall Installation/Repair - Fixing holes or installing new drywall', 'Drywall services including installation, repair of holes and damage, and finishing for smooth, professional walls.', 0, 0),
|
||||
('Flooring Services - Installing or repairing hardwood, tile, or carpet', 'Flooring installation and repair services for hardwood, tile, carpeting, laminate, and other flooring types.', 0, 0),
|
||||
('Carpet Cleaning - Deep cleaning or stain removal for carpets', 'Professional carpet cleaning services including deep cleaning, stain removal, and maintenance to extend carpet life.', 0, 0),
|
||||
('Chimney Sweep - Cleaning chimneys to ensure safe fireplace use', 'Chimney sweeping and inspection services to clean creosote buildup and ensure safe, efficient fireplace operation.', 0, 0),
|
||||
('Pool Maintenance - Cleaning, repairs, and chemical balancing for pools', 'Complete pool maintenance services including cleaning, repairs, chemical balancing, and seasonal maintenance.', 0, 0),
|
||||
('Fence Installation/Repair - Building or fixing fences for privacy or security', 'Fence installation and repair services including new fence construction and fixing damaged sections for privacy and security.', 0, 0),
|
||||
('Home Inspection - Pre-purchase or routine home condition assessments', 'Comprehensive home inspection services for pre-purchase evaluations or routine condition assessments.', 0, 0),
|
||||
('Window Replacement - Installing energy-efficient windows or repairing frames', 'Window replacement and repair services including energy-efficient window installation and frame repairs.', 0, 0),
|
||||
('Junk Removal - Hauling away unwanted items or debris', 'Junk removal services for clearing unwanted items, debris, and clutter from homes, offices, and construction sites.', 0, 0);
|
||||
@@ -1,25 +1,17 @@
|
||||
use axum::{
|
||||
extract::{State, Json},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
routing::post,
|
||||
Router,
|
||||
body::Body,
|
||||
http::Request as HttpRequest,
|
||||
};
|
||||
use crate::auth::structs::{AppState, User, RegisterRequest, LoginRequest, Claims};
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
use jsonwebtoken::{encode, Header, EncodingKey};
|
||||
|
||||
|
||||
// Create auth router
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/auth/register", post(register))
|
||||
.route("/auth/login", post(login))
|
||||
}
|
||||
|
||||
use jsonwebtoken::{decode, encode, Header, EncodingKey, DecodingKey, Validation};
|
||||
|
||||
// A helper function to convert any error into a 500 Internal Server Error response.
|
||||
fn internal_error<E>(err: E) -> Response
|
||||
@@ -38,7 +30,7 @@ where
|
||||
}
|
||||
|
||||
|
||||
async fn register(
|
||||
pub async fn register(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -94,8 +86,8 @@ pub async fn login(
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// 1. Fetch user from the database
|
||||
let user = match sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = $1")
|
||||
.bind(&payload.username)
|
||||
let user = match sqlx::query_as::<_, User>("SELECT * FROM users WHERE TRIM(username) = $1")
|
||||
.bind(&payload.username.trim())
|
||||
.fetch_optional(&*state.db)
|
||||
.await
|
||||
{
|
||||
@@ -157,3 +149,55 @@ pub async fn login(
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({ "token": token, "user": user }))).into_response()
|
||||
}
|
||||
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<AppState>,
|
||||
mut request: HttpRequest<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
// Manually extract Authorization header
|
||||
let auth_header = match request.headers().get(axum::http::header::AUTHORIZATION) {
|
||||
Some(header) => header.to_str().ok(),
|
||||
None => return Err((StatusCode::UNAUTHORIZED, Json(serde_json::json!({"error": "Missing authorization header"}))).into_response()),
|
||||
};
|
||||
|
||||
let token = match auth_header.and_then(|h| h.strip_prefix("Bearer ")) {
|
||||
Some(t) => t,
|
||||
None => return Err((StatusCode::UNAUTHORIZED, Json(serde_json::json!({"error": "Invalid authorization header"}))).into_response()),
|
||||
};
|
||||
|
||||
let claims = match decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(state.jwt_secret.as_bytes()),
|
||||
&Validation::default(),
|
||||
) {
|
||||
Ok(token_data) => token_data.claims,
|
||||
Err(_) => return Err((StatusCode::UNAUTHORIZED, Json(serde_json::json!({"error": "Invalid token"}))).into_response()),
|
||||
};
|
||||
|
||||
// Fetch user from database using username from claims
|
||||
// Note: Database might pad CHAR fields with spaces, so we trim the username
|
||||
let trimmed_username = claims.sub.trim();
|
||||
eprintln!("Looking up user: '{}' (trimmed: '{}')", &claims.sub, trimmed_username);
|
||||
let user = match sqlx::query_as::<_, User>("SELECT * FROM users WHERE TRIM(username) = $1")
|
||||
.bind(trimmed_username)
|
||||
.fetch_one(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(user) => {
|
||||
eprintln!("Found user: {}", user.username.trim());
|
||||
user
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Database error finding user '{}' : {:?}", trimmed_username, e);
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "User not found"}))).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
// Insert the user and state into request extensions
|
||||
request.extensions_mut().insert(user);
|
||||
request.extensions_mut().insert(state.clone());
|
||||
|
||||
// Proceed to the next handler
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@ use chrono::NaiveDateTime;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db_url: String,
|
||||
pub db: Arc<PgPool>,
|
||||
pub jwt_secret: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
|
||||
33
src/company/category.rs
Normal file
33
src/company/category.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::Json,
|
||||
http::StatusCode,
|
||||
};
|
||||
use crate::auth::structs::AppState;
|
||||
use crate::company::structs::ServiceCategory;
|
||||
use crate::state::structs::ErrorResponse;
|
||||
|
||||
pub async fn get_all_categories(
|
||||
State(app_state): State<AppState>,
|
||||
) -> Result<Json<Vec<ServiceCategory>>, (StatusCode, Json<ErrorResponse>)> {
|
||||
println!("Querying all service categories");
|
||||
|
||||
match sqlx::query_as::<_, ServiceCategory>("SELECT id, name, description, clicks_total, total_companies
|
||||
FROM service_categories ORDER BY name ASC")
|
||||
.fetch_all(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(categories) => {
|
||||
Ok(Json(categories))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Database error fetching service categories: {}", e);
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "Failed to retrieve service categories. Please try again later.".to_string(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,284 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
http::{StatusCode, Method},
|
||||
response::{IntoResponse, Response},
|
||||
Json, Router, extract::State,
|
||||
routing::post,
|
||||
Json, extract::{State, Extension},
|
||||
body::Body,
|
||||
http::Request,
|
||||
};
|
||||
use crate::auth::structs::AppState;
|
||||
use sqlx::query_as;
|
||||
use crate::auth::structs::{AppState, User};
|
||||
use crate::company::structs::Company;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn company_routes() -> Router<AppState> {
|
||||
let router = Router::new().route("/company", post(update_or_create_company));
|
||||
router
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CompanyRequest {
|
||||
pub name: String,
|
||||
pub address: Option<String>,
|
||||
pub town: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub owner_name: Option<String>,
|
||||
pub owner_phone_number: Option<String>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
// Define a simple error type. This is necessary for the Result pattern.
|
||||
#[allow(dead_code)]
|
||||
pub enum AppError {
|
||||
Internal(String),
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
pub async fn update_or_create_company(
|
||||
// Teach Axum how to convert our error into a response.
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
||||
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
|
||||
};
|
||||
(status, Json(json!({"success": false, "error": error_message}))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// COMPANY HANDLERS
|
||||
// ===============================================================
|
||||
|
||||
pub async fn get_company(
|
||||
State(state): State<AppState>,
|
||||
Json(company): Json<Company>,
|
||||
Extension(user): Extension<User>,
|
||||
) -> Response {
|
||||
let result = query_as::<_, Company>(
|
||||
"SELECT * FROM company WHERE id = $1",
|
||||
match sqlx::query_as::<_, Company>(
|
||||
"SELECT * FROM company WHERE user_id = $1 AND active = true"
|
||||
)
|
||||
.bind(company.id)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(Some(company)) => (StatusCode::OK, Json(company)).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, Json(json!({"error": "Company not found"}))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(existing_company) => {
|
||||
match existing_company {
|
||||
Some(_) => {
|
||||
// Update existing company
|
||||
let result = sqlx::query(
|
||||
"UPDATE company SET active = $2, name = $3, address = $4, town = $5, state = $6, phone = $7, owner_name = $8, owner_phone_number = $9, email = $10, user_id = $11 WHERE id = $1"
|
||||
pub async fn create_company(
|
||||
State(state): State<AppState>,
|
||||
Extension(user): Extension<User>,
|
||||
Json(payload): Json<CompanyRequest>,
|
||||
) -> Response {
|
||||
// Check if company already exists
|
||||
match sqlx::query("SELECT 1 FROM company WHERE user_id = $1 AND active = true")
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "Company already exists"}))).into_response(),
|
||||
Ok(None) => {},
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
|
||||
match sqlx::query_as::<_, Company>(
|
||||
"INSERT INTO company (name, address, town, state, phone, owner_name, owner_phone_number, email, user_id, active, created) VALUES ($1, $2, $3, $4::text, $5, $6, $7, $8, $9, true, CURRENT_DATE) RETURNING *"
|
||||
)
|
||||
.bind(company.id)
|
||||
.bind(company.active)
|
||||
.bind(company.name)
|
||||
.bind(company.address)
|
||||
.bind(company.town)
|
||||
.bind(company.state)
|
||||
.bind(company.phone)
|
||||
.bind(company.owner_name)
|
||||
.bind(company.owner_phone_number)
|
||||
.bind(company.email)
|
||||
.bind(company.user_id)
|
||||
.execute(&*state.db)
|
||||
.await;
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.address)
|
||||
.bind(&payload.town)
|
||||
.bind(&payload.state)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.owner_name)
|
||||
.bind(&payload.owner_phone_number)
|
||||
.bind(&payload.email)
|
||||
.bind(user.id)
|
||||
.fetch_one(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(company) => (StatusCode::CREATED, Json(company)).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(_) => (StatusCode::OK, "Company updated successfully".to_string()).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error updating company: {}", e)).into_response(),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Create new company
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO company (active, created, name, address, town, state, phone, owner_name, owner_phone_number, email, user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
|
||||
pub async fn update_company(
|
||||
State(state): State<AppState>,
|
||||
Extension(user): Extension<User>,
|
||||
Json(payload): Json<CompanyRequest>,
|
||||
) -> Response {
|
||||
match sqlx::query_as::<_, Company>(
|
||||
"UPDATE company SET name = $1, address = $2, town = $3, state = $4::text, phone = $5, owner_name = $6, owner_phone_number = $7, email = $8 WHERE user_id = $9 AND active = true RETURNING *"
|
||||
)
|
||||
.bind(company.active)
|
||||
.bind(chrono::Utc::now().naive_utc().date())
|
||||
.bind(company.name)
|
||||
.bind(company.address)
|
||||
.bind(company.town)
|
||||
.bind(company.state)
|
||||
.bind(company.phone)
|
||||
.bind(company.owner_name)
|
||||
.bind(company.owner_phone_number)
|
||||
.bind(company.email)
|
||||
.bind(company.user_id)
|
||||
.execute(&*state.db)
|
||||
.await;
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.address)
|
||||
.bind(&payload.town)
|
||||
.bind(&payload.state)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.owner_name)
|
||||
.bind(&payload.owner_phone_number)
|
||||
.bind(&payload.email)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(company)) => (StatusCode::OK, Json(company)).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, Json(json!({"error": "Company not found"}))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(_) => (StatusCode::CREATED, "Company created successfully".to_string()).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating company: {}", e)).into_response(),
|
||||
pub async fn delete_company(
|
||||
State(state): State<AppState>,
|
||||
Extension(user): Extension<User>,
|
||||
) -> Response {
|
||||
match sqlx::query("UPDATE company SET active = false WHERE user_id = $1 AND active = true")
|
||||
.bind(user.id)
|
||||
.execute(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.rows_affected() == 0 {
|
||||
(StatusCode::NOT_FOUND, Json(json!({"error": "Company not found"}))).into_response()
|
||||
} else {
|
||||
(StatusCode::OK, Json(json!({"success": true, "message": "Company deleted"}))).into_response()
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error checking for existing company: {}", e)).into_response(),
|
||||
|
||||
pub async fn company_handler(
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
// Extract user and state from extensions
|
||||
let user = match request.extensions().get::<User>().cloned() {
|
||||
Some(user) => user,
|
||||
None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "Unauthorized"}))).into_response(),
|
||||
};
|
||||
|
||||
let state = match request.extensions().get::<AppState>().cloned() {
|
||||
Some(state) => state,
|
||||
None => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "State not found"}))).into_response(),
|
||||
};
|
||||
|
||||
let method = request.method().clone();
|
||||
|
||||
match method {
|
||||
Method::GET => get_company_logic(&state, &user).await,
|
||||
Method::POST => {
|
||||
let body = match hyper::body::to_bytes(request.into_body()).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "Invalid body"}))).into_response(),
|
||||
};
|
||||
let payload: CompanyRequest = match serde_json::from_slice(&body) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "Invalid JSON"}))).into_response(),
|
||||
};
|
||||
create_company_logic(&state, &user, payload).await
|
||||
}
|
||||
Method::PUT => {
|
||||
let body = match hyper::body::to_bytes(request.into_body()).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "Invalid body"}))).into_response(),
|
||||
};
|
||||
let payload: CompanyRequest = match serde_json::from_slice(&body) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "Invalid JSON"}))).into_response(),
|
||||
};
|
||||
update_company_logic(&state, &user, payload).await
|
||||
}
|
||||
Method::DELETE => delete_company_logic(&state, &user).await,
|
||||
_ => (StatusCode::METHOD_NOT_ALLOWED, Json(json!({"error": "Method not allowed"}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_company_logic(state: &AppState, user: &User) -> Response {
|
||||
match sqlx::query_as::<_, Company>(
|
||||
"SELECT * FROM company WHERE user_id = $1 AND active = true"
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(company)) => (StatusCode::OK, Json(company)).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, Json(json!({"error": "Company not found"}))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_company_logic(state: &AppState, user: &User, payload: CompanyRequest) -> Response {
|
||||
// Check if company already exists
|
||||
match sqlx::query("SELECT 1 FROM company WHERE user_id = $1 AND active = true")
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "Company already exists"}))).into_response(),
|
||||
Ok(None) => {},
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
|
||||
match sqlx::query_as::<_, Company>(
|
||||
"INSERT INTO company (name, address, town, state, phone, owner_name, owner_phone_number, email, user_id, active, created) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, true, CURRENT_DATE) RETURNING *"
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.address)
|
||||
.bind(&payload.town)
|
||||
.bind(&payload.state)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.owner_name)
|
||||
.bind(&payload.owner_phone_number)
|
||||
.bind(&payload.email)
|
||||
.bind(user.id)
|
||||
.fetch_one(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(company) => (StatusCode::CREATED, Json(company)).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_company_logic(state: &AppState, user: &User, payload: CompanyRequest) -> Response {
|
||||
eprintln!("Updating company for user {}: {:?}", user.id, payload);
|
||||
match sqlx::query_as::<_, Company>(
|
||||
"UPDATE company SET name = $1, address = $2, town = $3, state = $4::text, phone = $5, owner_name = $6, owner_phone_number = $7, email = $8 WHERE user_id = $9 AND active = true RETURNING *"
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.address)
|
||||
.bind(&payload.town)
|
||||
.bind(&payload.state)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.owner_name)
|
||||
.bind(&payload.owner_phone_number)
|
||||
.bind(&payload.email)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(company)) => {
|
||||
eprintln!("Updated company successfully");
|
||||
(StatusCode::OK, Json(company)).into_response()
|
||||
},
|
||||
Ok(None) => {
|
||||
eprintln!("No company found to update, creating new one");
|
||||
create_company_logic(state, user, payload).await
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Database error updating company: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_company_logic(state: &AppState, user: &User) -> Response {
|
||||
match sqlx::query("UPDATE company SET active = false WHERE user_id = $1 AND active = true")
|
||||
.bind(user.id)
|
||||
.execute(&*state.db)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.rows_affected() == 0 {
|
||||
(StatusCode::NOT_FOUND, Json(json!({"error": "Company not found"}))).into_response()
|
||||
} else {
|
||||
(StatusCode::OK, Json(json!({"success": true, "message": "Company deleted"}))).into_response()
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod company;
|
||||
pub mod category;
|
||||
pub mod structs;
|
||||
@@ -4,6 +4,7 @@ use sqlx::FromRow;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Company {
|
||||
pub id: i32,
|
||||
pub active: bool,
|
||||
@@ -12,10 +13,19 @@ pub struct Company {
|
||||
pub address: Option<String>,
|
||||
pub town: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub phone: Option<i64>,
|
||||
pub phone: Option<String>,
|
||||
pub owner_name: Option<String>,
|
||||
pub owner_phone_number: Option<i64>,
|
||||
pub owner_phone_number: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub user_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ServiceCategory {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub clicks_total: i32,
|
||||
pub total_companies: i32,
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use crate::auth::structs::AppState;
|
||||
|
||||
// Define the handler for the /user/ endpoint
|
||||
async fn get_user(State(state): State<AppState>) -> impl IntoResponse {
|
||||
pub async fn get_user(State(state): State<AppState>) -> impl IntoResponse {
|
||||
// Placeholder for user data retrieval logic
|
||||
// In a real application, you would query the database using state.db
|
||||
let users = sqlx::query("SELECT * FROM users").fetch_all(&*state.db).await;
|
||||
@@ -17,9 +15,3 @@ async fn get_user(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to retrieve users: {}", e)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// Define the router for the data module
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/user/", get(get_user))
|
||||
}
|
||||
|
||||
240
src/listing/data.rs
Normal file
240
src/listing/data.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use axum::{
|
||||
extract::{Path, State, Extension},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use crate::auth::structs::{AppState, User};
|
||||
use crate::company::structs::Company;
|
||||
use crate::listing::structs::{Listing, CreateListingRequest, UpdateListingRequest};
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn get_listings(
|
||||
State(app_state): State<AppState>,
|
||||
Extension(user): Extension<User>,
|
||||
) -> Result<Json<Vec<Listing>>, (StatusCode, Json<serde_json::Value>)> {
|
||||
match sqlx::query_as::<_, Listing>(
|
||||
"SELECT id, company_name, is_active, price_per_gallon, price_per_gallon_cash, note, minimum_order, service, bio_percent, phone, online_ordering, county_id, user_id, last_edited FROM listings WHERE user_id = $1 ORDER BY id DESC"
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_all(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(listings) => Ok(Json(listings)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": format!("Failed to fetch listings: {}", e)}))
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_listing_by_id(
|
||||
State(app_state): State<AppState>,
|
||||
Path(listing_id): Path<i32>,
|
||||
Extension(user): Extension<User>,
|
||||
) -> Result<Json<Listing>, (StatusCode, Json<serde_json::Value>)> {
|
||||
match sqlx::query_as::<_, Listing>(
|
||||
"SELECT id, company_name, is_active, price_per_gallon, price_per_gallon_cash, note, minimum_order, service, bio_percent, phone, online_ordering, county_id, user_id, last_edited FROM listings WHERE id = $1 AND user_id = $2"
|
||||
)
|
||||
.bind(listing_id)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(listing)) => Ok(Json(listing)),
|
||||
Ok(None) => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({"error": "Listing not found"}))
|
||||
)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": format!("Failed to fetch listing: {}", e)}))
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_listing(
|
||||
State(app_state): State<AppState>,
|
||||
Extension(user): Extension<User>,
|
||||
Json(payload): Json<CreateListingRequest>,
|
||||
) -> Result<Json<Listing>, (StatusCode, Json<serde_json::Value>)> {
|
||||
eprintln!("DEBUG: Starting create_listing for user_id: {}", user.id);
|
||||
eprintln!("DEBUG: Payload: {:?}", payload);
|
||||
|
||||
// Create the listing directly without company validation
|
||||
match sqlx::query_as::<_, Listing>(
|
||||
"INSERT INTO listings (company_name, is_active, price_per_gallon, price_per_gallon_cash, note, minimum_order, service, bio_percent, phone, online_ordering, county_id, user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, company_name, is_active, price_per_gallon, price_per_gallon_cash, note, minimum_order, service, bio_percent, phone, online_ordering, county_id, user_id, last_edited"
|
||||
)
|
||||
.bind(&payload.company_name)
|
||||
.bind(payload.is_active)
|
||||
.bind(payload.price_per_gallon)
|
||||
.bind(payload.price_per_gallon_cash)
|
||||
.bind(&payload.note)
|
||||
.bind(payload.minimum_order)
|
||||
.bind(payload.service)
|
||||
.bind(payload.bio_percent)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.online_ordering)
|
||||
.bind(payload.county_id)
|
||||
.bind(user.id)
|
||||
.fetch_one(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(listing) => {
|
||||
eprintln!("DEBUG: Successfully created listing: {:?}", listing);
|
||||
Ok(Json(listing))
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("DEBUG: Error creating listing: {:?}", e);
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": format!("Failed to create listing: {}", e)}))
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_listing(
|
||||
State(app_state): State<AppState>,
|
||||
Path(listing_id): Path<i32>,
|
||||
Extension(user): Extension<User>,
|
||||
Json(payload): Json<UpdateListingRequest>,
|
||||
) -> Result<Json<Listing>, (StatusCode, Json<serde_json::Value>)> {
|
||||
// Build dynamic update query
|
||||
let mut query = "UPDATE listings SET ".to_string();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
let mut param_count = 1;
|
||||
|
||||
if let Some(company_name) = &payload.company_name {
|
||||
params.push(format!("company_name = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(is_active) = payload.is_active {
|
||||
params.push(format!("is_active = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(price_per_gallon) = payload.price_per_gallon {
|
||||
params.push(format!("price_per_gallon = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(price_per_gallon_cash) = payload.price_per_gallon_cash {
|
||||
params.push(format!("price_per_gallon_cash = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(note) = &payload.note {
|
||||
params.push(format!("note = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(minimum_order) = payload.minimum_order {
|
||||
params.push(format!("minimum_order = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(service) = payload.service {
|
||||
params.push(format!("service = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(bio_percent) = payload.bio_percent {
|
||||
params.push(format!("bio_percent = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(phone) = &payload.phone {
|
||||
params.push(format!("phone = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(online_ordering) = &payload.online_ordering {
|
||||
params.push(format!("online_ordering = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
if let Some(county_id) = payload.county_id {
|
||||
params.push(format!("county_id = ${}", param_count));
|
||||
param_count += 1;
|
||||
}
|
||||
|
||||
if params.is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "No fields to update"}))
|
||||
));
|
||||
}
|
||||
|
||||
query.push_str(¶ms.join(", "));
|
||||
query.push_str(&format!(" WHERE id = ${} AND user_id = ${} RETURNING *", param_count, param_count + 1));
|
||||
|
||||
// This is a simplified version - in production, you'd want to build the query more safely
|
||||
// For now, let's use a simpler approach
|
||||
match sqlx::query_as::<_, Listing>(
|
||||
"UPDATE listings SET company_name = COALESCE($1, company_name), is_active = COALESCE($2, is_active), price_per_gallon = COALESCE($3, price_per_gallon), price_per_gallon_cash = COALESCE($4, price_per_gallon_cash), note = COALESCE($5, note), minimum_order = COALESCE($6, minimum_order), service = COALESCE($7, service), bio_percent = COALESCE($8, bio_percent), phone = COALESCE($9, phone), online_ordering = COALESCE($10, online_ordering), county_id = COALESCE($11, county_id), last_edited = CURRENT_TIMESTAMP WHERE id = $12 AND user_id = $13 RETURNING id, company_name, is_active, price_per_gallon, price_per_gallon_cash, note, minimum_order, service, bio_percent, phone, online_ordering, county_id, user_id, last_edited"
|
||||
)
|
||||
.bind(&payload.company_name)
|
||||
.bind(payload.is_active)
|
||||
.bind(payload.price_per_gallon)
|
||||
.bind(payload.price_per_gallon_cash)
|
||||
.bind(&payload.note)
|
||||
.bind(payload.minimum_order)
|
||||
.bind(payload.service)
|
||||
.bind(payload.bio_percent)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.online_ordering)
|
||||
.bind(payload.county_id)
|
||||
.bind(listing_id)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(listing)) => Ok(Json(listing)),
|
||||
Ok(None) => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({"error": "Listing not found"}))
|
||||
)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": format!("Failed to update listing: {}", e)}))
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_listings_by_county(
|
||||
State(app_state): State<AppState>,
|
||||
Path(county_id): Path<i32>,
|
||||
) -> Result<Json<Vec<Listing>>, (StatusCode, Json<serde_json::Value>)> {
|
||||
match sqlx::query_as::<_, Listing>(
|
||||
"SELECT id, company_name, is_active, price_per_gallon, price_per_gallon_cash, note, minimum_order, service, bio_percent, phone, online_ordering, county_id, user_id, last_edited FROM listings WHERE county_id = $1 AND is_active = true ORDER BY last_edited DESC"
|
||||
)
|
||||
.bind(county_id)
|
||||
.fetch_all(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(listings) => Ok(Json(listings)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": format!("Failed to fetch listings: {}", e)}))
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_listing(
|
||||
State(app_state): State<AppState>,
|
||||
Path(listing_id): Path<i32>,
|
||||
Extension(user): Extension<User>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||
match sqlx::query("DELETE FROM listings WHERE id = $1 AND user_id = $2")
|
||||
.bind(listing_id)
|
||||
.bind(user.id)
|
||||
.execute(&*app_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.rows_affected() == 0 {
|
||||
Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(json!({"error": "Listing not found"}))
|
||||
))
|
||||
} else {
|
||||
Ok(Json(json!({"success": true, "message": "Listing deleted"})))
|
||||
}
|
||||
}
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": format!("Failed to delete listing: {}", e)}))
|
||||
)),
|
||||
}
|
||||
}
|
||||
2
src/listing/mod.rs
Normal file
2
src/listing/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod structs;
|
||||
pub mod data;
|
||||
52
src/listing/structs.rs
Normal file
52
src/listing/structs.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Listing {
|
||||
pub id: i32,
|
||||
pub company_name: String,
|
||||
pub is_active: bool,
|
||||
pub price_per_gallon: f64,
|
||||
pub price_per_gallon_cash: Option<f64>,
|
||||
pub note: Option<String>,
|
||||
pub minimum_order: Option<i32>,
|
||||
pub service: bool,
|
||||
pub bio_percent: i32,
|
||||
pub phone: Option<String>,
|
||||
pub online_ordering: String,
|
||||
pub county_id: i32,
|
||||
pub user_id: i32,
|
||||
pub last_edited: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateListingRequest {
|
||||
pub company_name: String,
|
||||
pub is_active: bool,
|
||||
pub price_per_gallon: f64,
|
||||
pub price_per_gallon_cash: Option<f64>,
|
||||
pub note: Option<String>,
|
||||
pub minimum_order: Option<i32>,
|
||||
pub service: bool,
|
||||
pub bio_percent: i32,
|
||||
pub phone: Option<String>,
|
||||
pub online_ordering: String,
|
||||
pub county_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateListingRequest {
|
||||
pub company_name: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub price_per_gallon: Option<f64>,
|
||||
pub price_per_gallon_cash: Option<f64>,
|
||||
pub note: Option<String>,
|
||||
pub minimum_order: Option<i32>,
|
||||
pub service: Option<bool>,
|
||||
pub bio_percent: Option<i32>,
|
||||
pub phone: Option<String>,
|
||||
pub online_ordering: Option<String>,
|
||||
pub county_id: Option<i32>,
|
||||
}
|
||||
42
src/main.rs
42
src/main.rs
@@ -6,10 +6,11 @@ use axum::{
|
||||
use std::env;
|
||||
use tower_http::cors::{CorsLayer, Any};
|
||||
use crate::auth::structs::AppState;
|
||||
use crate::auth::auth::router as auth_router;
|
||||
use crate::data::data::router as data_router;
|
||||
use crate::state::data::router as state_router;
|
||||
use crate::company::company::company_routes;
|
||||
use crate::auth::auth::{auth_middleware, login, register};
|
||||
use crate::data::data::get_user;
|
||||
use crate::state::data::{get_counties_by_state, get_county_by_id};
|
||||
use crate::listing::data::{get_listings, get_listing_by_id, get_listings_by_county, create_listing, update_listing, delete_listing};
|
||||
use axum::middleware;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -17,6 +18,7 @@ mod auth;
|
||||
mod data;
|
||||
mod state;
|
||||
mod company;
|
||||
mod listing;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -35,7 +37,6 @@ async fn main() {
|
||||
|
||||
// Create app state
|
||||
let state = AppState {
|
||||
db_url: database_url.clone(),
|
||||
db: db.clone(),
|
||||
jwt_secret: env::var("JWT_SECRET").expect("JWT_SECRET must be set"),
|
||||
};
|
||||
@@ -43,18 +44,29 @@ async fn main() {
|
||||
// Configure CORS
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(tower_http::cors::AllowOrigin::exact(frontend_origin.parse::<header::HeaderValue>().unwrap()))
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
||||
.allow_headers(Any);
|
||||
|
||||
// Build router
|
||||
let app = Router::new()
|
||||
.route("/", axum::routing::get(|| async { "API is running" }))
|
||||
.merge(auth_router())
|
||||
.merge(data_router())
|
||||
.merge(state_router())
|
||||
.merge(company_routes().await)
|
||||
.with_state(state.clone())
|
||||
.layer(cors);
|
||||
// Build router with separated public and protected routes
|
||||
let protected_routes = Router::new()
|
||||
.route("/user", axum::routing::get(get_user))
|
||||
.route("/company", axum::routing::any(crate::company::company::company_handler))
|
||||
.route("/listing", axum::routing::get(get_listings))
|
||||
.route("/listing", axum::routing::post(create_listing))
|
||||
.route("/listing/:listing_id", axum::routing::get(get_listing_by_id))
|
||||
.route("/listing/:listing_id", axum::routing::put(update_listing))
|
||||
.route("/listing/:listing_id", axum::routing::delete(delete_listing))
|
||||
.route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware));
|
||||
|
||||
let public_routes = Router::new()
|
||||
.route("/auth/register", axum::routing::post(register))
|
||||
.route("/auth/login", axum::routing::post(login))
|
||||
.route("/categories", axum::routing::get(crate::company::category::get_all_categories))
|
||||
.route("/state/:state_abbr", axum::routing::get(get_counties_by_state))
|
||||
.route("/state/:state_abbr/:county_id", axum::routing::get(get_county_by_id))
|
||||
.route("/listings/county/:county_id", axum::routing::get(get_listings_by_county));
|
||||
|
||||
let app = public_routes.merge(protected_routes).with_state(state).layer(cors);
|
||||
|
||||
// Print server status
|
||||
println!("Server is running on http://0.0.0.0:9552");
|
||||
|
||||
@@ -2,19 +2,11 @@ use axum::{
|
||||
extract::{Path, State},
|
||||
response::Json,
|
||||
http::StatusCode,
|
||||
Router,
|
||||
routing::get,
|
||||
};
|
||||
use crate::auth::structs::AppState;
|
||||
use crate::state::structs::{County, ErrorResponse};
|
||||
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/state/:state_abbr", get(get_counties_by_state))
|
||||
.route("/state/:state_abbr/:county_id", get(get_county_by_id))
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub async fn get_counties_by_state(
|
||||
@@ -84,4 +76,3 @@ pub async fn get_county_by_id(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user