chore: comprehensive README with binary distribution docs + build-all.sh
This commit is contained in:
parent
ca970104ee
commit
10c22bcaee
3 changed files with 341 additions and 176 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -24,5 +24,9 @@ storage/*
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
expenseflow-*
|
||||
|
||||
# Go
|
||||
vendor/
|
||||
|
|
|
|||
463
README.md
463
README.md
|
|
@ -1,236 +1,288 @@
|
|||
# ExpenseFlow - AI-Powered Expense Tracker
|
||||
# ExpenseFlow — AI-Powered Expense Tracker
|
||||
|
||||
A production-ready, mobile-first Progressive Web App (PWA) for expense management with passwordless email OTP authentication, event-based expense tracking, AI receipt extraction via DeepSeek Vision, and CSV/PDF reporting delivered via email.
|
||||
> A production-ready, mobile-first Progressive Web App (PWA) that uses passwordless email OTP login, event-based expense tracking, AI receipt extraction (DeepSeek Vision), and event filing (CSV/PDF via email).
|
||||
|
||||
**Tech Stack:** Go 1.22+ · HTMX · SQLite · DeepSeek Vision API · PWA
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
## 📦 Quick Start — Binary Distribution
|
||||
|
||||
- **Passwordless OTP Auth** — Email-based 6-digit code, no passwords, HTTP-only session cookie
|
||||
- **Event Dashboard** — Create events (e.g. "WebSummit 2026"), track status (open/closed), reopen closed events
|
||||
- **AI Receipt Capture** — Snap a photo → DeepSeek Vision extracts amount, merchant, category, date → save
|
||||
- **Event Filing** — Generate CSV or PDF report → email as attachment → auto-close event
|
||||
- **PWA** — Installable on mobile homescreen, offline shell, camera capture for receipts
|
||||
Pre-compiled binaries are available for download from the [Releases](https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases) page.
|
||||
|
||||
### Download & Run
|
||||
|
||||
```bash
|
||||
# Linux (amd64)
|
||||
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-linux-amd64
|
||||
chmod +x expenseflow
|
||||
./expenseflow
|
||||
|
||||
# Linux (arm64) — Raspberry Pi, etc.
|
||||
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-linux-arm64
|
||||
chmod +x expenseflow
|
||||
./expenseflow
|
||||
|
||||
# macOS (Intel)
|
||||
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-darwin-amd64
|
||||
chmod +x expenseflow
|
||||
./expenseflow
|
||||
|
||||
# macOS (Apple Silicon M1/M2/M3)
|
||||
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-darwin-arm64
|
||||
chmod +x expenseflow
|
||||
./expenseflow
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:8080` by default. Set `PORT=3000` to change the port.
|
||||
|
||||
> **Note:** The binary embeds no configuration. You must create a `.env` file (see [Configuration](#-configuration) below) in the same directory you run the binary from.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| **Backend** | Go (`net/http` + chi router) |
|
||||
| **Frontend** | HTMX (server-driven UI, partial page updates) |
|
||||
| **Database** | SQLite (auto-migrated on startup) |
|
||||
| **AI OCR** | DeepSeek Vision API |
|
||||
| **Email** | SMTP (OTP delivery + report attachments) |
|
||||
| **PWA** | `manifest.json` + Service Worker |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## 🔧 Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.21+
|
||||
- GCC (required for CGO/SQLite via `mattn/go-sqlite3`)
|
||||
- **Go 1.22+** — [Download](https://go.dev/dl/)
|
||||
- **GCC** (CGO is required for the SQLite driver)
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install build-essential
|
||||
|
||||
# macOS
|
||||
xcode-select --install
|
||||
|
||||
# Alpine
|
||||
apk add build-base
|
||||
```
|
||||
|
||||
### Clone & Build
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/your-org/expenseflow.git
|
||||
cd expenseflow
|
||||
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
|
||||
cd ExpenseFlow
|
||||
|
||||
# Copy environment configuration
|
||||
# Build for your current platform
|
||||
go build -o expenseflow .
|
||||
|
||||
# The binary is now ready: ./expenseflow
|
||||
```
|
||||
|
||||
### Cross-Compilation
|
||||
|
||||
The binary uses CGO (for `mattn/go-sqlite3`), so cross-compilation requires a cross-compiler.
|
||||
|
||||
```bash
|
||||
# Linux amd64
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-linux-gnu-gcc go build -o expenseflow-linux-amd64 .
|
||||
|
||||
# Linux arm64 (Raspberry Pi, etc.)
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -o expenseflow-linux-arm64 .
|
||||
|
||||
# macOS Intel
|
||||
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CC=o64-clang go build -o expenseflow-darwin-amd64 .
|
||||
|
||||
# macOS Apple Silicon
|
||||
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-apple-darwin-clang go build -o expenseflow-darwin-arm64 .
|
||||
```
|
||||
|
||||
> **Tip:** For macOS cross-compilation from Linux, use [osxcross](https://github.com/tpoechtrager/osxcross). For ARM Linux, install `gcc-aarch64-linux-gnu`.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your credentials:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
|
||||
# Run the application
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Open [http://localhost:8080](http://localhost:8080) in your browser.
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `SMTP_HOST` | Yes* | — | SMTP server hostname |
|
||||
| `SMTP_PORT` | Yes* | — | SMTP server port (usually 587) |
|
||||
| `SMTP_USER` | Yes* | — | SMTP username |
|
||||
| `SMTP_PASS` | Yes* | — | SMTP password |
|
||||
| `DEEPSEEK_API_KEY` | Yes* | — | DeepSeek Vision API key |
|
||||
| `BASE_URL` | No | `http://localhost:8080` | Public URL for email links |
|
||||
| `PORT` | No | `8080` | HTTP server port |
|
||||
|
||||
*\* The app will start without SMTP/DeepSeek configured, but OTP emails and AI extraction will not work.*
|
||||
|
||||
### Getting Credentials
|
||||
|
||||
- **DeepSeek Vision API:** Sign up at [platform.deepseek.com](https://platform.deepseek.com) and create an API key.
|
||||
- **SMTP:** Use any SMTP provider. The default config points to an OX hosting SMTP server.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
## 🚀 Usage
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
### 1. Start the Server
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `SMTP_HOST` | SMTP server hostname | `smtp.openxchange.eu` |
|
||||
| `SMTP_PORT` | SMTP server port | `587` |
|
||||
| `SMTP_USER` | SMTP authentication username | `post@2-4-h.app` |
|
||||
| `SMTP_PASS` | SMTP authentication password | — |
|
||||
| `DEEPSEEK_API_KEY` | DeepSeek Vision API key | — |
|
||||
| `BASE_URL` | Public base URL for absolute links in emails | `http://localhost:8080` |
|
||||
```bash
|
||||
./expenseflow
|
||||
```
|
||||
|
||||
---
|
||||
### 2. Open in Browser
|
||||
|
||||
## Project Structure
|
||||
Navigate to [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
### 3. Full Acceptance Flow
|
||||
|
||||
```
|
||||
ExpenseFlow/
|
||||
├── main.go # Entry point, router, static file server
|
||||
├── go.mod # Module dependencies
|
||||
├── .env # Credentials (not committed)
|
||||
├── .env.example # Env template
|
||||
├── internal/
|
||||
│ ├── ai/
|
||||
│ │ └── deepseek.go # DeepSeek Vision API client
|
||||
│ ├── auth/
|
||||
│ │ ├── otp.go # OTP generation & validation
|
||||
│ │ └── session.go # Session management (in-memory store)
|
||||
│ ├── database/
|
||||
│ │ └── db.go # SQLite init, queries, auto-migration
|
||||
│ ├── email/
|
||||
│ │ └── smtp.go # SMTP email sender (OTP + reports)
|
||||
│ ├── handlers/
|
||||
│ │ ├── auth.go # Auth endpoints (login, OTP, verify)
|
||||
│ │ ├── events.go # Event CRUD + reopen
|
||||
│ │ ├── expenses.go # Upload, AI extract, save
|
||||
│ │ └── file.go # CSV/PDF generation, email delivery
|
||||
│ └── utils/
|
||||
│ └── uuid.go # UUID generation helper
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ └── style.css # Application styles
|
||||
│ ├── icons/ # PWA icons (192x192, 512x512)
|
||||
│ ├── manifest.json # PWA manifest
|
||||
│ └── sw.js # Service Worker (offline cache)
|
||||
├── templates/ # HTMX templates (server-rendered HTML)
|
||||
│ ├── layout.html
|
||||
│ ├── index.html
|
||||
│ ├── dashboard.html
|
||||
│ ├── event_expenses.html
|
||||
│ ├── receipt_form.html
|
||||
│ └── expense_list.html
|
||||
└── storage/ # Uploaded receipt images (created at runtime)
|
||||
1. Enter your email → click "Send Verification Code"
|
||||
2. Check your inbox for the 6-digit OTP code
|
||||
3. Enter the OTP → click "Verify Code"
|
||||
4. Create an event (e.g., "WebSummit 2026")
|
||||
5. Click "Add Expenses" on the event card
|
||||
6. Click "Capture Receipt" → take a photo or select an image
|
||||
7. AI extracts: amount, merchant, category, date → pre-fills the form
|
||||
8. Review and click "Save Expense"
|
||||
9. Click "File Event" → enter recipient email → choose CSV or PDF
|
||||
10. Report is emailed and event status changes to "closed"
|
||||
11. Click "Reopen" to re-open a closed event
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
## 📡 API Reference
|
||||
|
||||
### Public Endpoints (no authentication)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/` | Landing page — email input for OTP login |
|
||||
| `POST` | `/request-otp` | Request a 6-digit OTP code (sent via email) |
|
||||
| `POST` | `/verify-otp` | Verify OTP and create session cookie |
|
||||
| `GET` | `/dashboard` | User dashboard — list of events |
|
||||
| `POST` | `/events` | Create a new expense event |
|
||||
| `GET` | `/events/{id}/expenses` | View expenses for an event |
|
||||
| `POST` | `/expenses/upload` | Upload receipt image → AI extraction |
|
||||
| `POST` | `/expenses` | Save an expense record |
|
||||
| `POST` | `/events/{id}/file` | File report (CSV/PDF) and close event |
|
||||
| `GET` | `/` | Landing page with email login form |
|
||||
| `POST` | `/request-otp` | Request a 6-digit OTP code (sends email) |
|
||||
| `POST` | `/verify-otp` | Verify OTP code and create session |
|
||||
| `POST` | `/logout` | Clear session and redirect to login |
|
||||
|
||||
### Protected Endpoints (require session cookie)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/dashboard` | Event dashboard |
|
||||
| `POST` | `/events` | Create a new event |
|
||||
| `GET` | `/events/{id}/expenses` | View event with expense list |
|
||||
| `PUT` | `/events/{id}/reopen` | Reopen a closed event |
|
||||
| `POST` | `/expenses/upload` | Upload receipt image (multipart) |
|
||||
| `POST` | `/expenses` | Save expense from form data |
|
||||
| `POST` | `/events/{id}/file` | Generate report (CSV/PDF) and email it |
|
||||
|
||||
### Static Files
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/static/css/style.css` | Application stylesheet |
|
||||
| `/static/icons/icon-192.png` | PWA icon (192×192) |
|
||||
| `/static/icons/icon-512.png` | PWA icon (512×512) |
|
||||
| `/manifest.json` | PWA manifest |
|
||||
| `/sw.js` | Service worker |
|
||||
| `/storage/{filename}` | Uploaded receipt images |
|
||||
|
||||
---
|
||||
|
||||
## Feature Walkthrough
|
||||
## 🏗️ Project Structure
|
||||
|
||||
### 1. Passwordless OTP Auth
|
||||
|
||||
Log in with just your email — no passwords needed.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
User->>Browser: Enter email
|
||||
Browser->>Server: POST /request-otp (email)
|
||||
Server->>Email: Send 6-digit code
|
||||
User->>Browser: Enter code
|
||||
Browser->>Server: POST /verify-otp (email, code)
|
||||
Server->>Browser: Set session cookie, redirect to /dashboard
|
||||
```
|
||||
|
||||
### 2. Event Dashboard
|
||||
|
||||
- **Open events** — click to view expenses and capture receipts
|
||||
- **Closed events** — show a **Reopen** button (`PUT /events/{id}/reopen`)
|
||||
- Each card shows: event name, status badge, created date
|
||||
|
||||
### 3. AI Receipt Capture
|
||||
|
||||
```bash
|
||||
# Upload a receipt image
|
||||
curl -X POST http://localhost:8080/expenses/upload \
|
||||
-H "Cookie: session_token=..." \
|
||||
-F "image=@receipt.jpg"
|
||||
ExpenseFlow/
|
||||
├── main.go # Entry point, router, middleware, server
|
||||
├── go.mod / go.sum # Go module definition
|
||||
├── .env.example # Environment variable template
|
||||
├── README.md # This file
|
||||
├── internal/
|
||||
│ ├── database/db.go # SQLite init, auto-migration, 11 query functions
|
||||
│ ├── auth/
|
||||
│ │ ├── otp.go # 6-digit OTP generation + 3-fail lockout
|
||||
│ │ └── session.go # In-memory session store (crypto tokens, 24h TTL)
|
||||
│ ├── handlers/
|
||||
│ │ ├── auth.go # Auth endpoints + middleware
|
||||
│ │ ├── events.go # Event CRUD + dashboard
|
||||
│ │ ├── expenses.go # Receipt upload, AI extraction, save
|
||||
│ │ └── file.go # CSV/PDF generation + email filing
|
||||
│ ├── ai/deepseek.go # DeepSeek Vision API client
|
||||
│ ├── email/smtp.go # SMTP sender (OTP + attachments)
|
||||
│ └── utils/uuid.go # UUID generation
|
||||
├── templates/
|
||||
│ ├── index.html # Landing page
|
||||
│ ├── dashboard.html # Event cards + create form
|
||||
│ ├── event_expenses.html # Event detail + capture + filing
|
||||
│ ├── receipt_form.html # AI-prefilled edit form
|
||||
│ └── expense_list.html # HTMX expense list fragment
|
||||
├── static/
|
||||
│ ├── css/style.css # Mobile-first responsive CSS (1845 lines)
|
||||
│ ├── manifest.json # PWA manifest
|
||||
│ ├── sw.js # Service worker
|
||||
│ └── icons/ # PWA placeholder icons
|
||||
└── storage/ # Uploaded receipts (created at runtime)
|
||||
```
|
||||
|
||||
What happens:
|
||||
1. Image saved to `./storage/{uuid}.jpg`
|
||||
2. Sent to DeepSeek Vision API for OCR
|
||||
3. AI returns: `{amount, currency, merchant, category, date}`
|
||||
4. Pre-filled form rendered for user review/edit
|
||||
5. On save → expense stored, list updated
|
||||
|
||||
### 4. Event Filing
|
||||
|
||||
Generate a CSV or PDF report and email it:
|
||||
|
||||
```bash
|
||||
# File event as PDF
|
||||
curl -X POST http://localhost:8080/events/123/file \
|
||||
-H "Cookie: session_token=..." \
|
||||
-d "email=user@example.com&format=pdf"
|
||||
```
|
||||
|
||||
- Creates report from all event expenses
|
||||
- Sends email with attachment
|
||||
- Event status changes to `closed`
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Test Scenario
|
||||
## 🧩 Features in Detail
|
||||
|
||||
Run through the full flow manually:
|
||||
### 🔐 Passwordless OTP Authentication
|
||||
- Email-based 6-digit code, 5-minute expiry
|
||||
- Auto-creates user account on first login
|
||||
- 3 failed attempts trigger a 1-minute cooldown
|
||||
- HTTP-only session cookie (`SameSite=Lax`, 24h TTL)
|
||||
- No passwords to store or forget
|
||||
|
||||
1. **Visit** `http://localhost:8080` — see email input
|
||||
2. **Enter email** — receive 6-digit OTP in inbox
|
||||
3. **Enter OTP** — redirected to dashboard (empty)
|
||||
4. **Create event** — name it "Test Event"
|
||||
5. **Click event** — see expense list (empty) + **Capture Receipt** button
|
||||
6. **Upload receipt** — snap/take a photo → AI extracts amount, merchant, category, date
|
||||
7. **Review & save** — pre-filled form, click Save → expense appears in list
|
||||
8. **File event** — click **File Event**, enter email, pick PDF → report sent, event closes
|
||||
9. **Verify** — event shows as **closed** on dashboard
|
||||
10. **Reopen** — click **Reopen** → badge changes back to **open**
|
||||
### 📋 Event-Based Expense Tracking
|
||||
- Group expenses into events (trips, conferences, months)
|
||||
- Open/closed lifecycle with reopen support
|
||||
- Dashboard with event cards showing name, status, and creation date
|
||||
|
||||
### 🤖 AI Receipt Extraction
|
||||
- Upload receipt images (JPEG/PNG, max 10 MB)
|
||||
- DeepSeek Vision API extracts: amount, currency, merchant, category, date
|
||||
- Editable pre-filled form on failure or success
|
||||
- Images stored locally in `./storage/`
|
||||
|
||||
### 📧 Email Reporting
|
||||
- Generate CSV (via `encoding/csv`) or PDF (via `gofpdf`)
|
||||
- Automatic email delivery via SMTP with file attachment
|
||||
- Event auto-closes after successful filing
|
||||
- Multipart MIME support with proper content headers
|
||||
|
||||
### 📱 Progressive Web App
|
||||
- Installable on mobile and desktop (manifest.json)
|
||||
- Offline shell caching (service worker)
|
||||
- Camera capture for receipts (`capture="environment"`)
|
||||
- Theme color: `#10b981` (emerald green)
|
||||
- Responsive design: 320px → 768px → 1024px+
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
## 🗄️ Database Schema (SQLite)
|
||||
|
||||
SQLite database (`expenses.db`) created and auto-migrated on first startup.
|
||||
|
||||
### Tables
|
||||
Auto-created on first run — 4 tables with foreign keys:
|
||||
|
||||
```sql
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- OTP codes
|
||||
CREATE TABLE auth_otps (
|
||||
email TEXT PRIMARY KEY,
|
||||
otp_code TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- Events (expense containers)
|
||||
CREATE TABLE events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT CHECK(status IN ('open','closed')) DEFAULT 'open',
|
||||
status TEXT CHECK(status IN ('open', 'closed')) DEFAULT 'open',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Expenses (receipt records)
|
||||
CREATE TABLE expenses (
|
||||
id TEXT PRIMARY KEY,
|
||||
event_id TEXT NOT NULL,
|
||||
|
|
@ -248,31 +300,90 @@ CREATE TABLE expenses (
|
|||
|
||||
---
|
||||
|
||||
## PWA Features
|
||||
## 🐳 Docker
|
||||
|
||||
- **Installable** — `manifest.json` with standalone display, theme color `#10b981`
|
||||
- **Offline shell** — Service Worker caches core assets (CSS, HTMX, shell) on install
|
||||
- **Camera capture** — `<input type="file" accept="image/*" capture="environment">` for mobile receipt snapping
|
||||
```dockerfile
|
||||
FROM golang:1.22-alpine AS builder
|
||||
RUN apk add --no-cache build-base
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go build -o expenseflow .
|
||||
|
||||
### Service Worker
|
||||
FROM alpine:3.19
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/expenseflow .
|
||||
COPY --from=builder /app/templates ./templates
|
||||
COPY --from=builder /app/static ./static
|
||||
COPY --from=builder /app/.env.example ./.env.example
|
||||
EXPOSE 8080
|
||||
CMD ["./expenseflow"]
|
||||
```
|
||||
|
||||
- Cache name: `expenseflow-v1`
|
||||
- **Install**: caches `/`, CSS, HTMX library
|
||||
- **Fetch**: cache-first for static assets, network-only for API calls
|
||||
```bash
|
||||
docker build -t expenseflow .
|
||||
docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage expenseflow
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
## 🔒 Security
|
||||
|
||||
| Area | Implementation |
|
||||
|------|---------------|
|
||||
| **Session** | In-memory store, 24h TTL, HTTP-only cookie |
|
||||
| **OTP** | 5-minute expiry, 3-attempt cooldown |
|
||||
| **Uploads** | Max 10 MB, JPEG/PNG only, sanitized filenames |
|
||||
| **AI fallback** | Editable empty form if DeepSeek API fails |
|
||||
| **Sessions** | Cryptographically random tokens (32 bytes, hex-encoded), in-memory store, 24h TTL |
|
||||
| **OTP** | 6-digit codes from `crypto/rand`, 5-minute expiry, 3-fail lockout (1 minute) |
|
||||
| **Cookies** | HTTP-only, SameSite=Lax, path restricted |
|
||||
| **SQL Injection** | Parameterized queries on all database operations |
|
||||
| **File Upload** | Magic byte validation (JPEG/PNG), 10 MB limit, sanitized filenames (UUID) |
|
||||
| **Credentials** | All secrets via environment variables only — never hardcoded |
|
||||
| **HTMX** | Server-rendered HTML, no client-side data exposure |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## 🧪 Development
|
||||
|
||||
MIT
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
go vet ./...
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Manual Smoke Test
|
||||
|
||||
```bash
|
||||
# Start server
|
||||
go run main.go &
|
||||
|
||||
# Test landing page
|
||||
curl -s http://localhost:8080/ | head -5
|
||||
|
||||
# Test OTP request
|
||||
curl -s -X POST -d "email=test@example.com" http://localhost:8080/request-otp
|
||||
|
||||
# Check database
|
||||
sqlite3 expenses.db "SELECT * FROM auth_otps;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT — Free to use, modify, and distribute.
|
||||
|
||||
---
|
||||
|
||||
## 🙌 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
||||
3. Commit changes (`git commit -am 'feat: add my feature'`)
|
||||
4. Push (`git push origin feature/my-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
---
|
||||
|
||||
*Built with Go, HTMX, SQLite, and ❤️*
|
||||
|
|
|
|||
50
build-all.sh
Executable file
50
build-all.sh
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# build-all.sh — Cross-compile ExpenseFlow for multiple platforms
|
||||
#
|
||||
# Prerequisites:
|
||||
# linux/amd64: gcc (native)
|
||||
# linux/arm64: gcc-aarch64-linux-gnu (sudo apt install gcc-aarch64-linux-gnu)
|
||||
# darwin/amd64: o64-clang (via osxcross)
|
||||
# darwin/arm64: aarch64-apple-darwin-clang (via osxcross)
|
||||
#
|
||||
# Usage: ./build-all.sh
|
||||
# Output: ./dist/expenseflow-{platform}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUTDIR="dist"
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
VERSION="${1:-v1.0.0}"
|
||||
LDFLAGS="-s -w"
|
||||
|
||||
echo "==> Building ExpenseFlow $VERSION"
|
||||
|
||||
build() {
|
||||
local GOOS="$1" GOARCH="$2" CC="$3" SUFFIX="$4"
|
||||
local OUT="$OUTDIR/expenseflow-$SUFFIX"
|
||||
echo " $SUFFIX ..."
|
||||
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=1 CC="$CC" \
|
||||
go build -ldflags="$LDFLAGS" -o "$OUT" .
|
||||
echo " => $(ls -lh "$OUT" | awk '{print $5}')"
|
||||
}
|
||||
|
||||
# linux/amd64 — native
|
||||
build linux amd64 "gcc" "linux-amd64"
|
||||
|
||||
# linux/arm64 — cross
|
||||
if command -v aarch64-linux-gnu-gcc &>/dev/null; then
|
||||
build linux arm64 "aarch64-linux-gnu-gcc" "linux-arm64"
|
||||
fi
|
||||
|
||||
# macOS builds — only if osxcross toolchain is available
|
||||
if command -v o64-clang &>/dev/null; then
|
||||
build darwin amd64 "o64-clang" "darwin-amd64"
|
||||
fi
|
||||
if command -v aarch64-apple-darwin-clang &>/dev/null; then
|
||||
build darwin arm64 "aarch64-apple-darwin-clang" "darwin-arm64"
|
||||
fi
|
||||
|
||||
echo "==> All builds complete. Output in $OUTDIR/"
|
||||
ls -lh "$OUTDIR/"
|
||||
Loading…
Reference in a new issue