389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
# ExpenseFlow — AI-Powered Expense Tracker
|
||
|
||
> 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
|
||
|
||
---
|
||
|
||
## 📦 Quick Start — Binary Distribution
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 🔧 Building from Source
|
||
|
||
### Prerequisites
|
||
|
||
- **Go 1.22+** — [Download](https://go.dev/dl/)
|
||
- **GCC** (CGO is required for the SQLite driver)
|
||
|
||
```bash
|
||
# Debian/Ubuntu
|
||
sudo apt install build-essential
|
||
|
||
# macOS
|
||
xcode-select --install
|
||
|
||
# Alpine
|
||
apk add build-base
|
||
```
|
||
|
||
### Clone & Build
|
||
|
||
```bash
|
||
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
|
||
cd ExpenseFlow
|
||
|
||
# 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
|
||
```
|
||
|
||
| 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.
|
||
|
||
---
|
||
|
||
## 🚀 Usage
|
||
|
||
### 1. Start the Server
|
||
|
||
```bash
|
||
./expenseflow
|
||
```
|
||
|
||
### 2. Open in Browser
|
||
|
||
Navigate to [http://localhost:8080](http://localhost:8080)
|
||
|
||
### 3. Full Acceptance Flow
|
||
|
||
```
|
||
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 Reference
|
||
|
||
### Public Endpoints (no authentication)
|
||
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| `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 |
|
||
|
||
---
|
||
|
||
## 🏗️ Project Structure
|
||
|
||
```
|
||
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)
|
||
```
|
||
|
||
---
|
||
|
||
## 🧩 Features in Detail
|
||
|
||
### 🔐 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
|
||
|
||
### 📋 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 Schema (SQLite)
|
||
|
||
Auto-created on first run — 4 tables with foreign keys:
|
||
|
||
```sql
|
||
CREATE TABLE users (
|
||
id TEXT PRIMARY KEY,
|
||
email TEXT UNIQUE NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE TABLE auth_otps (
|
||
email TEXT PRIMARY KEY,
|
||
otp_code TEXT NOT NULL,
|
||
expires_at DATETIME NOT NULL
|
||
);
|
||
|
||
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',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||
);
|
||
|
||
CREATE TABLE expenses (
|
||
id TEXT PRIMARY KEY,
|
||
event_id TEXT NOT NULL,
|
||
amount REAL NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
merchant TEXT NOT NULL,
|
||
category TEXT NOT NULL,
|
||
description TEXT,
|
||
date TEXT NOT NULL,
|
||
image_path TEXT NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(event_id) REFERENCES events(id) ON DELETE CASCADE
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 🐳 Docker
|
||
|
||
```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 .
|
||
|
||
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"]
|
||
```
|
||
|
||
```bash
|
||
docker build -t expenseflow .
|
||
docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage expenseflow
|
||
```
|
||
|
||
---
|
||
|
||
## 🔒 Security
|
||
|
||
| Area | Implementation |
|
||
|------|---------------|
|
||
| **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 |
|
||
|
||
---
|
||
|
||
## 🧪 Development
|
||
|
||
### 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 ❤️*
|