- 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>
209 lines
5.8 KiB
Markdown
209 lines
5.8 KiB
Markdown
# 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:**
|
|
```bash
|
|
curl -X POST http://localhost:9552/auth/register \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"dealer1","password":"secret123","email":"dealer@example.com"}'
|
|
```
|
|
|
|
**Login:**
|
|
```bash
|
|
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:**
|
|
```bash
|
|
curl http://localhost:9552/state/MA
|
|
```
|
|
|
|
**Get oil prices for county 5:**
|
|
```bash
|
|
curl http://localhost:9552/oil-prices/county/5
|
|
```
|
|
|
|
**Create a listing (authenticated):**
|
|
```bash
|
|
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_gallon` must be > 0
|
|
- `price_per_gallon_cash` must be >= 0
|
|
- `bio_percent` must be 0-100
|
|
- `minimum_order` must 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:
|
|
|
|
```bash
|
|
psql $DATABASE_URL -f schema.sql
|
|
psql $DATABASE_URL -f seed_categories.sql
|
|
```
|
|
|
|
### Run Locally
|
|
|
|
```bash
|
|
cargo run
|
|
```
|
|
|
|
### Run with Hot Reload
|
|
|
|
```bash
|
|
cargo install cargo-watch
|
|
cargo watch -x run
|
|
```
|
|
|
|
## Docker
|
|
|
|
**Production:**
|
|
```bash
|
|
docker build -t api-rust .
|
|
docker run -p 9552:9552 --env-file .env api-rust
|
|
```
|
|
|
|
**Development (with cargo-watch):**
|
|
```bash
|
|
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
|
|
|
|
1. User registers or logs in
|
|
2. Server returns JWT as httpOnly cookie (`auth_token`, 24h expiry, SameSite=Lax)
|
|
3. Subsequent requests include cookie automatically
|
|
4. Auth middleware validates JWT, loads user from DB, attaches to request
|
|
5. Also accepts `Authorization: Bearer <token>` header as fallback
|
|
6. 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:
|
|
|
|
```json
|
|
{"error": "description of what went wrong"}
|
|
```
|
|
|
|
Status codes: 400 (validation), 401 (unauthorized), 404 (not found), 500 (internal).
|