diff --git a/install.sh b/install.sh index 8607f19..0467bf7 100755 --- a/install.sh +++ b/install.sh @@ -73,9 +73,23 @@ if [ "${1:-}" = "-update" ]; then cd "$INSTALL_DIR" fi + # Determine the app user from the existing service file (most reliable). + APP_USER="" + if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then + APP_USER=$(grep -Po '^User=\K.*' "/etc/systemd/system/${SERVICE_NAME}.service" 2>/dev/null || true) + fi + # Fallback: check the owner of the install directory or binary. + if [ -z "$APP_USER" ]; then + APP_USER=$(stat -c '%U' "$INSTALL_DIR/app" 2>/dev/null || stat -c '%U' "$INSTALL_DIR" 2>/dev/null || true) + fi + if [ -z "$APP_USER" ] || [ "$APP_USER" = "root" ]; then + APP_USER="${SUDO_USER:-$(whoami)}" + fi # Reset ownership so the app user can write files. - APP_USER="${APP_USER:-${SUDO_USER:-$(whoami)}}" sudo_if chown -R "${APP_USER}:${APP_USER}" "$INSTALL_DIR" 2>/dev/null || true + # Ensure key directories have correct permissions. + sudo_if chmod 755 "$INSTALL_DIR" "$INSTALL_DIR/templates" "$INSTALL_DIR/static" 2>/dev/null || true + sudo_if chmod 775 "$INSTALL_DIR/storage" 2>/dev/null || true # Backup the database before making any changes. if [ -f "$INSTALL_DIR/expenses.db" ]; then @@ -118,12 +132,12 @@ if [ "${1:-}" = "-update" ]; then export GOPATH="${INSTALL_DIR}/.go" export GOCACHE="${INSTALL_DIR}/.go/build" mkdir -p "${GOMODCACHE}" "${GOCACHE}" 2>/dev/null || sudo_if mkdir -p "${GOMODCACHE}" "${GOCACHE}" - sudo_if chown -R "${APP_USER:-${SUDO_USER:-$(whoami)}}" "${INSTALL_DIR}/.go" "${INSTALL_DIR}/app" 2>/dev/null || true + sudo_if chown -R "${APP_USER}" "${INSTALL_DIR}/.go" "${INSTALL_DIR}/app" 2>/dev/null || true sudo_if rm -f app CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" -o app . # Fix ownership of all files (build cache may be root-owned from earlier builds). - sudo_if chown -R "${APP_USER:-${SUDO_USER:-$(whoami)}}:${APP_USER:-${SUDO_USER:-$(whoami)}}" "$INSTALL_DIR" 2>/dev/null || true + sudo_if chown -R "${APP_USER}:${APP_USER}" "$INSTALL_DIR" 2>/dev/null || true info "Starting service..." sudo_if systemctl start "$SERVICE_NAME" diff --git a/internal/handlers/expenses.go b/internal/handlers/expenses.go index 8b9d165..2b9ded8 100644 --- a/internal/handlers/expenses.go +++ b/internal/handlers/expenses.go @@ -7,6 +7,7 @@ package handlers import ( "database/sql" "fmt" + "html/template" "io" "log" "net/http" @@ -59,7 +60,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(10 << 20); err != nil { log.Printf("ERROR [%s] handlers: UploadReceipt: parse form: %v", time.Now().Format(time.RFC3339), err) - http.Error(w, "Failed to parse upload form", http.StatusBadRequest) + renderUploadError(w, "Failed to parse upload form.") return } defer r.MultipartForm.RemoveAll() @@ -69,7 +70,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("ERROR [%s] handlers: UploadReceipt: missing receipt field: %v", time.Now().Format(time.RFC3339), err) - http.Error(w, "Missing receipt file", http.StatusBadRequest) + renderUploadError(w, "Missing receipt file.") return } defer file.Close() @@ -78,7 +79,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if header.Size > 10<<20 { log.Printf("ERROR [%s] handlers: UploadReceipt: file too large: %d bytes", time.Now().Format(time.RFC3339), header.Size) - http.Error(w, "File too large. Maximum size is 10 MB.", http.StatusBadRequest) + renderUploadError(w, "File too large. Maximum size is 10 MB.") return } @@ -87,7 +88,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("ERROR [%s] handlers: UploadReceipt: read file: %v", time.Now().Format(time.RFC3339), err) - http.Error(w, "Failed to read uploaded file", http.StatusInternalServerError) + renderUploadError(w, "Failed to read uploaded file.") return } @@ -96,7 +97,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if ext == "" { log.Printf("ERROR [%s] handlers: UploadReceipt: unsupported file type: %q", time.Now().Format(time.RFC3339), ext) - http.Error(w, "Unsupported file format. Please upload a receipt image (JPEG, PNG, HEIC) or PDF.", http.StatusBadRequest) + renderUploadError(w, "Unsupported file format. Please upload a receipt image (JPEG, PNG, HEIC) or PDF.") return } @@ -107,7 +108,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if err := os.MkdirAll("storage", 0755); err != nil { log.Printf("ERROR [%s] handlers: UploadReceipt: mkdir storage: %v", time.Now().Format(time.RFC3339), err) - http.Error(w, "Server error", http.StatusInternalServerError) + renderUploadError(w, "Server error. Please try again.") return } @@ -115,7 +116,7 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) { if err := os.WriteFile(storagePath, fileData, 0644); err != nil { log.Printf("ERROR [%s] handlers: UploadReceipt: write file: %v", time.Now().Format(time.RFC3339), err) - http.Error(w, "Failed to save receipt image", http.StatusInternalServerError) + renderUploadError(w, "Failed to save receipt image. Please try again.") return } @@ -586,6 +587,16 @@ func setCurrentEventID(w http.ResponseWriter, eventID string) { // Helpers // --------------------------------------------------------------------------- +// renderUploadError writes an HTMX-compatible error fragment into the +// #receipt-form container (the upload target). Uses HTTP 200 so HTMX +// always swaps the content (HTMX skips 4xx/5xx by default). +func renderUploadError(w http.ResponseWriter, message string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `