feat: add packaging, deployment, error pages, and project docs
Phase 7 — Polish: - Error page template with styled 404/403/500 pages - Error rendering helper on Renderer Phase 8 — Packaging & Deployment: - Containerfile: multi-stage build, non-root user, health check, OCI labels with build date and git revision - Makefile: build, test, cross-compile, deb, rpm, container, tarballs, checksums targets - nfpm.yaml: .deb and .rpm package config - systemd service: hardened with NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp, RestrictSUIDSGID - Default environment file with commented examples - postinstall/preremove scripts (shellcheck validated) - compose.yaml: example Podman/Docker Compose - Caddyfile.example: subdomain, subpath, and remote proxy configs - CHANGELOG.md for release notes - CLAUDE.md with architecture, conventions, and quick reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
845b152f15
commit
1fc42bf1b2
16 changed files with 435 additions and 2 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,6 +1,10 @@
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
/favoritter
|
/favoritter
|
||||||
/dist/
|
/dist/*.tar.gz
|
||||||
|
/dist/*.deb
|
||||||
|
/dist/*.rpm
|
||||||
|
/dist/checksums.txt
|
||||||
|
/dist/favoritter_*
|
||||||
|
|
||||||
# Data (database and uploads)
|
# Data (database and uploads)
|
||||||
/data/
|
/data/
|
||||||
|
|
|
||||||
23
CHANGELOG.md
Normal file
23
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Favoritter are documented here.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- User registration with configurable signup modes (open, approval-required, closed)
|
||||||
|
- Favorites with description, optional URL, image upload, tags, and privacy controls
|
||||||
|
- Tag system with autocomplete, browsing, and admin management
|
||||||
|
- User profiles (public or limited visibility) with avatars
|
||||||
|
- Admin panel: user management, tag management, signup requests, site settings
|
||||||
|
- Atom feeds: global, per-user, per-tag
|
||||||
|
- JSON and CSV import/export for data portability
|
||||||
|
- JSON REST API under `/api/v1/`
|
||||||
|
- OpenGraph meta tags for link previews
|
||||||
|
- Argon2id password hashing
|
||||||
|
- CSRF protection, rate limiting, security headers
|
||||||
|
- Proxy-aware deployment (WireGuard/Tailscale support)
|
||||||
|
- Configurable base path for subdomain and subpath deployment
|
||||||
|
- Systemd service file with security hardening
|
||||||
|
- Containerfile for Podman/Docker deployment
|
||||||
|
- Makefile with cross-compilation, .deb/.rpm packaging targets
|
||||||
64
CLAUDE.md
Normal file
64
CLAUDE.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Favoritter — Project Guide
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build ./cmd/favoritter # Build
|
||||||
|
go test ./... # Test
|
||||||
|
FAVORITTER_DEV_MODE=true \
|
||||||
|
FAVORITTER_ADMIN_USERNAME=admin \
|
||||||
|
FAVORITTER_ADMIN_PASSWORD=dev \
|
||||||
|
go run ./cmd/favoritter # Run (dev mode)
|
||||||
|
make build # Build with version info
|
||||||
|
make test # Run tests
|
||||||
|
make container # Build container image
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Single Go binary serving HTML (server-rendered templates + HTMX) and a JSON API. SQLite for storage, filesystem for uploaded images. All templates and static assets are embedded via `go:embed`.
|
||||||
|
|
||||||
|
### Directory Layout
|
||||||
|
|
||||||
|
- `cmd/favoritter/` — Entry point, wiring, graceful shutdown
|
||||||
|
- `internal/config/` — Environment variable configuration
|
||||||
|
- `internal/database/` — SQLite connection, PRAGMAs, migration runner
|
||||||
|
- `internal/model/` — Domain types (no logic, no DB)
|
||||||
|
- `internal/store/` — Data access layer (one file per entity, plain SQL)
|
||||||
|
- `internal/handler/` — HTTP handlers for web UI
|
||||||
|
- `internal/handler/api/` — JSON REST API handlers
|
||||||
|
- `internal/middleware/` — HTTP middleware (auth, CSRF, rate limit, etc.)
|
||||||
|
- `internal/render/` — Template rendering with layout support
|
||||||
|
- `internal/image/` — Image upload processing (validate, resize, strip EXIF)
|
||||||
|
- `web/templates/` — HTML templates (layouts, pages, partials)
|
||||||
|
- `web/static/` — CSS, JS, vendored Pico CSS + HTMX
|
||||||
|
- `dist/` — Packaging artifacts (systemd, env file, install scripts)
|
||||||
|
|
||||||
|
### Key Design Decisions
|
||||||
|
|
||||||
|
- **Go 1.22+ stdlib router** — no framework, `http.ServeMux` with method routing
|
||||||
|
- **3 external dependencies** — modernc.org/sqlite (pure Go), golang.org/x/crypto (Argon2id), gorilla/feeds
|
||||||
|
- **`SetMaxOpenConns(1)`** — SQLite works best with a single writer; PRAGMAs are set once on the single connection
|
||||||
|
- **Templates embedded in binary** — `//go:embed` for single-binary deployment; dev mode reads from disk for live reload
|
||||||
|
- **Middleware chain order matters** — Recovery → SecurityHeaders → BasePath → RealIP → Logger → SessionLoader → CSRF → MustResetGuard
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
SQLite with WAL mode. Migrations are embedded SQL files in `internal/database/migrations/`, applied sequentially on startup. Forward-only — no down migrations.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Norwegian Bokmål** for all user-facing text (templates, flash messages, error text)
|
||||||
|
- **SPDX license headers** on all source files
|
||||||
|
- **Argon2id** for password hashing (never bcrypt)
|
||||||
|
- **UUID filenames** for all uploads — never use user-provided filenames
|
||||||
|
- **Errors**: log with `slog.Error` at the handler level, return generic messages to users — never leak internal errors
|
||||||
|
- **Tests**: use real in-memory SQLite (`:memory:`), set fast Argon2 params (`Memory=1024, Time=1`) in tests
|
||||||
|
- **Session cookie name**: use `middleware.SessionCookieName` constant, never hardcode `"session"`
|
||||||
|
- **CSRF**: auto-included in HTMX requests via `htmx:configRequest` JS hook
|
||||||
|
|
||||||
|
## Hosting
|
||||||
|
|
||||||
|
- Hosted on Forgejo at `kode.naiv.no` — use `fj` CLI, not `gh`
|
||||||
|
- Supports deployment as: standalone binary, .deb package, .rpm package, or container
|
||||||
|
- Reverse proxy (Caddy) may be on a different machine — always use `EXTERNAL_URL` and `TRUSTED_PROXIES`
|
||||||
32
Caddyfile.example
Normal file
32
Caddyfile.example
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Example Caddy configurations for Favoritter.
|
||||||
|
# Copy the relevant section to your Caddyfile.
|
||||||
|
|
||||||
|
# --- Option 1: Subdomain deployment ---
|
||||||
|
# Set FAVORITTER_EXTERNAL_URL=https://faves.example.com
|
||||||
|
# Set FAVORITTER_TRUSTED_PROXIES to Caddy's IP
|
||||||
|
|
||||||
|
faves.example.com {
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Option 2: Subpath deployment ---
|
||||||
|
# Set FAVORITTER_BASE_PATH=/faves
|
||||||
|
# Set FAVORITTER_EXTERNAL_URL=https://example.com/faves
|
||||||
|
# Set FAVORITTER_TRUSTED_PROXIES to Caddy's IP
|
||||||
|
|
||||||
|
# example.com {
|
||||||
|
# handle_path /faves/* {
|
||||||
|
# reverse_proxy localhost:8080
|
||||||
|
# }
|
||||||
|
# # Redirect /faves to /faves/
|
||||||
|
# redir /faves /faves/ permanent
|
||||||
|
# }
|
||||||
|
|
||||||
|
# --- Option 3: Remote proxy (WireGuard/Tailscale) ---
|
||||||
|
# Caddy runs on a different machine than Favoritter.
|
||||||
|
# Set FAVORITTER_TRUSTED_PROXIES=100.64.0.0/10 (Tailscale)
|
||||||
|
# or the specific WireGuard IP of the Caddy machine.
|
||||||
|
|
||||||
|
# faves.example.com {
|
||||||
|
# reverse_proxy 100.64.1.2:8080
|
||||||
|
# }
|
||||||
42
Containerfile
Normal file
42
Containerfile
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Build: BUILDAH_FORMAT=docker podman build \
|
||||||
|
# --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
# --build-arg GIT_REVISION="$(git describe --always --dirty)" \
|
||||||
|
# -t favoritter .
|
||||||
|
|
||||||
|
FROM docker.io/library/golang:1.23-bookworm AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG BUILD_DATE=unknown
|
||||||
|
RUN CGO_ENABLED=0 go build \
|
||||||
|
-ldflags="-s -w -X main.version=${VERSION} -X main.buildDate=${BUILD_DATE}" \
|
||||||
|
-o /favoritter ./cmd/favoritter
|
||||||
|
|
||||||
|
FROM docker.io/library/debian:bookworm-slim
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG GIT_REVISION
|
||||||
|
LABEL org.opencontainers.image.created="${BUILD_DATE}" \
|
||||||
|
org.opencontainers.image.revision="${GIT_REVISION}" \
|
||||||
|
org.opencontainers.image.source="https://kode.naiv.no/olemd/favoritter" \
|
||||||
|
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||||
|
org.opencontainers.image.title="Favoritter" \
|
||||||
|
org.opencontainers.image.description="Self-hosted favorites web app"
|
||||||
|
RUN printf 'build_date=%s\ngit_revision=%s\n' "${BUILD_DATE}" "${GIT_REVISION}" > /etc/build-info
|
||||||
|
|
||||||
|
RUN useradd -r -s /usr/sbin/nologin favoritter \
|
||||||
|
&& mkdir -p /data/uploads \
|
||||||
|
&& chown -R favoritter:favoritter /data
|
||||||
|
USER favoritter
|
||||||
|
COPY --from=builder /favoritter /usr/local/bin/favoritter
|
||||||
|
|
||||||
|
ENV FAVORITTER_DB_PATH=/data/favoritter.db \
|
||||||
|
FAVORITTER_UPLOAD_DIR=/data/uploads \
|
||||||
|
FAVORITTER_LISTEN=:8080
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME ["/data"]
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s CMD ["/usr/local/bin/favoritter", "-healthcheck"]
|
||||||
|
ENTRYPOINT ["/usr/local/bin/favoritter"]
|
||||||
85
Makefile
Normal file
85
Makefile
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# Favoritter build system
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||||
|
BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
LDFLAGS := -s -w -X main.version=$(VERSION) -X main.buildDate=$(BUILD_DATE)
|
||||||
|
PLATFORMS := linux/amd64 linux/arm64
|
||||||
|
DIST := dist
|
||||||
|
|
||||||
|
.PHONY: build build-all deb rpm container artifacts checksums clean test lint
|
||||||
|
|
||||||
|
## Build for current platform
|
||||||
|
build:
|
||||||
|
go build -ldflags="$(LDFLAGS)" -o favoritter ./cmd/favoritter
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
## Cross-compile for all platforms
|
||||||
|
build-all: $(DIST)
|
||||||
|
@for platform in $(PLATFORMS); do \
|
||||||
|
os=$${platform%%/*}; \
|
||||||
|
arch=$${platform##*/}; \
|
||||||
|
echo "Building $$os/$$arch..."; \
|
||||||
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \
|
||||||
|
go build -ldflags="$(LDFLAGS)" \
|
||||||
|
-o $(DIST)/favoritter_$(VERSION)_$${os}_$${arch} \
|
||||||
|
./cmd/favoritter; \
|
||||||
|
done
|
||||||
|
|
||||||
|
## Build .deb packages (requires nfpm)
|
||||||
|
deb: build-all
|
||||||
|
@for platform in $(PLATFORMS); do \
|
||||||
|
arch=$${platform##*/}; \
|
||||||
|
echo "Packaging deb for $$arch..."; \
|
||||||
|
ARCH=$$arch VERSION=$(VERSION) nfpm package \
|
||||||
|
--packager deb \
|
||||||
|
--target $(DIST)/favoritter_$(VERSION)_$${arch}.deb; \
|
||||||
|
done
|
||||||
|
|
||||||
|
## Build .rpm packages (requires nfpm)
|
||||||
|
rpm: build-all
|
||||||
|
@for platform in $(PLATFORMS); do \
|
||||||
|
arch=$${platform##*/}; \
|
||||||
|
echo "Packaging rpm for $$arch..."; \
|
||||||
|
ARCH=$$arch VERSION=$(VERSION) nfpm package \
|
||||||
|
--packager rpm \
|
||||||
|
--target $(DIST)/favoritter_$(VERSION)_$${arch}.rpm; \
|
||||||
|
done
|
||||||
|
|
||||||
|
## Build container image
|
||||||
|
container:
|
||||||
|
BUILDAH_FORMAT=docker podman build \
|
||||||
|
--build-arg BUILD_DATE="$(BUILD_DATE)" \
|
||||||
|
--build-arg GIT_REVISION="$(VERSION)" \
|
||||||
|
--build-arg VERSION="$(VERSION)" \
|
||||||
|
-t favoritter:$(VERSION) \
|
||||||
|
-t favoritter:latest .
|
||||||
|
|
||||||
|
## Package binaries into tarballs
|
||||||
|
tarballs: build-all
|
||||||
|
@for platform in $(PLATFORMS); do \
|
||||||
|
os=$${platform%%/*}; \
|
||||||
|
arch=$${platform##*/}; \
|
||||||
|
name=favoritter_$(VERSION)_$${os}_$${arch}; \
|
||||||
|
echo "Creating tarball $$name.tar.gz..."; \
|
||||||
|
tar -czf $(DIST)/$$name.tar.gz \
|
||||||
|
-C $(DIST) $${name} \
|
||||||
|
-C $(CURDIR) README.md LICENSE; \
|
||||||
|
done
|
||||||
|
|
||||||
|
## Generate SHA256 checksums
|
||||||
|
checksums:
|
||||||
|
cd $(DIST) && sha256sum *.tar.gz *.deb *.rpm 2>/dev/null > checksums.txt || true
|
||||||
|
|
||||||
|
## Build all release artifacts
|
||||||
|
artifacts: tarballs deb rpm checksums
|
||||||
|
|
||||||
|
## Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -rf $(DIST) favoritter
|
||||||
|
|
||||||
|
$(DIST):
|
||||||
|
mkdir -p $(DIST)
|
||||||
24
compose.yaml
Normal file
24
compose.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Example Podman/Docker Compose configuration.
|
||||||
|
# Usage: podman-compose up -d
|
||||||
|
|
||||||
|
services:
|
||||||
|
favoritter:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Containerfile
|
||||||
|
args:
|
||||||
|
BUILD_DATE: "${BUILD_DATE:-unknown}"
|
||||||
|
GIT_REVISION: "${GIT_REVISION:-unknown}"
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- favoritter-data:/data
|
||||||
|
environment:
|
||||||
|
FAVORITTER_ADMIN_USERNAME: "${FAVORITTER_ADMIN_USERNAME:-admin}"
|
||||||
|
FAVORITTER_ADMIN_PASSWORD: "${FAVORITTER_ADMIN_PASSWORD:?Set FAVORITTER_ADMIN_PASSWORD}"
|
||||||
|
FAVORITTER_EXTERNAL_URL: "${FAVORITTER_EXTERNAL_URL:-}"
|
||||||
|
FAVORITTER_SITE_NAME: "${FAVORITTER_SITE_NAME:-Favoritter}"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
favoritter-data:
|
||||||
16
dist/favoritter.env
vendored
Normal file
16
dist/favoritter.env
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Favoritter configuration
|
||||||
|
# See README.md for all available environment variables.
|
||||||
|
|
||||||
|
FAVORITTER_DB_PATH=/var/lib/favoritter/favoritter.db
|
||||||
|
FAVORITTER_UPLOAD_DIR=/var/lib/favoritter/uploads
|
||||||
|
FAVORITTER_LISTEN=127.0.0.1:8080
|
||||||
|
|
||||||
|
# Uncomment and set on first run to create the admin user:
|
||||||
|
# FAVORITTER_ADMIN_USERNAME=admin
|
||||||
|
# FAVORITTER_ADMIN_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Set this to your public URL for correct feeds, cookies, and OpenGraph:
|
||||||
|
# FAVORITTER_EXTERNAL_URL=https://faves.example.com
|
||||||
|
|
||||||
|
# If your reverse proxy is on another machine (WireGuard/Tailscale):
|
||||||
|
# FAVORITTER_TRUSTED_PROXIES=100.64.0.0/10
|
||||||
25
dist/favoritter.service
vendored
Normal file
25
dist/favoritter.service
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Favoritter - Self-hosted favorites web app
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=favoritter
|
||||||
|
Group=favoritter
|
||||||
|
EnvironmentFile=/etc/favoritter/favoritter.env
|
||||||
|
ExecStart=/usr/bin/favoritter
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
ReadWritePaths=/var/lib/favoritter
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
RestrictSUIDSGID=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
21
dist/postinstall.sh
vendored
Executable file
21
dist/postinstall.sh
vendored
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Post-install script for Favoritter .deb/.rpm package.
|
||||||
|
# Creates the system user and sets directory permissions.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Create system user if it doesn't exist.
|
||||||
|
if ! getent passwd favoritter >/dev/null 2>&1; then
|
||||||
|
useradd -r -s /usr/sbin/nologin -d /var/lib/favoritter favoritter
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure data directories exist with correct ownership.
|
||||||
|
install -d -o favoritter -g favoritter -m 0750 /var/lib/favoritter
|
||||||
|
install -d -o favoritter -g favoritter -m 0750 /var/lib/favoritter/uploads
|
||||||
|
|
||||||
|
# Reload systemd to pick up the service file.
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl daemon-reload
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Favoritter installed. Configure /etc/favoritter/favoritter.env then run:"
|
||||||
|
echo " sudo systemctl enable --now favoritter"
|
||||||
9
dist/preremove.sh
vendored
Executable file
9
dist/preremove.sh
vendored
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Pre-remove script for Favoritter .deb/.rpm package.
|
||||||
|
# Stops the service before package removal.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl stop favoritter 2>/dev/null || true
|
||||||
|
systemctl disable favoritter 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
@ -143,3 +143,8 @@ func (h *Handler) Routes() *http.ServeMux {
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleNotFound renders a styled 404 page.
|
||||||
|
func (h *Handler) handleNotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.deps.Renderer.Error(w, r, http.StatusNotFound, "Ikke funnet")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ func RequireAdmin(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := UserFromContext(r.Context())
|
user := UserFromContext(r.Context())
|
||||||
if user == nil || !user.IsAdmin() {
|
if user == nil || !user.IsAdmin() {
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte("Forbidden"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,18 @@ func (r *Renderer) Page(w http.ResponseWriter, req *http.Request, name string, d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error renders an error page with the given HTTP status code.
|
||||||
|
func (r *Renderer) Error(w http.ResponseWriter, req *http.Request, code int, message string) {
|
||||||
|
w.WriteHeader(code)
|
||||||
|
r.Page(w, req, "error", PageData{
|
||||||
|
Title: message,
|
||||||
|
Data: map[string]any{
|
||||||
|
"Code": code,
|
||||||
|
"Message": message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Partial renders a partial template (for HTMX responses).
|
// Partial renders a partial template (for HTMX responses).
|
||||||
func (r *Renderer) Partial(w io.Writer, name string, data any) error {
|
func (r *Renderer) Partial(w io.Writer, name string, data any) error {
|
||||||
key := "partial:" + name
|
key := "partial:" + name
|
||||||
|
|
|
||||||
52
nfpm.yaml
Normal file
52
nfpm.yaml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# nfpm configuration for building .deb and .rpm packages.
|
||||||
|
# https://nfpm.goreleaser.com/
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ARCH=amd64 VERSION=1.0.0 nfpm package --packager deb --target dist/
|
||||||
|
# ARCH=arm64 VERSION=1.0.0 nfpm package --packager rpm --target dist/
|
||||||
|
|
||||||
|
name: favoritter
|
||||||
|
arch: "${ARCH}"
|
||||||
|
platform: linux
|
||||||
|
version: "${VERSION}"
|
||||||
|
maintainer: "Ole M. <olemd@kode.naiv.no>"
|
||||||
|
description: "Self-hosted favorites web app"
|
||||||
|
vendor: ""
|
||||||
|
homepage: "https://kode.naiv.no/olemd/favoritter"
|
||||||
|
license: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
contents:
|
||||||
|
- src: ./dist/favoritter_${VERSION}_linux_${ARCH}
|
||||||
|
dst: /usr/bin/favoritter
|
||||||
|
file_info:
|
||||||
|
mode: 0755
|
||||||
|
- src: ./dist/favoritter.service
|
||||||
|
dst: /lib/systemd/system/favoritter.service
|
||||||
|
- src: ./dist/favoritter.env
|
||||||
|
dst: /etc/favoritter/favoritter.env
|
||||||
|
type: config|noreplace
|
||||||
|
- dst: /var/lib/favoritter
|
||||||
|
type: dir
|
||||||
|
file_info:
|
||||||
|
mode: 0750
|
||||||
|
owner: favoritter
|
||||||
|
group: favoritter
|
||||||
|
- dst: /var/lib/favoritter/uploads
|
||||||
|
type: dir
|
||||||
|
file_info:
|
||||||
|
mode: 0750
|
||||||
|
owner: favoritter
|
||||||
|
group: favoritter
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
postinstall: ./dist/postinstall.sh
|
||||||
|
preremove: ./dist/preremove.sh
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
deb:
|
||||||
|
depends:
|
||||||
|
- libc6
|
||||||
|
rpm:
|
||||||
|
depends:
|
||||||
|
- glibc
|
||||||
18
web/templates/pages/error.html
Normal file
18
web/templates/pages/error.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{{define "content"}}
|
||||||
|
{{with .Data}}
|
||||||
|
<article>
|
||||||
|
<hgroup>
|
||||||
|
<h1>{{.Code}}</h1>
|
||||||
|
<p>{{.Message}}</p>
|
||||||
|
</hgroup>
|
||||||
|
{{if eq .Code 404}}
|
||||||
|
<p>Siden du leter etter finnes ikke. Den kan ha blitt flyttet eller slettet.</p>
|
||||||
|
{{else if eq .Code 403}}
|
||||||
|
<p>Du har ikke tilgang til denne siden.</p>
|
||||||
|
{{else}}
|
||||||
|
<p>Noe gikk galt. Prøv igjen senere.</p>
|
||||||
|
{{end}}
|
||||||
|
<p><a href="{{basePath}}/">Tilbake til forsiden</a></p>
|
||||||
|
</article>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue