chore: rewrite install.sh with AI provider choice + -update flag + pure Go
- install.sh now: • Clones repo to /opt/rx • Builds pure-Go binary (no CGO) • Prompts AI choice: Gemini / OpenAI / Ollama • If Ollama: auto-installs Ollama + pulls qwen3.5:2b • Creates .env interactively • Creates systemd service • Supports -update flag (pull, rebuild, restart) - README.md fully rewritten for new install flow - All binaries removed from git tracking (dist/ in .gitignore)
This commit is contained in:
parent
6de9c27f9c
commit
39fa7f6a12
2 changed files with 357 additions and 359 deletions
449
README.md
449
README.md
|
|
@ -1,42 +1,71 @@
|
|||
# ExpenseFlow — AI-Powered Expense Tracker
|
||||
# ReceiptNext — 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).
|
||||
> A production-ready, mobile-first Progressive Web App (PWA) for expense management with passwordless OTP login, AI receipt extraction (Gemini / OpenAI / Ollama), and CSV/PDF email reporting with receipt images.
|
||||
|
||||
**Tech Stack:** Go 1.22+ · HTMX · SQLite · DeepSeek Vision API · PWA
|
||||
**Tech Stack:** Go 1.23+ · HTMX · SQLite (pure Go) · Google Gemini / OpenAI / Ollama · 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
|
||||
## 🚀 One-Command Install
|
||||
|
||||
```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
|
||||
sudo curl -sL https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/install.sh | bash
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:8080` by default. Set `PORT=3000` to change the port.
|
||||
Or from a local clone:
|
||||
|
||||
> **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.
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
|
||||
1. Clone the repo to `/opt/rx/`
|
||||
2. Build the binary (pure Go, no CGO, no dependencies)
|
||||
3. Ask which AI provider to use:
|
||||
|
||||
```
|
||||
1) Google Gemini (cloud API, needs API key)
|
||||
2) OpenAI / Compatible (OpenAI, Perplexity, Groq, etc.)
|
||||
3) Ollama (local — installs Ollama + qwen3.5:2b automatically)
|
||||
```
|
||||
|
||||
4. Prompt for SMTP settings (for OTP emails and report delivery)
|
||||
5. Create `/opt/rx/.env` with all configuration
|
||||
6. Set up a systemd service that auto-starts on boot
|
||||
7. Start ReceiptNext
|
||||
|
||||
**Result:** A fully configured, always-running expense tracker at `http://YOUR_SERVER:8080`.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Updating
|
||||
|
||||
```bash
|
||||
sudo /opt/rx/install.sh -update
|
||||
```
|
||||
|
||||
This pulls the latest code, rebuilds the binary, and restarts the service.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Binary Download (manual)
|
||||
|
||||
Pre-compiled static binaries are on the [Releases page](https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases):
|
||||
|
||||
```bash
|
||||
# Linux amd64
|
||||
curl -L -o receiptnext https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/receiptnext-linux-amd64
|
||||
chmod +x receiptnext
|
||||
./receiptnext
|
||||
|
||||
# Linux arm64 (Raspberry Pi, etc.)
|
||||
curl -L -o receiptnext https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/receiptnext-linux-arm64
|
||||
chmod +x receiptnext
|
||||
./receiptnext
|
||||
```
|
||||
|
||||
No dependencies, no CGO, no libc. Drop it on any Linux box and run.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -44,78 +73,49 @@ The server starts on `http://localhost:8080` by default. Set `PORT=3000` to chan
|
|||
|
||||
### Prerequisites
|
||||
|
||||
- **Go 1.22+** — [Download](https://go.dev/dl/)
|
||||
- **GCC** (CGO is required for the SQLite driver)
|
||||
- **Go 1.23+** — [Download](https://go.dev/dl/)
|
||||
- No GCC, no CGO, no cross-compilers needed
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install build-essential
|
||||
|
||||
# macOS
|
||||
xcode-select --install
|
||||
|
||||
# Alpine
|
||||
apk add build-base
|
||||
```
|
||||
|
||||
### Clone & Build
|
||||
### Build
|
||||
|
||||
```bash
|
||||
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
|
||||
cd ExpenseFlow
|
||||
git clone https://git.lohmar.co.uk/cclohmar/ReceiptNext.git
|
||||
cd ReceiptNext
|
||||
|
||||
# Build for your current platform
|
||||
go build -o expenseflow .
|
||||
CGO_ENABLED=0 go build -o receiptnext .
|
||||
|
||||
# The binary is now ready: ./expenseflow
|
||||
# Cross-compile for any platform (no extra tools needed!)
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o receiptnext-linux-arm64 .
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o receiptnext-darwin-amd64 .
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o receiptnext-darwin-arm64 .
|
||||
```
|
||||
|
||||
### 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`.
|
||||
The binary is fully static — zero runtime dependencies.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your credentials:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Configuration is via environment variables in `.env`. The install script builds this for you interactively.
|
||||
|
||||
| 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 |
|
||||
| `BASE_URL` | No | `http://localhost:8080` | Public URL for email links |
|
||||
| **AI Provider** | | | |
|
||||
| `AI_PROVIDER` | No | `gemini` | `gemini`, `openai`, or `ollama` |
|
||||
| `GEMINI_API_KEY` | For Gemini | — | Google Gemini API key |
|
||||
| `OPENAI_API_KEY` | For OpenAI | — | OpenAI-compatible API key |
|
||||
| `AI_MODEL` | For OpenAI/Ollama | `gpt-4o-mini` / `qwen3.5:2b` | Model name |
|
||||
| `AI_BASE_URL` | For OpenAI/Ollama | `https://api.openai.com/v1` | API endpoint |
|
||||
| **SMTP** | | | |
|
||||
| `SMTP_HOST` | See note | — | SMTP server hostname |
|
||||
| `SMTP_PORT` | See note | `587` | SMTP server port |
|
||||
| `SMTP_USER` | See note | — | SMTP username |
|
||||
| `SMTP_PASS` | See note | — | SMTP password |
|
||||
|
||||
*\* 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.
|
||||
> **Note:** SMTP is optional without it, OTP codes are logged to the server console for testing. Reports cannot be emailed without SMTP.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -124,208 +124,119 @@ cp .env.example .env
|
|||
### 1. Start the Server
|
||||
|
||||
```bash
|
||||
./expenseflow
|
||||
# If installed via systemd:
|
||||
systemctl start receiptnext
|
||||
|
||||
# Or run directly:
|
||||
./receiptnext
|
||||
```
|
||||
|
||||
### 2. Open in Browser
|
||||
|
||||
Navigate to [http://localhost:8080](http://localhost:8080)
|
||||
Navigate to `http://YOUR_SERVER: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
|
||||
2. Check your inbox (or server log) for the 6-digit OTP
|
||||
3. Enter OTP → click "Verify Code"
|
||||
4. Click "+ New" → name your event → set claim currency → enter a conversion sample
|
||||
5. Click "Open" on the event
|
||||
6. Tap "📷 Camera" or "📁 Upload" to add a receipt
|
||||
7. AI extracts amount, merchant, category, date → form is pre-filled
|
||||
8. Click "Save Expense"
|
||||
9. Click "Submit Event" → enter recipient email → choose CSV or PDF
|
||||
10. Report + receipt images ZIP are emailed → event auto-closes
|
||||
11. Click "Reopen" to add more receipts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 API Reference
|
||||
|
||||
### Public Endpoints (no authentication)
|
||||
### Public
|
||||
|
||||
| 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 |
|
||||
| `GET` | `/` | Landing page |
|
||||
| `POST` | `/request-otp` | Request OTP code |
|
||||
| `POST` | `/verify-otp` | Verify OTP and create session |
|
||||
| `POST` | `/logout` | Clear session |
|
||||
|
||||
### Protected Endpoints (require session cookie)
|
||||
### Protected (requires session)
|
||||
|
||||
| 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 |
|
||||
| `GET` | `/dashboard` | Event list |
|
||||
| `POST` | `/events` | Create event |
|
||||
| `GET` | `/events/{id}/expenses` | View event + expenses |
|
||||
| `PUT` | `/events/{id}/reopen` | Reopen closed event |
|
||||
| `POST` | `/expenses/upload` | Upload receipt image/PDF |
|
||||
| `POST` | `/expenses` | Save expense |
|
||||
| `GET` | `/expenses/{id}/edit` | Get edit form |
|
||||
| `PUT` | `/expenses/{id}` | Update expense |
|
||||
| `POST` | `/events/{id}/file` | File report (CSV/PDF + images ZIP) |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 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)
|
||||
/opt/rx/
|
||||
├── receiptnext # Compiled binary
|
||||
├── .env # Configuration
|
||||
├── templates/ # Go HTML templates
|
||||
├── static/ # CSS, icons, favicon, service worker
|
||||
│ ├── css/style.css
|
||||
│ ├── favicon.svg # Rx logo
|
||||
│ ├── manifest.json
|
||||
│ ├── sw.js
|
||||
│ └── icons/
|
||||
├── storage/ # Uploaded receipt images (runtime)
|
||||
├── install.sh # Installer script
|
||||
├── Makefile # Build targets
|
||||
└── contrib/
|
||||
└── receiptnext.service # Systemd service file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Features in Detail
|
||||
## 🧩 Features
|
||||
|
||||
### 🔐 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
|
||||
- 3 failed attempts → 1-minute cooldown
|
||||
- HTTP-only session cookie, 24h TTL
|
||||
|
||||
### 🤖 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/`
|
||||
- **Google Gemini** (default) — cloud vision API
|
||||
- **OpenAI-compatible** — works with OpenAI, Perplexity, Groq, Together AI, etc.
|
||||
- **Ollama** — local, offline, no API key needed (uses `qwen3.5:2b`)
|
||||
- Supports: JPEG, PNG, WebP, HEIC, PDF (email receipts from Uber, etc.)
|
||||
|
||||
### 💱 Currency Conversion
|
||||
- Sample-based: enter a real receipt amount and what you were charged
|
||||
- System computes the rate automatically
|
||||
- Each expense stores original + converted amount
|
||||
|
||||
### 📧 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
|
||||
- CSV or PDF report
|
||||
- Receipt images bundled as ZIP (`expense-{event}-images.zip`)
|
||||
- Filenames: `expense-{event}-report.csv`, `expense-{event}-report.pdf`
|
||||
- Item numbers match between report rows and ZIP images
|
||||
|
||||
### 📱 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+
|
||||
- Installable on mobile home screen
|
||||
- Camera capture + gallery upload
|
||||
- Dark "Terminal Mint" theme (`#0F172A` base)
|
||||
- Rx favicon in emerald green
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Schema (SQLite)
|
||||
## 🗄️ Database
|
||||
|
||||
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
|
||||
```
|
||||
SQLite, auto-created on first run — 4 tables: `users`, `auth_otps`, `events`, `expenses`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -333,57 +244,13 @@ docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage
|
|||
|
||||
| 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 |
|
||||
| Sessions | `crypto/rand` tokens, in-memory, 24h TTL |
|
||||
| OTP | `crypto/rand` codes, 5min expiry, lockout after 3 failures |
|
||||
| Cookies | HTTP-only, SameSite=Lax, path-restricted |
|
||||
| SQL | Parameterized queries everywhere |
|
||||
| Uploads | Magic byte validation, max 10MB, UUID filenames |
|
||||
| Config | `.env` file is `chmod 600` — readable only by root |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 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 ❤️*
|
||||
*Built with Go, HTMX, SQLite and ❤️*
|
||||
|
|
|
|||
267
install.sh
Normal file → Executable file
267
install.sh
Normal file → Executable file
|
|
@ -1,18 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# install.sh — ReceiptNext installer
|
||||
# ReceiptNext Installer
|
||||
# =====================
|
||||
#
|
||||
# Copies the binary and assets to /opt/rx/, prompts for configuration,
|
||||
# and sets up a systemd service.
|
||||
# Usage:
|
||||
# sudo ./install.sh — Full install (clone, build, configure, service)
|
||||
# sudo ./install.sh -update — Pull latest, rebuild, restart
|
||||
#
|
||||
# Usage: sudo ./install.sh
|
||||
# Installs to /opt/rx/ and sets up a systemd service.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="/opt/rx"
|
||||
BIN_PATH="/opt/rx/receiptnext"
|
||||
SERVICE_NAME="receiptnext"
|
||||
REPO_URL="https://git.lohmar.co.uk/cclohmar/ReceiptNext.git"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
|
|
@ -22,98 +24,218 @@ NC='\033[0m'
|
|||
|
||||
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||
prompt() { echo -en "${CYAN}→${NC} $1"; }
|
||||
err() { echo -e "${RED}[✗]${NC} $1"; }
|
||||
ask() { echo -en "${CYAN}→${NC} $1"; }
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Please run as root: sudo ./install.sh${NC}"
|
||||
err "Please run as root: sudo ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# UPDATE MODE
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
if [ "${1:-}" = "-update" ]; then
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════╗"
|
||||
echo " ║ ReceiptNext — Update Mode ║"
|
||||
echo " ╚═══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
if [ ! -d "$INSTALL_DIR" ]; then
|
||||
err "$INSTALL_DIR does not exist. Run install.sh without -update first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
if [ -d .git ]; then
|
||||
info "Pulling latest code..."
|
||||
git pull
|
||||
else
|
||||
warn "Not a git repository — re-cloning..."
|
||||
cd /tmp
|
||||
rm -rf receiptnext-update
|
||||
git clone "$REPO_URL" receiptnext-update
|
||||
rsync -a --delete receiptnext-update/ "$INSTALL_DIR/"
|
||||
rm -rf receiptnext-update
|
||||
cd "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
info "Rebuilding binary..."
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o receiptnext .
|
||||
|
||||
info "Restarting service..."
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
info "Update complete — ReceiptNext is running"
|
||||
else
|
||||
warn "Service did not start. Check: systemctl status $SERVICE_NAME"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# FRESH INSTALL
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════╗"
|
||||
echo " ║ ReceiptNext Installer ║"
|
||||
echo " ║ ReceiptNext Installer ║"
|
||||
echo " ╚═══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ── Detect binary ────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BINARY=""
|
||||
for f in "$SCRIPT_DIR/receiptnext" "$SCRIPT_DIR/dist/receiptnext-linux-amd64" "$SCRIPT_DIR/dist/receiptnext-linux-arm64"; do
|
||||
if [ -f "$f" ] && [ -x "$f" ]; then
|
||||
BINARY="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$BINARY" ]; then
|
||||
echo -e "${RED}No binary found. Build it first: go build -o receiptnext .${NC}"
|
||||
exit 1
|
||||
# ── 1. Clone repo ────────────────────────────────────────────────
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
warn "$INSTALL_DIR already exists — pulling latest..."
|
||||
cd "$INSTALL_DIR"
|
||||
git pull
|
||||
else
|
||||
info "Cloning repository to $INSTALL_DIR..."
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
BINARY_NAME=$(basename "$BINARY")
|
||||
info "Using binary: $BINARY"
|
||||
# ── 2. Build binary ──────────────────────────────────────────────
|
||||
info "Building binary (pure Go, no CGO)..."
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o receiptnext .
|
||||
info "Binary built: $INSTALL_DIR/receiptnext"
|
||||
|
||||
# ── Create install directory ─────────────────────────────────────
|
||||
mkdir -p "$INSTALL_DIR"/{templates,static/css,static/icons,storage}
|
||||
info "Created $INSTALL_DIR"
|
||||
# ── 3. Create runtime directories ────────────────────────────────
|
||||
mkdir -p storage
|
||||
chmod 755 templates static storage
|
||||
|
||||
# ── Copy binary and assets ───────────────────────────────────────
|
||||
cp "$BINARY" "$BIN_PATH"
|
||||
chmod +x "$BIN_PATH"
|
||||
cp -r "$SCRIPT_DIR/templates/." "$INSTALL_DIR/templates/"
|
||||
cp -r "$SCRIPT_DIR/static/." "$INSTALL_DIR/static/"
|
||||
info "Copied binary + assets to $INSTALL_DIR"
|
||||
|
||||
# ── Prompt for configuration ─────────────────────────────────────
|
||||
# ── 4. AI Provider ───────────────────────────────────────────────
|
||||
echo ""
|
||||
echo " ── Configuration ──"
|
||||
echo " (Press Enter to skip any field)"
|
||||
echo " ── AI Provider ──"
|
||||
echo " Which AI should process receipt images?"
|
||||
echo " 1) Google Gemini (cloud API, needs API key)"
|
||||
echo " 2) OpenAI / Compatible (OpenAI, Perplexity, Groq, etc.)"
|
||||
echo " 3) Ollama (local, installs automatically)"
|
||||
echo ""
|
||||
ask " Choose [1-3] (default: 1): "
|
||||
read -r AI_CHOICE
|
||||
AI_CHOICE="${AI_CHOICE:-1}"
|
||||
|
||||
read -p " SMTP host [$SMTP_HOST]: " SMTP_HOST_IN
|
||||
SMTP_HOST="${SMTP_HOST_IN:-$SMTP_HOST}"
|
||||
read -p " SMTP port [${SMTP_PORT:-587}]: " SMTP_PORT_IN
|
||||
SMTP_PORT="${SMTP_PORT_IN:-${SMTP_PORT:-587}}"
|
||||
read -p " SMTP user [$SMTP_USER]: " SMTP_USER_IN
|
||||
SMTP_USER="${SMTP_USER_IN:-$SMTP_USER}"
|
||||
read -p " SMTP password [$SMTP_PASS]: " SMTP_PASS_IN
|
||||
SMTP_PASS="${SMTP_PASS_IN:-$SMTP_PASS}"
|
||||
read -p " Gemini API key [$GEMINI_API_KEY]: " GEMINI_IN
|
||||
GEMINI_API_KEY="${GEMINI_IN:-$GEMINI_API_KEY}"
|
||||
read -p " Public server URL [${BASE_URL:-http://localhost:8080}]: " BASE_URL_IN
|
||||
BASE_URL="${BASE_URL_IN:-${BASE_URL:-http://localhost:8080}}"
|
||||
read -p " Port for web server [${PORT:-8080}]: " PORT_IN
|
||||
PORT="${PORT_IN:-${PORT:-8080}}"
|
||||
case "$AI_CHOICE" in
|
||||
2)
|
||||
AI_PROVIDER="openai"
|
||||
ask " OpenAI API key: "
|
||||
read -r OPENAI_API_KEY
|
||||
ask " Model [gpt-4o-mini]: "
|
||||
read -r AI_MODEL
|
||||
AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
|
||||
ask " Base URL [https://api.openai.com/v1]: "
|
||||
read -r AI_BASE_URL
|
||||
AI_BASE_URL="${AI_BASE_URL:-https://api.openai.com/v1}"
|
||||
;;
|
||||
3)
|
||||
AI_PROVIDER="ollama"
|
||||
AI_MODEL="qwen3.5:2b"
|
||||
info "Installing Ollama..."
|
||||
if ! command -v ollama &>/dev/null; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh 2>&1 | tail -3
|
||||
else
|
||||
info "Ollama already installed"
|
||||
fi
|
||||
info "Pulling model $AI_MODEL (this may take a while)..."
|
||||
ollama pull "$AI_MODEL" 2>&1 | tail -3
|
||||
AI_BASE_URL="http://localhost:11434"
|
||||
;;
|
||||
*)
|
||||
AI_PROVIDER="gemini"
|
||||
ask " Gemini API key: "
|
||||
read -r GEMINI_API_KEY
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Write .env ───────────────────────────────────────────────────
|
||||
# ── 5. SMTP Configuration ────────────────────────────────────────
|
||||
echo ""
|
||||
echo " ── Email (SMTP) ──"
|
||||
echo " Required for sending OTP codes and expense reports."
|
||||
echo " Leave blank to skip (OTP codes will be logged to console)."
|
||||
echo ""
|
||||
ask " SMTP host: "
|
||||
read -r SMTP_HOST
|
||||
if [ -n "$SMTP_HOST" ]; then
|
||||
ask " SMTP port [587]: "
|
||||
read -r SMTP_PORT
|
||||
SMTP_PORT="${SMTP_PORT:-587}"
|
||||
ask " SMTP user: "
|
||||
read -r SMTP_USER
|
||||
ask " SMTP password: "
|
||||
read -r SMTP_PASS
|
||||
fi
|
||||
|
||||
# ── 6. General settings ──────────────────────────────────────────
|
||||
echo ""
|
||||
echo " ── General ──"
|
||||
ask " Public URL [http://localhost:8080]: "
|
||||
read -r BASE_URL
|
||||
BASE_URL="${BASE_URL:-http://localhost:8080}"
|
||||
ask " Web server port [8080]: "
|
||||
read -r PORT
|
||||
PORT="${PORT:-8080}"
|
||||
|
||||
# ── 7. Write .env ────────────────────────────────────────────────
|
||||
info "Creating .env..."
|
||||
cat > "$INSTALL_DIR/.env" << ENVEOF
|
||||
# ReceiptNext Configuration
|
||||
# Created by install.sh on $(date)
|
||||
# Generated by install.sh on $(date)
|
||||
|
||||
PORT=${PORT}
|
||||
BASE_URL=${BASE_URL}
|
||||
AI_PROVIDER=${AI_PROVIDER}
|
||||
ENVEOF
|
||||
|
||||
case "$AI_PROVIDER" in
|
||||
openai)
|
||||
cat >> "$INSTALL_DIR/.env" << ENVEOF
|
||||
OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
AI_MODEL=${AI_MODEL}
|
||||
AI_BASE_URL=${AI_BASE_URL}
|
||||
ENVEOF
|
||||
;;
|
||||
ollama)
|
||||
cat >> "$INSTALL_DIR/.env" << ENVEOF
|
||||
AI_MODEL=${AI_MODEL}
|
||||
AI_BASE_URL=${AI_BASE_URL}
|
||||
ENVEOF
|
||||
;;
|
||||
gemini)
|
||||
cat >> "$INSTALL_DIR/.env" << ENVEOF
|
||||
GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
ENVEOF
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$SMTP_HOST" ]; then
|
||||
cat >> "$INSTALL_DIR/.env" << ENVEOF
|
||||
SMTP_HOST=${SMTP_HOST}
|
||||
SMTP_PORT=${SMTP_PORT}
|
||||
SMTP_USER=${SMTP_USER}
|
||||
SMTP_PASS=${SMTP_PASS}
|
||||
GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
BASE_URL=${BASE_URL}
|
||||
PORT=${PORT}
|
||||
ENVEOF
|
||||
fi
|
||||
|
||||
chmod 600 "$INSTALL_DIR/.env"
|
||||
info "Created $INSTALL_DIR/.env"
|
||||
info "Configuration saved to $INSTALL_DIR/.env"
|
||||
|
||||
# ── Create systemd service ───────────────────────────────────────
|
||||
# ── 8. Systemd service ───────────────────────────────────────────
|
||||
info "Creating systemd service..."
|
||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << SERVEOF
|
||||
[Unit]
|
||||
Description=ReceiptNext — AI-Powered Expense Tracker
|
||||
Documentation=https://git.lohmar.co.uk/cclohmar/ReceiptNext
|
||||
After=network.target
|
||||
After=network.target ollama.service
|
||||
Wants=ollama.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
ExecStart=${BIN_PATH}
|
||||
ExecStart=${INSTALL_DIR}/receiptnext
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
EnvironmentFile=${INSTALL_DIR}/.env
|
||||
|
|
@ -126,10 +248,11 @@ SERVEOF
|
|||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${SERVICE_NAME}"
|
||||
info "Systemd service created: ${SERVICE_NAME}"
|
||||
info "Service created: /etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
# ── Start service ────────────────────────────────────────────────
|
||||
systemctl start "${SERVICE_NAME}" 2>/dev/null || true
|
||||
# ── 9. Start ─────────────────────────────────────────────────────
|
||||
info "Starting ReceiptNext..."
|
||||
systemctl restart "${SERVICE_NAME}" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
|
|
@ -137,11 +260,14 @@ echo " ╔═══════════════════════
|
|||
echo " ║ Installation Complete! ║"
|
||||
echo " ╚═══════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " Install dir: $INSTALL_DIR"
|
||||
echo " Config: $INSTALL_DIR/.env"
|
||||
echo " Binary: $BIN_PATH"
|
||||
echo " Service: systemctl status $SERVICE_NAME"
|
||||
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
||||
echo " Install: $INSTALL_DIR"
|
||||
echo " Binary: $INSTALL_DIR/receiptnext"
|
||||
echo " Config: $INSTALL_DIR/.env"
|
||||
echo " Templates: $INSTALL_DIR/templates/"
|
||||
echo " Static: $INSTALL_DIR/static/"
|
||||
echo " Storage: $INSTALL_DIR/storage/"
|
||||
echo " Service: systemctl status $SERVICE_NAME"
|
||||
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
||||
echo ""
|
||||
|
||||
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
||||
|
|
@ -149,5 +275,10 @@ if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
|||
echo " Open http://localhost:${PORT} (or your server IP)"
|
||||
else
|
||||
warn "Service did not start. Check: systemctl status ${SERVICE_NAME}"
|
||||
echo " Logs: journalctl -u ${SERVICE_NAME} -n 50 --no-pager"
|
||||
journalctl -u "${SERVICE_NAME}" -n 20 --no-pager
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ── Update ──"
|
||||
echo " To update later: sudo ./install.sh -update"
|
||||
echo ""
|
||||
|
|
|
|||
Loading…
Reference in a new issue