Create docs
This commit is contained in:
parent
4fa62fc164
commit
50b0b29e10
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Forgejo Tickets
|
||||||
|
|
||||||
|
A customer-facing support ticket system that bridges a web UI with Forgejo issue tracking. Customers create and manage support tickets through a clean web interface, while tickets automatically sync to Forgejo repositories as issues for internal team tracking.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your database URL, session secret, etc.
|
||||||
|
make build
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
The public server starts on `:8080` and the admin server on `:8081`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Topic | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| [Architecture](docs/architecture.md) | Two-server design, directory structure, package responsibilities, request lifecycle |
|
||||||
|
| [Configuration](docs/configuration.md) | Full environment variable reference with defaults and descriptions |
|
||||||
|
| [Deployment](docs/deployment.md) | Local dev, Docker, Nomad, Makefile targets, database setup |
|
||||||
|
| [User Guide](docs/user-guide.md) | Registration, login, tickets, comments, email notifications |
|
||||||
|
| [Admin Guide](docs/admin-guide.md) | Dashboard, user/ticket/repo management, Tailscale auth |
|
||||||
|
| [Forgejo Integration](docs/forgejo-integration.md) | Issue sync, comment sync, webhooks, auto-close flow |
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
# Admin Guide
|
||||||
|
|
||||||
|
The admin panel runs on a separate port (default `:8081`) and provides management capabilities for users, tickets, and repos.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The admin server is protected by Tailscale identity verification. Every request triggers a whois lookup against the local Tailscale daemon (`http://100.100.100.100/localapi/v0/whois`) to identify the connecting user.
|
||||||
|
|
||||||
|
Access is controlled by the `TAILSCALE_ALLOWED_USERS` environment variable — a comma-separated list of Tailscale login names (e.g. `user@example.com,admin@example.com`).
|
||||||
|
|
||||||
|
**Dev mode**: If `TAILSCALE_ALLOWED_USERS` is empty, all requests are allowed without authentication. This is useful for local development where Tailscale may not be available.
|
||||||
|
|
||||||
|
See [Configuration](./configuration.md#admin) for the variable reference.
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
The dashboard (`GET /`) displays aggregate counts:
|
||||||
|
|
||||||
|
- Total users
|
||||||
|
- Total tickets
|
||||||
|
- Open tickets
|
||||||
|
- In-progress tickets
|
||||||
|
- Closed tickets
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
### List Users
|
||||||
|
|
||||||
|
`GET /users` — Displays all users ordered by creation date (most recent first, up to 100).
|
||||||
|
|
||||||
|
### User Detail
|
||||||
|
|
||||||
|
`GET /users/:id` — Shows user information (name, email, verification status, creation date) and their tickets with product and status.
|
||||||
|
|
||||||
|
### Create User
|
||||||
|
|
||||||
|
`GET /users/new` — Form to create a new user.
|
||||||
|
|
||||||
|
`POST /users` — Submits the form. Requires name and email. The system:
|
||||||
|
|
||||||
|
1. Generates a random 16-character temporary password.
|
||||||
|
2. Creates the user with email marked as verified.
|
||||||
|
3. Sends a welcome email with the temporary password and a link to log in.
|
||||||
|
|
||||||
|
The admin is redirected to the new user's detail page.
|
||||||
|
|
||||||
|
## Tickets
|
||||||
|
|
||||||
|
### List Tickets
|
||||||
|
|
||||||
|
`GET /tickets` — Displays all tickets with user name/email, product, and status. Supports filtering by status via the `status` query parameter (e.g. `?status=open`). Up to 100 tickets are shown.
|
||||||
|
|
||||||
|
### Ticket Detail
|
||||||
|
|
||||||
|
`GET /tickets/:id` — Shows full ticket details: title, description, status, submitting user, associated product, and the comment thread.
|
||||||
|
|
||||||
|
### Update Status
|
||||||
|
|
||||||
|
`POST /tickets/:id/status` — Changes a ticket's status. Send a `status` form field with one of: `open`, `in_progress`, `closed`.
|
||||||
|
|
||||||
|
## Repos
|
||||||
|
|
||||||
|
Repos map customer-facing "products" to Forgejo repositories. Each repo defines where tickets are synced as issues and where webhooks come from.
|
||||||
|
|
||||||
|
### List Repos
|
||||||
|
|
||||||
|
`GET /repos` — Shows all repos with their name, slug, Forgejo owner/repo, active status, and the webhook URL.
|
||||||
|
|
||||||
|
### Add Repo
|
||||||
|
|
||||||
|
`GET /repos/new` — Form to add a new repo.
|
||||||
|
|
||||||
|
`POST /repos` — Creates the repo. All fields are required:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| Name | Display name shown to customers in the product dropdown |
|
||||||
|
| Slug | URL-safe identifier used in the webhook URL path |
|
||||||
|
| Forgejo Owner | Owner (user or org) of the Forgejo repository |
|
||||||
|
| Forgejo Repo | Name of the Forgejo repository |
|
||||||
|
| Webhook Secret | Shared secret for HMAC-SHA256 webhook signature verification |
|
||||||
|
| Active | Whether this product appears in the customer ticket creation form |
|
||||||
|
|
||||||
|
### Edit Repo
|
||||||
|
|
||||||
|
`GET /repos/:id/edit` — Form to edit an existing repo, pre-filled with current values. Also displays the webhook URL.
|
||||||
|
|
||||||
|
`POST /repos/:id` — Updates the repo with the submitted values.
|
||||||
|
|
||||||
|
### Webhook URL
|
||||||
|
|
||||||
|
Each repo's webhook URL follows the pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
{BASE_URL}/webhooks/forgejo/{slug}
|
||||||
|
```
|
||||||
|
|
||||||
|
This URL should be configured in the Forgejo repository's webhook settings. See [Forgejo Integration](./forgejo-integration.md#webhook-setup) for detailed setup instructions.
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Two-Server Design
|
||||||
|
|
||||||
|
The application runs two separate HTTP servers from a single binary:
|
||||||
|
|
||||||
|
- **Public server** (default `:8080`) — Customer-facing UI: registration, login, ticket management, OAuth callbacks, Forgejo webhooks.
|
||||||
|
- **Admin server** (default `:8081`) — Internal admin panel: dashboard, user/ticket/repo management. Protected by Tailscale authentication.
|
||||||
|
|
||||||
|
Both servers are started as goroutines in `cmd/server/main.go` and share the same database connection, template renderer, and service instances. Graceful shutdown handles SIGINT/SIGTERM with a 30-second timeout.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/
|
||||||
|
server/
|
||||||
|
main.go # Entry point — wires dependencies, starts both servers
|
||||||
|
|
||||||
|
internal/
|
||||||
|
config/
|
||||||
|
config.go # Loads env vars into Config struct
|
||||||
|
database/
|
||||||
|
database.go # PostgreSQL connection (GORM) and migration runner
|
||||||
|
models/
|
||||||
|
models.go # GORM models: User, OAuthAccount, Session, Repo, Ticket, TicketComment, EmailToken
|
||||||
|
auth/
|
||||||
|
auth.go # Service: register, login, password hashing, session create/destroy
|
||||||
|
session.go # Session middleware, RequireAuth, CurrentUser helpers
|
||||||
|
store.go # PGStore — PostgreSQL-backed gorilla/sessions store
|
||||||
|
tokens.go # Email verification and password reset token generation/redemption
|
||||||
|
oauth.go # Google and Microsoft OAuth provider setup, FindOrCreateOAuthUser
|
||||||
|
apple.go # Apple Sign In provider (JWT client secret, ID token parsing)
|
||||||
|
middleware/
|
||||||
|
middleware.go # RequestID, Logging, Recovery middleware
|
||||||
|
csrf.go # CSRF protection via gorilla/csrf adapted for Gin
|
||||||
|
handlers/
|
||||||
|
public/
|
||||||
|
routes.go # Public router setup and route registration
|
||||||
|
home.go # Landing page
|
||||||
|
auth.go # Login, register, logout, email verification, password reset
|
||||||
|
oauth.go # OAuth login/callback for Google, Microsoft, Apple
|
||||||
|
tickets.go # Ticket list, create, detail, add comment
|
||||||
|
webhook.go # Forgejo webhook receiver (issue close -> ticket close)
|
||||||
|
admin/
|
||||||
|
routes.go # Admin router setup and route registration
|
||||||
|
auth.go # Tailscale whois-based authentication middleware
|
||||||
|
dashboard.go # Dashboard with aggregate counts
|
||||||
|
users.go # User list, detail, create
|
||||||
|
tickets.go # Ticket list (with status filter), detail, status update
|
||||||
|
repos.go # Repo list, create, edit
|
||||||
|
templates/
|
||||||
|
render.go # Template renderer: parses layouts + partials + pages, injects PageData
|
||||||
|
funcs.go # Template helper functions
|
||||||
|
email/
|
||||||
|
email.go # Postmark email client: verification, password reset, ticket closed, welcome
|
||||||
|
templates.go # HTML email templates
|
||||||
|
forgejo/
|
||||||
|
client.go # Forgejo API client: create issue, create comment
|
||||||
|
webhook.go # Webhook signature verification (HMAC-SHA256) and payload parsing
|
||||||
|
|
||||||
|
web/
|
||||||
|
templates/
|
||||||
|
layouts/
|
||||||
|
base.html # Public page layout
|
||||||
|
admin.html # Admin page layout
|
||||||
|
pages/ # Page templates (mapped by path: "login", "tickets/list", "admin/dashboard", etc.)
|
||||||
|
partials/ # Shared partials (nav, flash messages)
|
||||||
|
static/
|
||||||
|
css/
|
||||||
|
input.css # Tailwind CSS source
|
||||||
|
output.css # Compiled CSS (generated by build)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Responsibilities
|
||||||
|
|
||||||
|
| Package | Role |
|
||||||
|
|---------|------|
|
||||||
|
| `config` | Reads all configuration from environment variables; validates required fields |
|
||||||
|
| `database` | Opens the GORM PostgreSQL connection; runs auto-migrations including enum types and custom indexes |
|
||||||
|
| `models` | Defines all database models and the `AutoMigrate` function |
|
||||||
|
| `auth` | User registration/login with bcrypt, session management via PostgreSQL-backed gorilla/sessions, email token generation/redemption, OAuth user linking |
|
||||||
|
| `middleware` | Request ID generation, structured logging, panic recovery, CSRF protection |
|
||||||
|
| `handlers/public` | All customer-facing HTTP handlers and route wiring |
|
||||||
|
| `handlers/admin` | All admin HTTP handlers, Tailscale auth middleware, and route wiring |
|
||||||
|
| `templates` | Parses Go HTML templates with layout/partial/page inheritance; injects current user, CSRF token, and flash messages into every render |
|
||||||
|
| `email` | Sends transactional emails via Postmark: verification, password reset, ticket closed notification, welcome/account creation |
|
||||||
|
| `forgejo` | Forgejo API client for creating issues and comments; webhook signature verification and payload parsing |
|
||||||
|
|
||||||
|
## Request Lifecycle
|
||||||
|
|
||||||
|
### Public Server
|
||||||
|
|
||||||
|
```
|
||||||
|
Request
|
||||||
|
-> RequestID middleware (generates X-Request-ID header)
|
||||||
|
-> Logging middleware (logs method, path, status, duration)
|
||||||
|
-> Recovery middleware (catches panics)
|
||||||
|
-> Session middleware (loads user from session cookie into context)
|
||||||
|
-> CSRF middleware (gorilla/csrf — on routes that need it)
|
||||||
|
-> Handler (processes request)
|
||||||
|
-> Template render (injects User, CSRFToken, Flash into PageData)
|
||||||
|
```
|
||||||
|
|
||||||
|
The webhook endpoint (`POST /webhooks/forgejo/:repoSlug`) sits outside the CSRF group since it authenticates via HMAC signature.
|
||||||
|
|
||||||
|
### Admin Server
|
||||||
|
|
||||||
|
```
|
||||||
|
Request
|
||||||
|
-> RequestID middleware
|
||||||
|
-> Logging middleware
|
||||||
|
-> Recovery middleware
|
||||||
|
-> Tailscale auth middleware (whois lookup to verify allowed user)
|
||||||
|
-> Handler
|
||||||
|
-> Template render
|
||||||
|
```
|
||||||
|
|
||||||
|
The admin server has no session/CSRF middleware — authentication is handled entirely by Tailscale identity.
|
||||||
|
|
||||||
|
## Gin + Gorilla Interop
|
||||||
|
|
||||||
|
The application uses [Gin](https://github.com/gin-gonic/gin) as the HTTP framework but relies on [gorilla/sessions](https://github.com/gorilla/sessions) for session management and [gorilla/csrf](https://github.com/gorilla/csrf) for CSRF protection.
|
||||||
|
|
||||||
|
Gorilla middleware expects `http.ResponseWriter` and `*http.Request`, while Gin provides `*gin.Context`. The CSRF middleware bridges this by wrapping Gin's handler chain as an `http.Handler` and passing `c.Writer` / `c.Request` through. After gorilla/csrf processes the request (potentially adding context values), the modified `*http.Request` is written back to `c.Request` so downstream handlers can access the CSRF token via `csrf.Token(req)`.
|
||||||
|
|
||||||
|
See [Configuration](./configuration.md) for environment variable details and [Deployment](./deployment.md) for running the application.
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
All configuration is read from environment variables. Copy `.env.example` to `.env` for local development.
|
||||||
|
|
||||||
|
## Environment Variable Reference
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `DATABASE_URL` | Yes | — | PostgreSQL connection string (e.g. `postgres://user:password@localhost:5432/forgejo_tickets?sslmode=disable`) |
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `PUBLIC_ADDR` | No | `:8080` | Listen address for the public (customer-facing) server |
|
||||||
|
| `ADMIN_ADDR` | No | `:8081` | Listen address for the admin server |
|
||||||
|
| `BASE_URL` | No | `http://localhost:8080` | Public base URL used to construct links in emails and OAuth redirects |
|
||||||
|
|
||||||
|
### Sessions
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `SESSION_SECRET` | Yes | — | Secret key for signing session cookies and CSRF tokens. Must be a random hex string. |
|
||||||
|
|
||||||
|
Generate a session secret:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forgejo
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `FORGEJO_URL` | No | `""` | Base URL of the Forgejo instance (e.g. `https://forgejo.example.com`) |
|
||||||
|
| `FORGEJO_API_TOKEN` | No | `""` | API token for authenticating with the Forgejo API. Needs permission to create issues and comments. |
|
||||||
|
|
||||||
|
Both must be set for ticket-to-issue sync to work. See [Forgejo Integration](./forgejo-integration.md) for details.
|
||||||
|
|
||||||
|
### Email (Postmark)
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `POSTMARK_SERVER_TOKEN` | No | `""` | Postmark server API token for sending transactional emails |
|
||||||
|
| `POSTMARK_FROM_EMAIL` | No | `""` | Sender email address (e.g. `support@example.com`) |
|
||||||
|
|
||||||
|
Both must be set for email sending to work. Without them, email operations will fail silently in the logs.
|
||||||
|
|
||||||
|
### OAuth — Google
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `GOOGLE_CLIENT_ID` | No | `""` | Google OAuth 2.0 client ID |
|
||||||
|
| `GOOGLE_CLIENT_SECRET` | No | `""` | Google OAuth 2.0 client secret |
|
||||||
|
|
||||||
|
Leave empty to disable Google sign-in. Redirect URI must be configured as `{BASE_URL}/auth/google/callback` in the Google Cloud Console.
|
||||||
|
|
||||||
|
### OAuth — Microsoft
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `MICROSOFT_CLIENT_ID` | No | `""` | Microsoft Azure AD application (client) ID |
|
||||||
|
| `MICROSOFT_CLIENT_SECRET` | No | `""` | Microsoft Azure AD client secret |
|
||||||
|
| `MICROSOFT_TENANT_ID` | No | `common` | Azure AD tenant ID. Use `common` for multi-tenant, or a specific tenant ID to restrict. |
|
||||||
|
|
||||||
|
Leave `MICROSOFT_CLIENT_ID` empty to disable Microsoft sign-in. Redirect URI: `{BASE_URL}/auth/microsoft/callback`.
|
||||||
|
|
||||||
|
### OAuth — Apple
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `APPLE_CLIENT_ID` | No | `""` | Apple Services ID (e.g. `com.example.tickets`) |
|
||||||
|
| `APPLE_TEAM_ID` | No | `""` | Apple Developer Team ID |
|
||||||
|
| `APPLE_KEY_ID` | No | `""` | Key ID for the Sign in with Apple private key |
|
||||||
|
| `APPLE_KEY_PATH` | No | `""` | File path to the `.p8` private key file |
|
||||||
|
|
||||||
|
Leave `APPLE_CLIENT_ID` empty to disable Apple sign-in. Redirect URI: `{BASE_URL}/auth/apple/callback`. Note that Apple uses `form_post` response mode.
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `TAILSCALE_ALLOWED_USERS` | No | `""` | Comma-separated list of Tailscale login names allowed to access the admin panel (e.g. `user@example.com,admin@example.com`). If empty, all requests are allowed (dev mode). |
|
||||||
|
|
||||||
|
See [Admin Guide](./admin-guide.md#authentication) for details on how Tailscale authentication works.
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
1. Copy the example environment file and edit it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start PostgreSQL (e.g. via Docker):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name postgres \
|
||||||
|
-e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=forgejo_tickets \
|
||||||
|
-p 5432:5432 postgres:16-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs `go run ./cmd/server`, which auto-migrates the database on startup.
|
||||||
|
|
||||||
|
4. For CSS development, run the Tailwind watcher in a separate terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make tailwind-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
The public UI is at `http://localhost:8080` and the admin panel at `http://localhost:8081`.
|
||||||
|
|
||||||
|
## Makefile Targets
|
||||||
|
|
||||||
|
| Target | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `build` | Compiles Tailwind CSS then builds the Go binary (`forgejo-tickets`) |
|
||||||
|
| `run` | Runs the server directly with `go run` (no prior build needed) |
|
||||||
|
| `test` | Runs all Go tests (`go test ./...`) |
|
||||||
|
| `tailwind` | Compiles Tailwind CSS from `web/static/css/input.css` to `web/static/css/output.css` (minified) |
|
||||||
|
| `tailwind-watch` | Runs Tailwind in watch mode for development |
|
||||||
|
| `docker` | Builds the Docker image tagged with the git SHA and `latest` |
|
||||||
|
| `docker-push` | Builds and pushes the Docker image to the registry |
|
||||||
|
| `clean` | Removes the compiled binary and generated CSS |
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
The Dockerfile uses a 3-stage build:
|
||||||
|
|
||||||
|
1. **Node/Tailwind stage** (`node:22-alpine`) — Compiles Tailwind CSS.
|
||||||
|
2. **Go build stage** (`golang:1.23-alpine`) — Downloads dependencies and compiles the Go binary with `CGO_ENABLED=0`.
|
||||||
|
3. **Runtime stage** (`alpine:3.20`) — Minimal image with just the binary, templates, and static assets. Exposes ports 8080 and 8081.
|
||||||
|
|
||||||
|
Build and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker
|
||||||
|
docker run --env-file .env -p 8080:8080 -p 8081:8081 registry.ts.mattnite.net/forgejo-tickets:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nomad
|
||||||
|
|
||||||
|
The production deployment uses Nomad with the job spec at `infra/tickets/tickets.hcl`.
|
||||||
|
|
||||||
|
### Job Structure
|
||||||
|
|
||||||
|
- **Job**: `tickets` (service type, datacenter `dc1`)
|
||||||
|
- **Group**: `tickets` (count 1)
|
||||||
|
- Two ports on the Tailscale host network: `http` (public) and `admin`
|
||||||
|
- **Task**: `server` (Docker driver)
|
||||||
|
|
||||||
|
### Environment Injection
|
||||||
|
|
||||||
|
Secrets are stored as Nomad variables at the path `nomad/jobs/tickets` and injected via a template block:
|
||||||
|
|
||||||
|
- `DATABASE_URL`, `SESSION_SECRET`
|
||||||
|
- `FORGEJO_URL`, `FORGEJO_API_TOKEN`
|
||||||
|
- `POSTMARK_SERVER_TOKEN`
|
||||||
|
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`
|
||||||
|
- `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`
|
||||||
|
- `APPLE_CLIENT_ID`, `APPLE_TEAM_ID`, `APPLE_KEY_ID`
|
||||||
|
|
||||||
|
Non-secret configuration is set directly in the `env` block:
|
||||||
|
|
||||||
|
- `PUBLIC_ADDR` and `ADMIN_ADDR` use Nomad's dynamic port variables
|
||||||
|
- `BASE_URL` is the public-facing URL
|
||||||
|
- `POSTMARK_FROM_EMAIL` is the sender address
|
||||||
|
- `TAILSCALE_ALLOWED_USERS` controls admin access
|
||||||
|
|
||||||
|
### Traefik Routing
|
||||||
|
|
||||||
|
Two Nomad services are registered with Traefik tags:
|
||||||
|
|
||||||
|
| Service | Host | Purpose |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `tickets` | `tickets.ts.mattnite.net` | Public UI (internal Traefik, TLS via Let's Encrypt) |
|
||||||
|
| `tickets-admin` | `tickets-admin.ts.mattnite.net` | Admin panel (internal Traefik, TLS via Let's Encrypt) |
|
||||||
|
|
||||||
|
Both use the `websecure` entrypoint with automatic TLS certificate resolution.
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
The public service registers an HTTP health check at `GET /health` with a 10-second interval and 30-second timeout.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Docker logging is configured to ship to Loki at `loki.ts.mattnite.net` with job name and allocation ID as labels.
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
- CPU: 200 MHz
|
||||||
|
- Memory: 256 MB
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
The application uses PostgreSQL with GORM as the ORM. On startup, `database.RunMigrations()` calls `models.AutoMigrate()` which:
|
||||||
|
|
||||||
|
1. Creates PostgreSQL enum types (`ticket_status`, `token_type`) if they don't exist
|
||||||
|
2. Auto-migrates all model tables (User, OAuthAccount, Session, Repo, Ticket, TicketComment, EmailToken)
|
||||||
|
3. Creates composite and partial unique indexes
|
||||||
|
|
||||||
|
No separate migration tool is needed — the schema is managed entirely through GORM auto-migration.
|
||||||
|
|
||||||
|
See [Configuration](./configuration.md) for all environment variables and [Forgejo Integration](./forgejo-integration.md) for webhook URL setup.
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Forgejo Integration
|
||||||
|
|
||||||
|
The application syncs support tickets with Forgejo issues, providing a bridge between the customer-facing UI and internal issue tracking.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Two environment variables are required for the integration:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `FORGEJO_URL` | Base URL of your Forgejo instance (e.g. `https://forgejo.example.com`) |
|
||||||
|
| `FORGEJO_API_TOKEN` | Personal access token with permission to create issues and comments |
|
||||||
|
|
||||||
|
See [Configuration](./configuration.md#forgejo) for the full reference.
|
||||||
|
|
||||||
|
## Ticket-to-Issue Sync
|
||||||
|
|
||||||
|
When a customer creates a ticket:
|
||||||
|
|
||||||
|
1. The ticket is saved to the database immediately.
|
||||||
|
2. A goroutine asynchronously calls the Forgejo API to create a corresponding issue:
|
||||||
|
- **Endpoint**: `POST /api/v1/repos/{owner}/{repo}/issues`
|
||||||
|
- **Title**: Same as the ticket title
|
||||||
|
- **Body**: Ticket description + a footer line identifying the submitting user's email
|
||||||
|
3. On success, the Forgejo issue number is stored on the ticket (`forgejo_issue_number`).
|
||||||
|
|
||||||
|
If the API call fails, the error is logged but the ticket remains valid. The issue number will be `NULL`, meaning no further syncing occurs for that ticket.
|
||||||
|
|
||||||
|
## Comment Sync
|
||||||
|
|
||||||
|
When a customer adds a comment to a ticket that has a linked Forgejo issue:
|
||||||
|
|
||||||
|
1. The comment is saved to the database immediately.
|
||||||
|
2. A goroutine asynchronously calls the Forgejo API to create a comment on the issue:
|
||||||
|
- **Endpoint**: `POST /api/v1/repos/{owner}/{repo}/issues/{number}/comments`
|
||||||
|
- **Body**: Comment text + a footer line identifying the commenting user's email
|
||||||
|
3. On success, the Forgejo comment ID is stored on the ticket comment (`forgejo_comment_id`).
|
||||||
|
|
||||||
|
Comments are only synced if `forgejo_issue_number` is set on the ticket.
|
||||||
|
|
||||||
|
## Webhook Setup
|
||||||
|
|
||||||
|
To receive events from Forgejo (e.g. issue closed), configure a webhook in each Forgejo repository:
|
||||||
|
|
||||||
|
### 1. Get the Webhook URL
|
||||||
|
|
||||||
|
The URL follows the pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
{BASE_URL}/webhooks/forgejo/{slug}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `{slug}` is the repo slug configured in the [admin panel](./admin-guide.md#repos). For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://tickets.example.com/webhooks/forgejo/my-product
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure in Forgejo
|
||||||
|
|
||||||
|
In the Forgejo repository settings, go to **Webhooks** and add a new webhook:
|
||||||
|
|
||||||
|
- **Target URL**: The webhook URL from step 1
|
||||||
|
- **HTTP Method**: POST
|
||||||
|
- **Content Type**: `application/json`
|
||||||
|
- **Secret**: Must match the webhook secret configured for this repo in the admin panel
|
||||||
|
- **Events**: Select "Issues" (or use "Custom Events" and check "Issues")
|
||||||
|
|
||||||
|
### 3. Signature Verification
|
||||||
|
|
||||||
|
All webhook requests are verified using HMAC-SHA256. The signature is read from the `X-Forgejo-Signature` header and compared against the computed HMAC of the request body using the repo's webhook secret.
|
||||||
|
|
||||||
|
Requests with missing or invalid signatures are rejected with `401 Unauthorized`.
|
||||||
|
|
||||||
|
## Auto-Close Flow
|
||||||
|
|
||||||
|
When an issue is closed in Forgejo, the following happens:
|
||||||
|
|
||||||
|
1. Forgejo sends a webhook POST to `{BASE_URL}/webhooks/forgejo/{slug}`.
|
||||||
|
2. The app verifies the HMAC-SHA256 signature against the repo's webhook secret.
|
||||||
|
3. The payload is parsed. Only `"action": "closed"` events are processed; all others return `200 OK` with no action.
|
||||||
|
4. The app looks up the ticket by `repo_id` and `forgejo_issue_number`.
|
||||||
|
5. If found, the ticket status is updated to `closed`.
|
||||||
|
6. A notification email is sent asynchronously to the ticket owner informing them their ticket has been resolved.
|
||||||
|
|
||||||
|
This enables the internal team to close issues directly in Forgejo and have the status reflected in the customer-facing UI automatically.
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
- All Forgejo API calls (issue creation, comment creation) are **asynchronous** — they run in goroutines and don't block the HTTP response.
|
||||||
|
- The API client uses a 30-second HTTP timeout.
|
||||||
|
- Authentication uses the `Authorization: token {api_token}` header.
|
||||||
|
- Webhook processing is **synchronous** — the ticket status update happens before returning the response to Forgejo.
|
||||||
|
|
||||||
|
See [Admin Guide](./admin-guide.md#repos) for managing repos through the admin UI and [Deployment](./deployment.md) for webhook URL configuration in production.
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# User Guide
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
### Email/Password
|
||||||
|
|
||||||
|
1. Navigate to `/register`.
|
||||||
|
2. Enter your name, email address, and a password (minimum 8 characters).
|
||||||
|
3. Confirm your password and submit.
|
||||||
|
4. Check your email for a verification link. The link expires in 24 hours.
|
||||||
|
5. Click the verification link to activate your account.
|
||||||
|
6. You can now log in at `/login`.
|
||||||
|
|
||||||
|
### OAuth (Social Login)
|
||||||
|
|
||||||
|
You can register and log in using a third-party provider if configured by the administrator:
|
||||||
|
|
||||||
|
- **Google** — Click "Sign in with Google" on the login page.
|
||||||
|
- **Microsoft** — Click "Sign in with Microsoft" on the login page.
|
||||||
|
- **Apple** — Click "Sign in with Apple" on the login page.
|
||||||
|
|
||||||
|
OAuth login automatically creates your account (if it doesn't exist) and marks your email as verified. If an account with your email already exists, the OAuth provider is linked to it.
|
||||||
|
|
||||||
|
## Login
|
||||||
|
|
||||||
|
Navigate to `/login` and enter your email and password, or use one of the OAuth providers. You must verify your email before logging in with a password.
|
||||||
|
|
||||||
|
After logging in, you are redirected to the ticket list.
|
||||||
|
|
||||||
|
## Logout
|
||||||
|
|
||||||
|
Click "Logout" in the navigation. This destroys your session and redirects you to the home page.
|
||||||
|
|
||||||
|
## Password Reset
|
||||||
|
|
||||||
|
1. Go to `/forgot-password`.
|
||||||
|
2. Enter your email address and submit.
|
||||||
|
3. If an account exists with that email, a reset link is sent. The link expires in 1 hour.
|
||||||
|
4. Click the reset link in the email.
|
||||||
|
5. Enter a new password (minimum 8 characters) and confirm it.
|
||||||
|
6. You can now log in with the new password.
|
||||||
|
|
||||||
|
The reset form displays a generic success message regardless of whether the email exists, to prevent account enumeration.
|
||||||
|
|
||||||
|
## Tickets
|
||||||
|
|
||||||
|
### Creating a Ticket
|
||||||
|
|
||||||
|
1. Navigate to `/tickets/new` (or click "New Ticket").
|
||||||
|
2. Select a product from the dropdown (these correspond to repos configured by the admin).
|
||||||
|
3. Enter a title and description.
|
||||||
|
4. Submit the form.
|
||||||
|
|
||||||
|
After creation, you are redirected to the ticket detail page. The ticket is also asynchronously created as an issue in the corresponding Forgejo repository. See [Forgejo Integration](./forgejo-integration.md) for how this sync works.
|
||||||
|
|
||||||
|
### Viewing Your Tickets
|
||||||
|
|
||||||
|
Navigate to `/tickets` to see all your tickets, ordered by most recent first (up to 50). Each ticket shows its title, associated product, status, and creation date.
|
||||||
|
|
||||||
|
### Ticket Detail
|
||||||
|
|
||||||
|
Click a ticket to see its full details including description, status, associated product, and the conversation thread. Only you can see your own tickets.
|
||||||
|
|
||||||
|
### Adding Comments
|
||||||
|
|
||||||
|
On the ticket detail page, type a comment in the text area and submit. Comments are displayed in chronological order. If the ticket is linked to a Forgejo issue, comments are also synced to the issue asynchronously.
|
||||||
|
|
||||||
|
### Ticket Statuses
|
||||||
|
|
||||||
|
- **Open** — Newly created, awaiting response.
|
||||||
|
- **In Progress** — Being worked on by the team.
|
||||||
|
- **Closed** — Resolved. You'll receive an email notification when a ticket is closed.
|
||||||
|
|
||||||
|
You cannot change ticket status yourself — it is managed by administrators or updated automatically when the linked Forgejo issue is closed.
|
||||||
|
|
||||||
|
## Email Notifications
|
||||||
|
|
||||||
|
You may receive the following emails:
|
||||||
|
|
||||||
|
| Email | When |
|
||||||
|
|-------|------|
|
||||||
|
| Verification | After registration — contains a link to verify your email |
|
||||||
|
| Password Reset | After requesting a password reset — contains a link to set a new password |
|
||||||
|
| Ticket Closed | When your ticket's status changes to closed (e.g. via Forgejo webhook) |
|
||||||
Loading…
Reference in New Issue