- Add full admin CRUD routes for users, companies, listings, oil-prices protected behind auth middleware (/admin/*) - Add public /stats endpoint returning latest market price aggregates - Add /health and / health check endpoints - Add `url` field to listings (struct, all SQL queries, create/update) - Add `phone` and `url` fields to OilPrice struct - Remove deprecated company CRUD handlers (get_company, create_company) - Update schema.sql; remove one-off alter/drop migration scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.8 KiB
5.8 KiB
NewEnglandBio Rust API
RESTful API for heating oil/biofuel price comparison built with Axum 0.6. Serves the SvelteKit frontend and manages users, companies, listings, and scraped oil prices.
Tech Stack
- Framework: Axum 0.6 (Tokio async runtime)
- Database: PostgreSQL via sqlx 0.6
- Auth: JWT (jsonwebtoken) + Argon2 password hashing
- CORS: tower-http
- Logging: tracing + tracing-subscriber
Project Structure
src/
├── main.rs # Server startup, route definitions, CORS config
├── auth/
│ ├── auth.rs # Register, login, logout, auth middleware
│ └── structs.rs # User, Claims, LoginRequest, RegisterRequest
├── data/
│ └── data.rs # get_user endpoint
├── company/
│ ├── company.rs # Company CRUD (single handler, method dispatch)
│ ├── category.rs # Service categories endpoint
│ └── structs.rs # Company, ServiceCategory
├── listing/
│ ├── data.rs # Listing CRUD + public county listings
│ └── structs.rs # Listing, CreateListingRequest, UpdateListingRequest
├── state/
│ ├── data.rs # County lookup endpoints
│ └── structs.rs # County, ErrorResponse
└── oil_prices/
├── data.rs # Oil prices by county
└── structs.rs # OilPrice
API Endpoints
Server listens on 0.0.0.0:9552.
Public Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /auth/register |
Register a new user |
| POST | /auth/login |
Login, returns JWT in httpOnly cookie |
| POST | /auth/logout |
Clear auth cookie |
| GET | /state/:state_abbr |
List counties in a state |
| GET | /state/:state_abbr/:county_id |
Get a specific county |
| GET | /categories |
List all service categories |
| GET | /oil-prices/county/:county_id |
Oil prices for a county (sorted by price) |
| GET | /listings/county/:county_id |
Active listings in a county |
Protected Endpoints (require JWT)
| Method | Path | Description |
|---|---|---|
| GET | /user |
Get authenticated user info |
| GET | /company |
Get user's active company |
| POST | /company |
Create company |
| PUT | /company |
Update company (or create if none) |
| DELETE | /company |
Soft-delete company (sets active=false) |
| GET | /listing |
Get all user's listings |
| GET | /listing/:id |
Get a specific listing (owner only) |
| POST | /listing |
Create listing |
| PUT | /listing/:id |
Update listing (partial updates supported) |
| DELETE | /listing/:id |
Delete listing |
Request/Response Examples
Register:
curl -X POST http://localhost:9552/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"dealer1","password":"secret123","email":"dealer@example.com"}'
Login:
curl -X POST http://localhost:9552/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{"username":"dealer1","password":"secret123"}'
Get counties in Massachusetts:
curl http://localhost:9552/state/MA
Get oil prices for county 5:
curl http://localhost:9552/oil-prices/county/5
Create a listing (authenticated):
curl -X POST http://localhost:9552/listing \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"company_name": "Acme Oil",
"is_active": true,
"price_per_gallon": 3.29,
"price_per_gallon_cash": 3.19,
"bio_percent": 5,
"service": true,
"online_ordering": "none",
"county_id": 5,
"town": "Worcester"
}'
Validation Rules
price_per_gallonmust be > 0price_per_gallon_cashmust be >= 0bio_percentmust be 0-100minimum_ordermust be >= 0
Setup
Environment
Create .env:
DATABASE_URL=postgres://postgres:password@192.168.1.204:5432/fuelprices
JWT_SECRET=YourSecretKeyHereAtLeast32Characters
FRONTEND_ORIGIN=http://localhost:9551
RUST_LOG=api_rust=info
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
JWT_SECRET |
Yes | JWT signing key |
FRONTEND_ORIGIN |
No | CORS allowed origin (default http://localhost:9551) |
RUST_LOG |
No | Log level filter (default api_rust=info) |
Database
Initialize the schema and seed data:
psql $DATABASE_URL -f schema.sql
psql $DATABASE_URL -f seed_categories.sql
Run Locally
cargo run
Run with Hot Reload
cargo install cargo-watch
cargo watch -x run
Docker
Production:
docker build -t api-rust .
docker run -p 9552:9552 --env-file .env api-rust
Development (with cargo-watch):
docker build -f Dockerfile.dev -t api-rust-dev .
docker run -p 9552:9552 -v $(pwd):/usr/src/app --env-file .env api-rust-dev
Authentication Flow
- User registers or logs in
- Server returns JWT as httpOnly cookie (
auth_token, 24h expiry, SameSite=Lax) - Subsequent requests include cookie automatically
- Auth middleware validates JWT, loads user from DB, attaches to request
- Also accepts
Authorization: Bearer <token>header as fallback - Logout clears the cookie
Database Tables
| Table | Description |
|---|---|
users |
User accounts (username, hashed password, email) |
company |
Company profiles (soft-delete via active flag) |
listings |
Price listings per county |
county |
Reference table of NE counties |
oil_prices |
Scraped price data from crawler |
service_categories |
Service category metadata |
No foreign key constraints — integrity enforced at application level.
Error Responses
All errors return JSON:
{"error": "description of what went wrong"}
Status codes: 400 (validation), 401 (unauthorized), 404 (not found), 500 (internal).