init push
This commit is contained in:
435
README.md
Normal file
435
README.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# Wucher Backend
|
||||
|
||||
Wucher is a Go backend built with GoFiber. The runtime uses MySQL for persistence and transient auth state, Amazon SQS for asynchronous email jobs, and Amazon SES v2 for email delivery.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.25+
|
||||
- MySQL
|
||||
- LocalStack or AWS SQS/SES access for queue testing
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `cmd/api/` API bootstrap
|
||||
- `cmd/worker/` SQS worker bootstrap
|
||||
- `internal/app/` application wiring
|
||||
- `internal/resilience/` circuit breaker executor and dependency error classifiers
|
||||
- `internal/service/` business logic
|
||||
- `internal/transport/http/` GoFiber handlers and middleware
|
||||
- `internal/repository/mysql/` MySQL repositories and transient stores
|
||||
- `internal/repository/sqs/` SQS publisher/consumer adapter
|
||||
- `internal/queue/` queue contracts, serializer, retry policy, and worker loop
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
- HTTP handlers stay thin and delegate to services.
|
||||
- Auth and other business writes persist to MySQL first.
|
||||
- API/service code never sends email synchronously.
|
||||
- Email jobs are written to `email_outbox_messages` when outbox is enabled, or published directly to SQS when outbox is disabled.
|
||||
- Worker publishes outbox rows to the SQS send queue, consumes SQS messages with long polling, and sends email through Amazon SES v2.
|
||||
- SES bounce, complaint, and delivery tracking should use a separate flow:
|
||||
- SES Configuration Set
|
||||
- SNS topic subscription
|
||||
- dedicated SQS event queue
|
||||
- Circuit breakers are applied only at outbound dependency boundaries:
|
||||
- Microsoft Entra token exchange
|
||||
- SES delivery
|
||||
- SQS publish/consume/ack calls
|
||||
- Transient auth state previously kept in Redis is now stored in MySQL tables:
|
||||
- `auth_runtime_tokens`
|
||||
- `queue_idempotency_records`
|
||||
|
||||
`gorm` auto-migration creates these tables on startup.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
Create local config from the example:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Run the API:
|
||||
|
||||
```bash
|
||||
go run ./cmd/api
|
||||
```
|
||||
|
||||
Run the worker:
|
||||
|
||||
```bash
|
||||
go run ./cmd/worker
|
||||
```
|
||||
|
||||
Worker monitor endpoints:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:9090/health
|
||||
curl http://127.0.0.1:9090/metrics
|
||||
curl http://127.0.0.1:9091/health
|
||||
curl http://127.0.0.1:9091/metrics
|
||||
```
|
||||
|
||||
- `/metrics` exposes Prometheus format for API and worker processes.
|
||||
- `/health` remains JSON health/worker snapshot.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Example values are in `.env` for local development.
|
||||
|
||||
Important groups:
|
||||
|
||||
- Email delivery
|
||||
- `EMAIL_PROVIDER`
|
||||
- `EMAIL_MOCK_DIR`
|
||||
- MySQL
|
||||
- `MYSQL_DSN`
|
||||
- `MYSQL_MAX_OPEN_CONNS`
|
||||
- `MYSQL_MAX_IDLE_CONNS`
|
||||
- `MYSQL_CONN_MAX_LIFETIME`
|
||||
- `MYSQL_CONN_MAX_IDLE_TIME`
|
||||
- Auth
|
||||
- `AUTH_EMAIL_VERIFY_URL`
|
||||
- `AUTH_PASSWORD_RESET_URL`
|
||||
- `AUTH_SECURITY_PIN_RESET_URL`
|
||||
- `AUTH_TOTP_CHALLENGE_TTL`
|
||||
- `AUTH_SSO_STATE_TTL`
|
||||
- `AUTH_JWT_ACCESS_SECRET`
|
||||
- `AUTH_JWT_REFRESH_SECRET`
|
||||
- `AUTH_WEBAUTHN_*`
|
||||
- Queue / worker
|
||||
- `QUEUE_MESSAGE_SCHEMA_VERSION`
|
||||
- `QUEUE_ENABLE_LEGACY_PAYLOAD_FALLBACK`
|
||||
- `QUEUE_IDEMPOTENCY_KEY_PREFIX`
|
||||
- `QUEUE_IDEMPOTENCY_TTL`
|
||||
- `QUEUE_PROCESSING_TTL`
|
||||
- `QUEUE_WORKER_CONCURRENCY`
|
||||
- `QUEUE_WORKER_MAX_BATCH_SIZE`
|
||||
- `QUEUE_WORKER_PROCESS_TIMEOUT`
|
||||
- `QUEUE_WORKER_BACKOFF_MIN`
|
||||
- `QUEUE_WORKER_BACKOFF_MAX`
|
||||
- `QUEUE_WORKER_PERMANENT_FAILURE_DELAY`
|
||||
- `QUEUE_OUTBOX_ENABLED`
|
||||
- `QUEUE_OUTBOX_POLL_INTERVAL`
|
||||
- `QUEUE_OUTBOX_BATCH_SIZE`
|
||||
- `QUEUE_OUTBOX_DISPATCH_TIMEOUT`
|
||||
- `QUEUE_OUTBOX_LOCK_TTL`
|
||||
- `QUEUE_OUTBOX_MAX_ATTEMPTS`
|
||||
- File queue / file worker
|
||||
- `FILE_QUEUE_MESSAGE_SCHEMA_VERSION`
|
||||
- `FILE_QUEUE_IDEMPOTENCY_KEY_PREFIX`
|
||||
- `FILE_QUEUE_IDEMPOTENCY_TTL`
|
||||
- `FILE_QUEUE_PROCESSING_TTL`
|
||||
- `FILE_QUEUE_WORKER_CONCURRENCY`
|
||||
- `FILE_QUEUE_WORKER_MAX_BATCH_SIZE`
|
||||
- `FILE_QUEUE_WORKER_PROCESS_TIMEOUT`
|
||||
- `FILE_QUEUE_WORKER_SHUTDOWN_TIMEOUT`
|
||||
- `FILE_QUEUE_WORKER_BACKOFF_MIN`
|
||||
- `FILE_QUEUE_WORKER_BACKOFF_MAX`
|
||||
- `FILE_QUEUE_WORKER_PERMANENT_FAILURE_DELAY`
|
||||
- `FILE_QUEUE_WORKER_MONITOR_ADDR`
|
||||
- `FILE_QUEUE_WORKER_HEALTH_ERROR_THRESHOLD`
|
||||
- `FILE_QUEUE_WORKER_DEPTH_POLL_INTERVAL`
|
||||
- `FILE_QUEUE_OUTBOX_ENABLED`
|
||||
- `FILE_QUEUE_OUTBOX_POLL_INTERVAL`
|
||||
- `FILE_QUEUE_OUTBOX_BATCH_SIZE`
|
||||
- `FILE_QUEUE_OUTBOX_DISPATCH_TIMEOUT`
|
||||
- `FILE_QUEUE_OUTBOX_LOCK_TTL`
|
||||
- `FILE_QUEUE_OUTBOX_MAX_ATTEMPTS`
|
||||
- Circuit breakers
|
||||
- `CB_DEFAULT_ENABLED`
|
||||
- `CB_DEFAULT_MAX_REQUESTS`
|
||||
- `CB_DEFAULT_INTERVAL`
|
||||
- `CB_DEFAULT_BUCKET_PERIOD`
|
||||
- `CB_DEFAULT_TIMEOUT`
|
||||
- `CB_DEFAULT_MIN_REQUESTS`
|
||||
- `CB_DEFAULT_FAILURE_RATIO`
|
||||
- `CB_DEFAULT_CONSECUTIVE_FAILURES`
|
||||
- `CB_MICROSOFT_SSO_ENABLED`
|
||||
- `CB_SES_ENABLED`
|
||||
- `CB_SQS_ENABLED`
|
||||
- AWS / SES
|
||||
- `AWS_REGION`
|
||||
- `SES_REGION`
|
||||
- `SES_FROM`
|
||||
- `SES_CONFIGURATION_SET`
|
||||
- `SES_SEND_TIMEOUT`
|
||||
- `SES_ENDPOINT`
|
||||
- `SES_ALLOW_INSECURE_ENDPOINT`
|
||||
- AWS / SQS
|
||||
- Email queue:
|
||||
- `AWS_REGION`
|
||||
- `AWS_ACCESS_KEY_ID`
|
||||
- `AWS_SECRET_ACCESS_KEY`
|
||||
- `SQS_ENDPOINT`
|
||||
- `SQS_ALLOW_INSECURE_ENDPOINT`
|
||||
- `SQS_QUEUE_URL`
|
||||
- `SQS_QUEUE_TYPE`
|
||||
- `SQS_MESSAGE_GROUP_ID`
|
||||
- `SQS_MAX_MESSAGES`
|
||||
- `SQS_WAIT_TIME_SECONDS`
|
||||
- `SQS_VISIBILITY_TIMEOUT`
|
||||
- File queue:
|
||||
- `FILE_SQS_REGION`
|
||||
- `FILE_SQS_ENDPOINT`
|
||||
- `FILE_SQS_QUEUE_URL`
|
||||
- `FILE_SQS_QUEUE_TYPE`
|
||||
- `FILE_SQS_MESSAGE_GROUP_ID`
|
||||
- `FILE_SQS_MAX_MESSAGES`
|
||||
- `FILE_SQS_WAIT_TIME_SECONDS`
|
||||
- `FILE_SQS_VISIBILITY_TIMEOUT`
|
||||
- `FILE_SQS_ALLOW_INSECURE_ENDPOINT`
|
||||
- File storage (S3)
|
||||
- `FILE_STORAGE_PROVIDER`
|
||||
- `FILE_S3_BUCKET`
|
||||
- `FILE_S3_REGION`
|
||||
- `FILE_S3_ENDPOINT`
|
||||
- `FILE_S3_FORCE_PATH_STYLE`
|
||||
- `FILE_S3_ALLOW_INSECURE_ENDPOINT`
|
||||
- `FILE_S3_AUTO_CREATE_BUCKET`
|
||||
- `FILE_OBJECT_KEY_PREFIX`
|
||||
- `FILE_S3_PRESIGN_PUT_TTL`
|
||||
- `FILE_S3_PRESIGN_GET_TTL`
|
||||
- WOPI / Collabora
|
||||
- `WOPI_TOKEN_SECRET`
|
||||
- `WOPI_PROOF_SECRET`
|
||||
- `WOPI_DISCOVERY_URL`
|
||||
- `WOPI_PUBLIC_BASE_URL`
|
||||
- `WOPI_EDIT_ACTION_URL`
|
||||
- `WOPI_TOKEN_TTL`
|
||||
- `WOPI_MAX_BODY_BYTES`
|
||||
- `WOPI_REQUIRE_PROOF`
|
||||
- Worker mode selector
|
||||
- `WORKER_JOB=email`
|
||||
- `WORKER_JOB=file_processing`
|
||||
|
||||
## WOPI / Collabora Hardening
|
||||
|
||||
- Keep WOPI endpoints behind the dedicated `/wopi` router and do not expose them through the session-authenticated app middleware chain.
|
||||
- Use HTTPS end to end in production.
|
||||
- Restrict network access to the Collabora host or private network allowlist.
|
||||
- Set Collabora `aliasgroup`/host configuration so it can only call back to this backend.
|
||||
- Keep `WOPI_TOKEN_SECRET` and `WOPI_PROOF_SECRET` in env or a secrets manager and rotate them periodically.
|
||||
- Scope IAM to the minimum S3 bucket/prefix needed by file storage.
|
||||
- Keep direct S3 URLs disabled for editor access; the backend should proxy file bytes.
|
||||
- Ensure backend logs capture open/save/exit-save events for auditability.
|
||||
- Verify `/hosting/discovery` reachability and S3 connectivity in health checks before enabling Collabora traffic.
|
||||
|
||||
Per-dependency overrides are supported with the same shape as the default block:
|
||||
|
||||
- `CB_MICROSOFT_SSO_*`
|
||||
- `CB_SES_*`
|
||||
- `CB_SQS_*`
|
||||
|
||||
Example: `CB_SES_TIMEOUT=45s` or `CB_SQS_CONSECUTIVE_FAILURES=3`.
|
||||
|
||||
## Circuit Breakers
|
||||
|
||||
- Breakers are scoped per outbound dependency, not per HTTP endpoint and not global middleware.
|
||||
- MySQL repositories and internal business logic are intentionally not wrapped.
|
||||
- Only dependency-health failures count toward opening the breaker.
|
||||
- Microsoft SSO: network errors, timeouts, HTTP `429`, and `5xx`
|
||||
- SES: network errors, throttling, and AWS server faults
|
||||
- SQS: network errors, throttling, and AWS server faults
|
||||
- Client/config/input errors are excluded so they do not open the breaker.
|
||||
- State transitions are logged through the standard application logger.
|
||||
|
||||
## Queue Behavior
|
||||
|
||||
- Long polling is enabled through `SQS_WAIT_TIME_SECONDS`.
|
||||
- Messages are deleted only after successful processing or explicit ack of duplicates.
|
||||
- Transient failures use `ChangeMessageVisibility` with backoff.
|
||||
- Permanent failures are left unacked so SQS redrive policy can move them to the DLQ.
|
||||
- The default producer schema is `v2`; legacy payload fallback remains available for older messages.
|
||||
- Configure DLQ on the queue itself in AWS/LocalStack. The application no longer publishes directly to a DLQ URL.
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
Scrape endpoints:
|
||||
|
||||
- API: `http://<api-host>:8080/metrics`
|
||||
- Email worker monitor: `http://<worker-host>:9090/metrics`
|
||||
- File worker monitor: `http://<worker-host>:9091/metrics`
|
||||
|
||||
Example Prometheus scrape config:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: "wucher-api"
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:8080"]
|
||||
- job_name: "wucher-email-worker"
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9090"]
|
||||
- job_name: "wucher-file-worker"
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9091"]
|
||||
```
|
||||
|
||||
Metrics and labels:
|
||||
|
||||
- `app_aws_requests_total{component,service,operation,result}`
|
||||
- `app_aws_request_duration_seconds{component,service,operation}`
|
||||
- `app_aws_errors_total{component,service,operation,error_class}` where `error_class` is `4xx`, `5xx`, or `other`
|
||||
- `app_aws_transferred_bytes_total{component,service,operation,direction}`
|
||||
- `app_queue_messages_received_total{component,queue_name}`
|
||||
- `app_queue_messages_deleted_total{component,queue_name}`
|
||||
- `app_queue_visibility_changes_total{component,queue_name,result}`
|
||||
- `app_queue_send_total{component,queue_name,result}`
|
||||
- `app_queue_retries_total{component,queue_name,result}`
|
||||
- `app_queue_message_bytes_total{component,queue_name,direction}`
|
||||
- `app_queue_depth{component,queue_name}`
|
||||
- `app_worker_jobs_processed_total{component,job_type,result}`
|
||||
- `app_worker_job_duration_seconds{component,job_type}`
|
||||
- `app_email_send_total{component,provider,result}`
|
||||
- `app_email_send_duration_seconds{component,provider}`
|
||||
- `app_file_upload_total{component,result}`
|
||||
- `app_file_delete_total{component,result}`
|
||||
- `app_file_head_total{component,result}`
|
||||
- `app_file_presign_download_total{component,result}`
|
||||
|
||||
PromQL examples:
|
||||
|
||||
- Error rate per AWS operation:
|
||||
|
||||
```promql
|
||||
sum by (component, service, operation) (rate(app_aws_requests_total{result="error"}[5m]))
|
||||
/
|
||||
clamp_min(sum by (component, service, operation) (rate(app_aws_requests_total[5m])), 1e-9)
|
||||
```
|
||||
|
||||
- P95 latency per AWS operation:
|
||||
|
||||
```promql
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum by (le, component, service, operation) (
|
||||
rate(app_aws_request_duration_seconds_bucket[5m])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
- Queue depth:
|
||||
|
||||
```promql
|
||||
max by (component, queue_name) (app_queue_depth)
|
||||
```
|
||||
|
||||
- Worker success/failure count:
|
||||
|
||||
```promql
|
||||
sum by (component, job_type, result) (increase(app_worker_jobs_processed_total[15m]))
|
||||
```
|
||||
|
||||
- SES send failure rate:
|
||||
|
||||
```promql
|
||||
sum by (component) (rate(app_email_send_total{provider="ses", result="error"}[5m]))
|
||||
/
|
||||
clamp_min(sum by (component) (rate(app_email_send_total{provider="ses"}[5m])), 1e-9)
|
||||
```
|
||||
|
||||
- S3 4xx/5xx error rate:
|
||||
|
||||
```promql
|
||||
sum by (operation, error_class) (rate(app_aws_errors_total{service="s3"}[5m]))
|
||||
```
|
||||
|
||||
- S3 GetObject transferred bytes (estimated from presign download flow):
|
||||
|
||||
```promql
|
||||
sum by (component) (rate(app_aws_transferred_bytes_total{service="s3",operation="getobject",direction="download"}[5m]))
|
||||
```
|
||||
|
||||
- SQS message bytes in/out:
|
||||
|
||||
```promql
|
||||
sum by (component, queue_name, direction) (rate(app_queue_message_bytes_total[5m]))
|
||||
```
|
||||
|
||||
- SQS retry/requeue count:
|
||||
|
||||
```promql
|
||||
sum by (component, queue_name, result) (increase(app_queue_retries_total[15m]))
|
||||
```
|
||||
|
||||
## File Manager
|
||||
|
||||
- Queue separation:
|
||||
- Email queue: `app-queue` (+ `app-queue-dlq`)
|
||||
- File queue: `app-file-queue` (+ `app-file-queue-dlq`)
|
||||
- `FILE_SQS_QUEUE_URL` must be different from `SQS_QUEUE_URL`.
|
||||
- API startup validates queue separation and refuses startup on misconfiguration.
|
||||
- Upload writes file metadata + file outbox durably; file queue publish is handled asynchronously by the file outbox dispatcher.
|
||||
|
||||
Local run checklist:
|
||||
|
||||
```bash
|
||||
# 1) Start localstack (queue + bucket bootstrap script will run)
|
||||
cd ../localstack && docker compose up -d
|
||||
|
||||
WORKER_JOB=file_processing go run ./cmd/worker
|
||||
```
|
||||
|
||||
## SES Delivery Notes
|
||||
|
||||
- `EMAIL_PROVIDER=mock` is the recommended local/dev default when you do not have real AWS SES access.
|
||||
- Mock delivery writes one JSON file per delivered email into `EMAIL_MOCK_DIR`.
|
||||
- `SES_FROM` must be a verified identity or domain in Amazon SES.
|
||||
- `SES_ENDPOINT` should be blank in production and only overridden for local/dev stacks such as LocalStack.
|
||||
- Do not point SES events to the main send queue. Use `SES_CONFIGURATION_SET` with SNS -> separate SQS event queue.
|
||||
- The codebase includes an SES event parser for SNS-wrapped notifications, but an event consumer is not started by default.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
Detailed migration notes are in [docs/queue-sqs-migration.md](docs/queue-sqs-migration.md).
|
||||
Data migration tracking wiki is in [docs/data-migration-wiki.md](docs/data-migration-wiki.md).
|
||||
|
||||
Main breaking changes:
|
||||
|
||||
- Redis runtime support has been removed from the codebase.
|
||||
- Redis env vars are obsolete and should be removed from deployment config.
|
||||
- SMTP runtime env/config has been removed in favor of SES.
|
||||
- Worker now relies on MySQL for queue idempotency and auth/runtime state persistence.
|
||||
- SQS must have a redrive policy and DLQ configured outside the application.
|
||||
|
||||
## CI / Build Selection
|
||||
|
||||
The pipeline always builds and pushes the `api` image on every push to `development` or `staging`.
|
||||
|
||||
To also build and push the worker or file-worker image, include the corresponding tag anywhere in the commit message:
|
||||
|
||||
| Tag | Image built | Docker target |
|
||||
|---|---|---|
|
||||
| `#PUSH-WORKER` | `mybitlab/wfm:<tag>-worker` | `worker` |
|
||||
| `#PUSH-FILE-WORKER` | `mybitlab/wfm:<tag>-file-worker` | `file-worker` |
|
||||
|
||||
Both tags can appear in the same commit message to build all three in one pipeline run:
|
||||
|
||||
```
|
||||
ci: deploy all services #PUSH-WORKER #PUSH-FILE-WORKER
|
||||
```
|
||||
|
||||
Omitting a tag skips that image entirely — it will not be built or pushed.
|
||||
|
||||
## Testing
|
||||
|
||||
Run the full test suite:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Generate coverage:
|
||||
|
||||
```bash
|
||||
go test ./... -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
```
|
||||
Reference in New Issue
Block a user