diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..51bc7dd --- /dev/null +++ b/Makefile @@ -0,0 +1,85 @@ +# ReceiptNext — AI-Powered Expense Tracker +# Makefile for build, install, and deployment + +BINARY = receiptnext +OUTDIR = dist +VERSION = v1.0.0 +LDFLAGS = -s -w + +.PHONY: all build clean install uninstall run stop restart logs + +all: build + +# ------------------------------------------------------------------- +# Build +# ------------------------------------------------------------------- + +build: + go build -ldflags="$(LDFLAGS)" -o $(BINARY) . + +build-linux-amd64: + GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(OUTDIR)/$(BINARY)-linux-amd64 . + +build-linux-arm64: + GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -ldflags="$(LDFLAGS)" -o $(OUTDIR)/$(BINARY)-linux-arm64 . + +build-all: build-linux-amd64 build-linux-arm64 + +# ------------------------------------------------------------------- +# Install (systemd service) +# ------------------------------------------------------------------- + +install: build + @echo "==> Installing ReceiptNext..." + cp $(BINARY) /usr/local/bin/$(BINARY) + @if [ ! -f /etc/$(BINARY)/.env ]; then \ + mkdir -p /etc/$(BINARY); \ + cp .env.example /etc/$(BINARY)/.env; \ + echo "==> Created /etc/$(BINARY)/.env — edit it with your credentials"; \ + fi + @if [ ! -f /etc/systemd/system/$(BINARY).service ]; then \ + cp contrib/$(BINARY).service /etc/systemd/system/; \ + systemctl daemon-reload; \ + systemctl enable $(BINARY); \ + echo "==> Systemd service installed"; \ + fi + @echo "==> Run 'systemctl start $(BINARY)' to start" + @echo "==> Run 'systemctl status $(BINARY)' to check status" + +uninstall: + -systemctl stop $(BINARY) 2>/dev/null + -systemctl disable $(BINARY) 2>/dev/null + -rm -f /etc/systemd/system/$(BINARY).service + -systemctl daemon-reload + -rm -f /usr/local/bin/$(BINARY) + @echo "==> ReceiptNext uninstalled" + +# ------------------------------------------------------------------- +# Service management +# ------------------------------------------------------------------- + +run: + ./$(BINARY) + +start: + systemctl start $(BINARY) + +stop: + systemctl stop $(BINARY) + +restart: + systemctl restart $(BINARY) + +logs: + journalctl -u $(BINARY) -f + +status: + systemctl status $(BINARY) + +# ------------------------------------------------------------------- +# Clean +# ------------------------------------------------------------------- + +clean: + rm -f $(BINARY) + rm -rf $(OUTDIR) diff --git a/contrib/receiptnext.service b/contrib/receiptnext.service new file mode 100644 index 0000000..608c22e --- /dev/null +++ b/contrib/receiptnext.service @@ -0,0 +1,25 @@ +[Unit] +Description=ReceiptNext — AI-Powered Expense Tracker +Documentation=https://git.lohmar.co.uk/cclohmar/ReceiptNext +After=network.target + +[Service] +Type=simple +User=receiptnext +Group=receiptnext +WorkingDirectory=/opt/receiptnext +ExecStart=/usr/local/bin/receiptnext +Restart=always +RestartSec=5 +EnvironmentFile=-/etc/receiptnext/.env +StandardOutput=append:/var/log/receiptnext.log +StandardError=append:/var/log/receiptnext.log + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=full +ProtectHome=true + +[Install] +WantedBy=multi-user.target diff --git a/go.mod b/go.mod index c3b2220..45842a2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/expenseflow +module github.com/cclohmar/ReceiptNext go 1.22 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..25a4b09 --- /dev/null +++ b/install.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# +# install.sh — ReceiptNext one-command installer for bare-metal servers +# +# Usage: +# curl -sL https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/install.sh | bash +# +# Or from a local clone: +# ./install.sh +# +# What it does: +# 1. Creates receiptnext user if missing +# 2. Creates /opt/receiptnext with templates/, static/, storage/ +# 3. Installs binary to /usr/local/bin/receiptnext +# 4. Creates /etc/receiptnext/.env from .env.example (if not exists) +# 5. Installs systemd service +# 6. Starts the service +# 7. Shows status and instructions + +set -euo pipefail + +BINARY="receiptnext" +INSTALL_DIR="/opt/${BINARY}" +BIN_DIR="/usr/local/bin" +ETC_DIR="/etc/${BINARY}" +SERVICE_FILE="contrib/${BINARY}.service" +LOG_FILE="/var/log/${BINARY}.log" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +info() { echo -e "${GREEN}[✓]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[✗]${NC} $1"; } + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + error "Please run as root: sudo ./install.sh" + exit 1 +fi + +echo "" +echo " ╔═══════════════════════════════════════╗" +echo " ║ ReceiptNext Installer ║" +echo " ║ AI-Powered Expense Tracker ║" +echo " ╚═══════════════════════════════════════╝" +echo "" + +# ── Step 1: Build or download binary ────────────────────────────── +if [ -f "./${BINARY}" ]; then + info "Binary found locally" +elif [ -f "go.mod" ]; then + info "Building from source..." + if ! command -v go &>/dev/null; then + error "Go is required to build from source. Install go 1.22+ and re-run." + exit 1 + fi + go build -ldflags="-s -w" -o "${BINARY}" . + info "Build complete" +else + # Try to download the latest release + TAG=$(curl -s https://git.lohmar.co.uk/api/v1/repos/cclohmar/ReceiptNext/releases/latest \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('tag_name','v1.0.0'))" 2>/dev/null || echo "v1.0.0") + ARCH=$(uname -m) + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) error "Unsupported architecture: $ARCH"; exit 1 ;; + esac + URL="https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/${TAG}/${BINARY}-linux-${ARCH}" + info "Downloading ${URL}..." + curl -sL -o "${BINARY}" "${URL}" + info "Download complete" +fi + +chmod +x "${BINARY}" + +# ── Step 2: Create system user ──────────────────────────────────── +if ! id -u ${BINARY} &>/dev/null 2>&1; then + useradd -r -s /bin/false -d "${INSTALL_DIR}" ${BINARY} + info "Created system user '${BINARY}'" +fi + +# ── Step 3: Create install directory ────────────────────────────── +mkdir -p "${INSTALL_DIR}"/{templates,static/css,static/icons,storage} + +# Copy templates and static assets +cp -r templates/* "${INSTALL_DIR}/templates/" +cp -r static/* "${INSTALL_DIR}/static/" +cp -r storage/. "${INSTALL_DIR}/storage/" 2>/dev/null || true + +# ── Step 4: Install binary ──────────────────────────────────────── +cp "${BINARY}" "${BIN_DIR}/${BINARY}" +chmod +x "${BIN_DIR}/${BINARY}" +info "Installed binary to ${BIN_DIR}/${BINARY}" + +# ── Step 5: Create config directory ─────────────────────────────── +mkdir -p "${ETC_DIR}" +if [ ! -f "${ETC_DIR}/.env" ]; then + if [ -f ".env.example" ]; then + cp .env.example "${ETC_DIR}/.env" + info "Created ${ETC_DIR}/.env — edit it with your credentials" + else + warn "No .env.example found — create ${ETC_DIR}/.env manually" + fi +else + info "${ETC_DIR}/.env already exists — keeping existing config" +fi + +# ── Step 6: Install systemd service ─────────────────────────────── +if [ -f "${SERVICE_FILE}" ]; then + cp "${SERVICE_FILE}" /etc/systemd/system/ + systemctl daemon-reload + systemctl enable ${BINARY} + info "Systemd service installed and enabled" +fi + +# ── Step 7: Set permissions ─────────────────────────────────────── +chown -R ${BINARY}:${BINARY} "${INSTALL_DIR}" +chown ${BINARY}:${BINARY} "${ETC_DIR}/.env" 2>/dev/null || true +touch "${LOG_FILE}" && chown ${BINARY}:${BINARY} "${LOG_FILE}" + +# ── Step 8: Start service ───────────────────────────────────────── +systemctl start ${BINARY} || warn "Service failed to start — check 'systemctl status ${BINARY}'" +sleep 2 + +echo "" +echo " ╔═══════════════════════════════════════╗" +echo " ║ Installation Complete! ║" +echo " ╚═══════════════════════════════════════╝" +echo "" +echo " Config: ${ETC_DIR}/.env" +echo " Binary: ${BIN_DIR}/${BINARY}" +echo " Data: ${INSTALL_DIR}" +echo " Logs: ${LOG_FILE}" +echo " Service: systemctl status ${BINARY}" +echo " Restart: systemctl restart ${BINARY}" +echo " Log tail: journalctl -u ${BINARY} -f" +echo "" + +if systemctl is-active --quiet ${BINARY}; then + info "ReceiptNext is running!" + echo " Open http://YOUR_SERVER_IP:8080 in your browser" + echo "" + echo " First steps:" + echo " 1. Edit ${ETC_DIR}/.env with your SMTP + AI credentials" + echo " 2. Restart: systemctl restart ${BINARY}" + echo "" +else + warn "Service is not running. Check: systemctl status ${BINARY}" +fi diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index b4ec847..2037a24 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -12,10 +12,10 @@ import ( "strings" "time" - "github.com/expenseflow/internal/auth" - "github.com/expenseflow/internal/database" - "github.com/expenseflow/internal/email" - "github.com/expenseflow/internal/utils" + "github.com/cclohmar/ReceiptNext/internal/auth" + "github.com/cclohmar/ReceiptNext/internal/database" + "github.com/cclohmar/ReceiptNext/internal/email" + "github.com/cclohmar/ReceiptNext/internal/utils" ) // --------------------------------------------------------------------------- diff --git a/internal/handlers/events.go b/internal/handlers/events.go index b9f3526..162a1fb 100644 --- a/internal/handlers/events.go +++ b/internal/handlers/events.go @@ -12,8 +12,8 @@ import ( "strconv" "time" - "github.com/expenseflow/internal/database" - "github.com/expenseflow/internal/utils" + "github.com/cclohmar/ReceiptNext/internal/database" + "github.com/cclohmar/ReceiptNext/internal/utils" "github.com/go-chi/chi/v5" ) diff --git a/internal/handlers/expenses.go b/internal/handlers/expenses.go index a948aaa..ad62f32 100644 --- a/internal/handlers/expenses.go +++ b/internal/handlers/expenses.go @@ -17,9 +17,9 @@ import ( "strings" "time" - "github.com/expenseflow/internal/ai" - "github.com/expenseflow/internal/database" - "github.com/expenseflow/internal/utils" + "github.com/cclohmar/ReceiptNext/internal/ai" + "github.com/cclohmar/ReceiptNext/internal/database" + "github.com/cclohmar/ReceiptNext/internal/utils" "github.com/go-chi/chi/v5" ) diff --git a/internal/handlers/file.go b/internal/handlers/file.go index 0e224ae..9a507ce 100644 --- a/internal/handlers/file.go +++ b/internal/handlers/file.go @@ -20,8 +20,8 @@ import ( "github.com/go-chi/chi/v5" "github.com/jung-kurt/gofpdf" - "github.com/expenseflow/internal/database" - "github.com/expenseflow/internal/email" + "github.com/cclohmar/ReceiptNext/internal/database" + "github.com/cclohmar/ReceiptNext/internal/email" ) // --------------------------------------------------------------------------- diff --git a/main.go b/main.go index 18c4fe9..3a21742 100644 --- a/main.go +++ b/main.go @@ -22,10 +22,10 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" - "github.com/expenseflow/internal/auth" - "github.com/expenseflow/internal/database" - "github.com/expenseflow/internal/email" - "github.com/expenseflow/internal/handlers" + "github.com/cclohmar/ReceiptNext/internal/auth" + "github.com/cclohmar/ReceiptNext/internal/database" + "github.com/cclohmar/ReceiptNext/internal/email" + "github.com/cclohmar/ReceiptNext/internal/handlers" ) func main() {