Files
api_rust/README.md
Edwin Eames 6c95a7d201 feat: add admin panel, stats endpoint, and url field to listings
- 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>
2026-03-06 11:34:03 -05:00

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).