CORS
cors.go never set Access-Control-Allow-Credentials, so the
cookie-authenticated API was unreadable from any cross-origin frontend:
the server answered correctly and the browser blocked the page from
reading it. Set for allowlisted origins on both the preflight and the
actual response. Three tests added.
HTTP_COOKIE_SAMESITE (lax|none|strict, default lax) is new. CORS is only
half of what a cross-origin browser call needs; SameSite is judged on
registrable domain, so a frontend on an unrelated domain gets perfect CORS
headers and still no cookie. "none" is the only value that survives that,
and validate() refuses it without the Secure flag.
The "*" rejection now explains itself: browsers refuse Allow-Origin "*"
together with credentials, so it would break every authenticated call
rather than loosen anything.
Transactional endpoints (api-contract.md 12.1)
POST /api/v1/job-applications/{id}/hire
POST /api/v1/job-postings/{id}/assignments
Replaces two client-side loops that wrote several records with no
transaction and no rollback. Each is now one endpoint and one transaction,
built over repo.Repo so org scoping, derived columns, type casts and error
translation are not re-derived. Authorization reuses the existing policy
table rather than adding a parallel one: a workflow is exactly as
privileged as the writes it performs. 13 tests, including both rollback
paths.
Bug fix in the repository layer
repo.bindValue handled int64/int/float64/string but not int32, which is
what pgx returns for a PostgreSQL `int` column. Nothing previously read a
record and wrote one of its fields elsewhere, so it never surfaced; the
hire flow does exactly that and failed with "ai_score must be a number".
Both KindInt and KindFloat now accept the widths pgx actually produces.
Deployment
infrastructure/Dockerfile.api multi-stage, cross-compiling (BUILDPLATFORM
+ GOARCH) so linux/amd64 builds from arm64 are compiled rather than
emulated. Alpine runtime, non-root uid 10001, 22.1 MB. Ships api, seed,
setpassword and migrate, plus the migrations, so a Kubernetes
initContainer can apply the schema from the same image and tag as the
API. HEALTHCHECK keys on status code, not body, so a "degraded" instance
is not pulled from rotation during a migration window.
infrastructure/docker-compose.yml migrations run to completion before the
API starts. Assumes a managed PostgreSQL; the local-db overlay adds one
with TLS enabled so APP_ENV=production is met rather than dodged.
scripts/drop_public_tables.go the one-off used to clear an unrelated
schema from krowdb on 2026-08-24, kept for the record. Build-tagged
ignore and gated on CONFIRM_DROP=yes.
Verified against PostgreSQL: 16/16 new tests pass, and the image was built,
run and exercised end to end (login, CORS preflight, authenticated reads,
transaction rollback).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmQiGq73Uyfq7J4yR8Vxxw
192 lines
7.5 KiB
Makefile
192 lines
7.5 KiB
Makefile
# ============================================================================
|
|
# Krow backend — developer tasks
|
|
#
|
|
# Migration commands are thin wrappers over the golang-migrate CLI. The
|
|
# migration FILES are the source of truth for the schema; nothing here or in
|
|
# go-api/ ever issues DDL of its own.
|
|
#
|
|
# brew install golang-migrate # or see github.com/golang-migrate/migrate
|
|
# ============================================================================
|
|
|
|
SHELL := /bin/bash
|
|
.DEFAULT_GOAL := help
|
|
|
|
# Capture APP_ENV as the process environment supplied it, BEFORE .env is
|
|
# included. Make gives an included assignment precedence over the environment,
|
|
# so without this `APP_ENV=production make migrate-down` would silently read
|
|
# `development` out of .env and the guard below would not fire.
|
|
APP_ENV_FROM_ENVIRONMENT := $(APP_ENV)
|
|
|
|
ifneq (,$(wildcard .env))
|
|
include .env
|
|
export
|
|
endif
|
|
|
|
# The environment wins; .env is only the fallback. A command-line override
|
|
# (`make migrate-down APP_ENV=production`) outranks both, as Make intends.
|
|
ifneq (,$(APP_ENV_FROM_ENVIRONMENT))
|
|
APP_ENV := $(APP_ENV_FROM_ENVIRONMENT)
|
|
endif
|
|
|
|
MIGRATIONS_DIR ?= ./migrations
|
|
DATABASE_SCHEMA ?= public
|
|
DATABASE_SSLMODE ?= disable
|
|
|
|
# URL-encode the credentials and the database name. "Krow-force" is mixed-case
|
|
# and hyphenated, and a password can contain anything; neither is safe raw.
|
|
enc = $(shell printf '%s' '$(1)' | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read(), safe=""))')
|
|
|
|
DB_URL = postgres://$(call enc,$(DATABASE_USER)):$(call enc,$(DATABASE_PASSWORD))@$(DATABASE_HOST):$(DATABASE_PORT)/$(call enc,$(DATABASE_NAME))?sslmode=$(DATABASE_SSLMODE)&search_path=$(DATABASE_SCHEMA)
|
|
MIGRATE = migrate -path $(MIGRATIONS_DIR) -database "$(DB_URL)"
|
|
|
|
PSQL = psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d "$(DATABASE_NAME)"
|
|
|
|
.PHONY: help
|
|
help: ## Show this help
|
|
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
|
|
|
# ── Go ──────────────────────────────────────────────────────────────────────
|
|
|
|
.PHONY: run
|
|
run: ## Run the API against the local database
|
|
cd go-api && go run ./cmd/api
|
|
|
|
.PHONY: build
|
|
build: ## Compile the API to go-api/bin/api
|
|
cd go-api && go build -o bin/api ./cmd/api
|
|
|
|
.PHONY: tidy
|
|
tidy: ## go mod tidy
|
|
cd go-api && go mod tidy
|
|
|
|
.PHONY: fmt
|
|
fmt: ## gofmt the module
|
|
cd go-api && gofmt -w ./cmd ./internal
|
|
|
|
.PHONY: vet
|
|
vet: ## go vet the module
|
|
cd go-api && go vet ./...
|
|
|
|
.PHONY: test
|
|
test: ## Run the Go tests
|
|
cd go-api && go test ./...
|
|
|
|
.PHONY: check
|
|
check: fmt vet test ## Format, vet and test
|
|
|
|
# ── Database ────────────────────────────────────────────────────────────────
|
|
|
|
.PHONY: db-create
|
|
db-create: ## Create the database if it does not exist (never drops anything)
|
|
@if psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d postgres -tAc \
|
|
"SELECT 1 FROM pg_database WHERE datname = '$(DATABASE_NAME)'" | grep -q 1; then \
|
|
echo "database \"$(DATABASE_NAME)\" already exists — nothing to do"; \
|
|
else \
|
|
psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d postgres -c \
|
|
'CREATE DATABASE "$(DATABASE_NAME)"' && echo "created \"$(DATABASE_NAME)\""; \
|
|
fi
|
|
|
|
.PHONY: seed
|
|
seed: ## Load the frontend demo dataset (idempotent upsert in one transaction)
|
|
cd go-api && SEED_FIXTURE_PATH=$(CURDIR)/seed/fixtures/seed.json go run ./cmd/seed
|
|
|
|
.PHONY: gen-resources
|
|
gen-resources: ## Regenerate domain descriptors from the live schema
|
|
python3 scripts/gen_resources.py > go-api/internal/domain/resources_gen.go
|
|
cd go-api && gofmt -w ./internal/domain
|
|
|
|
.PHONY: db-health
|
|
db-health: ## Ask the running API for its database health
|
|
@curl -fsS http://$(HTTP_HOST):$(HTTP_PORT)/health | python3 -m json.tool
|
|
|
|
# ── Migrations ──────────────────────────────────────────────────────────────
|
|
|
|
.PHONY: migrate-up
|
|
migrate-up: ## Apply all pending migrations
|
|
$(MIGRATE) up
|
|
|
|
.PHONY: migrate-down
|
|
migrate-down: ## Roll back exactly one migration (guarded outside development)
|
|
@if [ "$(APP_ENV)" != "development" ]; then \
|
|
echo "refusing: migrate-down is only permitted when APP_ENV=development (got '$(APP_ENV)')"; \
|
|
echo "roll back staging/production deliberately, with a reviewed plan and a fresh backup."; \
|
|
exit 1; \
|
|
fi
|
|
$(MIGRATE) down 1
|
|
|
|
.PHONY: migrate-status
|
|
migrate-status: ## Show the currently applied migration version
|
|
@$(MIGRATE) version
|
|
|
|
.PHONY: migrate-new
|
|
migrate-new: ## Create a new migration pair: make migrate-new NAME=add_widgets
|
|
@test -n "$(NAME)" || { echo "usage: make migrate-new NAME=add_widgets"; exit 1; }
|
|
migrate create -ext sql -dir $(MIGRATIONS_DIR) -seq $(NAME)
|
|
|
|
.PHONY: migrate-force
|
|
migrate-force: ## Clear a dirty state: make migrate-force VERSION=1
|
|
@test -n "$(VERSION)" || { echo "usage: make migrate-force VERSION=1"; exit 1; }
|
|
$(MIGRATE) force $(VERSION)
|
|
|
|
# There is deliberately no `migrate-drop` target. golang-migrate's `drop`
|
|
# command deletes every table in the database; it must never be one keystroke
|
|
# away from a developer who meant `down`. Run it by hand if you truly need it.
|
|
|
|
.PHONY: verify-schema
|
|
verify-schema: ## Print the tables, enums, FKs and indexes that exist in the target schema
|
|
@$(PSQL) -f scripts/verify_schema.sql
|
|
|
|
# ── Docker ──────────────────────────────────────────────────────────────────
|
|
#
|
|
# These read infrastructure/.env, NOT the repository-root .env that `make run`
|
|
# uses. Two files on purpose: the root one points at a developer's database
|
|
# from the host, the infrastructure one points at a deployment's database from
|
|
# inside a container, and DATABASE_HOST is different in each.
|
|
|
|
COMPOSE_DIR := infrastructure
|
|
COMPOSE_FILES := -f docker-compose.yml
|
|
# Layer the local database in with: make docker-up LOCAL_DB=1
|
|
ifdef LOCAL_DB
|
|
COMPOSE_FILES += -f docker-compose.local-db.yml
|
|
endif
|
|
COMPOSE = cd $(COMPOSE_DIR) && docker compose $(COMPOSE_FILES)
|
|
|
|
.PHONY: docker-build
|
|
docker-build: ## Build the API image
|
|
$(COMPOSE) build
|
|
|
|
.PHONY: docker-up
|
|
docker-up: ## Start the stack (migrations run first). LOCAL_DB=1 adds PostgreSQL
|
|
$(COMPOSE) up -d --build
|
|
@echo "api: http://127.0.0.1:8080/health"
|
|
@echo "logs: make docker-logs"
|
|
|
|
.PHONY: docker-down
|
|
docker-down: ## Stop the stack, keeping volumes
|
|
$(COMPOSE) down
|
|
|
|
.PHONY: docker-logs
|
|
docker-logs: ## Follow the API logs
|
|
$(COMPOSE) logs -f api
|
|
|
|
.PHONY: docker-ps
|
|
docker-ps: ## Show stack status and health
|
|
$(COMPOSE) ps
|
|
|
|
.PHONY: docker-migrate
|
|
docker-migrate: ## Apply migrations only, without starting the API
|
|
$(COMPOSE) run --rm migrate
|
|
|
|
.PHONY: docker-seed
|
|
docker-seed: ## Load the demo fixture into the deployment's database
|
|
$(COMPOSE) run --rm --entrypoint /usr/local/bin/seed api
|
|
|
|
.PHONY: docker-setpassword
|
|
docker-setpassword: ## Set a password: make docker-setpassword EMAIL=demo@krow.app
|
|
@test -n "$(EMAIL)" || { echo "usage: make docker-setpassword EMAIL=demo@krow.app"; exit 1; }
|
|
$(COMPOSE) run --rm -it --entrypoint /usr/local/bin/setpassword api -email $(EMAIL)
|
|
|
|
.PHONY: docker-config
|
|
docker-config: ## Render the fully resolved compose configuration
|
|
$(COMPOSE) config
|