# Go mkiso Server — Implementation Checklist

## Stage 1 — Plain MVP (no auth)

Goal: Functional replacement for all three PHP scripts. No security — plain GET endpoints.

- [ ] 1. **Project scaffolding**
  - Create `go.mod` (`module mkiso-server`, go 1.22+)
  - Create directory structure (`internal/{config,route,handler,service,repo}`, `pkg/dicom`)
  - Create `config.example.yaml` with all documented keys (no secrets)
  - Create `.gitignore` (ignore `config.yaml`, `*.iso`, temp dirs)

- [ ] 2. **Config loading** — `internal/config/config.go`
  - Define `Config` struct matching `docs/go-mkiso-design.md` § config.yaml
  - Load from YAML (`gopkg.in/yaml.v3`) or JSON (`encoding/json` for zero deps)
  - Validate required fields on load (pacs host, dcmtk paths)
  - ⚠️ Skip `auth:` section for Stage 1

- [ ] 3. **DICOM command runner** — `pkg/dicom/command.go`
  - `StartStoresCP`, `StopStoresCP`, `RunMoveSCU`, `RunStoreSCU`, `RunGenISOImage`
  - Port allocation: `allocatePort(basePort, portRange)` — mutex-guarded map
  - All functions return `(exitCode int, stdout, stderr string, error)`
  - Logging with slog

- [ ] 4. **Patient repo** — `internal/repo/patient.go`
  - HTTP client to patient-data API (see `docs/patient-api-spec.md`)
  - `ByAccessionNumber(ctx, accessionNumber) → (*PatientData, error)`
  - Configurable auth header from config (for when patient API itself needs auth)
  - Timeout + retry with backoff

- [ ] 5. **DICOM service** — `internal/service/dicom.go`
  - `FetchDICOM(ctx, accessionNumber, destDir) → (filesCount int, error)`
  - `FetchDICOMMultiple(ctx, accessionNumbers []string, destDir) → (totalFiles int, error)`
  - storescp lifecycle: start → wait ready → movescu → stop
  - `defer` for guaranteed storescp cleanup
  - ⚠️ After FetchDICOM, check `filesCount > 0`. If 0, return error `"no DICOM data"` before attempting ISO/genisoimage.

- [ ] 6. **ISO service** — `internal/service/iso.go`
  - `GenerateISO(ctx, accessionNumber) → (isoPath string, cleanup func(), error)`
  - `GenerateISOMultiple(ctx, accessionNumbers) → (isoPath string, cleanup func(), error)`
  - Temp dir + microdicom copy + FetchDICOM + genisoimage
  - Patient API for multi-accession filename

- [ ] 7. **Relay service** — `internal/service/relay.go` (see `todo/go-print-relay.detail.md`)
  - `RelayToCDPublisher(ctx, accessionNumbers) → (*RelayResult, error)`
  - Patient API validation + FetchDICOM + storescu to CD Publisher
  - Response includes `patient_name`, `destination`, `files_sent`

- [ ] 8. **Handlers** — `internal/handler/{health,iso,print}.go`
  - `GET /api/health` — health check + dependency status
  - `GET /api/iso/download?accession_number=X` — single ISO download
  - `GET /api/iso/download-multiple?accession_numbers=X,Y,Z` — multi ISO download
  - `GET /api/iso/print?accession_number=X` — DICOM relay (auto-detects comma → multi)
  - ⚠️ Print uses ONE endpoint for both single and multi: comma in `accession_number` triggers multi mode, no `print-multiple` endpoint needed (see pre-flight gap #3)

- [ ] 9. **Route wiring** — `internal/route/route.go`
  - Go 1.22+ `http.NewServeMux` with method+path patterns
  - Request ID middleware (UUID, `X-Request-ID` header)
  - Recovery middleware (catch panics, log stack, return 500)
  - **Stage 1**: 4 endpoints — health, download, download-multiple, print (single endpoint, auto-detects commas)
  - **No auth middleware in Stage 1**

- [ ] 10. **Main entrypoint** — `main.go`
  - Load config, wire dependencies, start server
  - Graceful shutdown (SIGINT/SIGTERM)
  - `slog.Info("server started", "port", cfg.Server.Port)`

- [ ] 11. **Stage 1 testing**
  - Test all four endpoints with curl (no auth header needed)
  - Test concurrent requests
  - Test error cases
  - Test through nginx reverse proxy

---

## Stage 2 — Add Authentication

Goal: Lock down endpoints. **Suggestion below**, implementor picks approach.

### Recommended: API Key via Reverse Proxy Injection

**Why**: Zero changes to the HIS `pacs_downloadiso` module. The existing `window.open()` calls continue to work because nginx injects the auth header before proxying to Go.

```
Browser (HIS)                    nginx (PACS server)              Go Server
────────────                     ───────────────────              ─────────
window.open("/mkiso.php?..")     
  ──────────── GET ───────────→  add X-API-Key header
                                  ───── GET + key ──────→  validate key → 200 OK
                                  ←─── ISO stream ───────
  ←── ISO download ────────────
```

**nginx config:**
```nginx
location ~ ^/(mkiso|mkiso_multiple|send_rimage_multiple)\.php$ {
    proxy_set_header X-API-Key "${MKISO_API_KEY}";
    proxy_pass http://127.0.0.1:8080/api/iso/$1$is_args$args;
}

# Direct API access (for testing/other clients)
location /api/ {
    proxy_pass http://127.0.0.1:8080;
}
```

**Go middleware (simple):**
```go
func APIKey(expectedKey string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Header.Get("X-API-Key") != expectedKey {
                http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}
```

**Pros:**
- Zero HIS code changes — nginx handles it
- Simple to implement (one string comparison in middleware)
- No token expiry, no login endpoint needed
- Key rotation = update config.yaml + reload nginx

**Cons:**
- Single shared key (no user-level audit)
- Key visible in nginx config
- No token expiry (must rotate manually)

### Alternative Suggestions

| Approach | Complexity | HIS Changes | Best For |
|----------|-----------|-------------|----------|
| **API Key (recommended)** | Low | None | Internal hospital network, nginx proxy in place |
| **Hardcoded JWT** | Low | None (nginx injects) | When you want expiry + claims but no login flow |
| **JWT + Login endpoint** | Medium | Yes (or client script) | Multi-client, need user-level audit |
| **IP whitelist** | Low | None | Fixed HIS server IP, simplest possible |
| **mTLS** | High | Yes (client cert) | Strictest security, external-facing |

### Stage 2 Todo Items

- [ ] 12. **Config additions** — `internal/config/config.go`
  - Add `auth.api_key` field (the expected key)
  - Add `auth.enabled` boolean (false in Stage 1 config, true in Stage 2)

- [ ] 13. **API Key middleware** — `internal/middleware/auth.go`
  - Extract `X-API-Key` from request header
  - Compare against config value
  - Return 401 on mismatch
  - Skip health endpoint (`/api/health` stays public)

- [ ] 14. **Route wiring update** — `internal/route/route.go`
  - Apply API key middleware to all `/api/iso/*` routes (download, download-multiple, print)
  - Keep `/api/health` public

- [ ] 15. **nginx config** — deploy reverse proxy with header injection
  - Map old PHP paths to new Go API paths
  - Inject `X-API-Key` header
  - Verify HIS `window.open()` calls work end-to-end

- [ ] 16. **Stage 2 testing**
  - Test with valid API key → 200
  - Test with invalid API key → 401
  - Test with missing API key → 401
  - Test health endpoint still public
  - Test through nginx proxy (key injected) → 200
  - Test direct access without nginx → 401

---

## Stage 3 (Future)

- [ ] JWT login endpoint (if multi-user audit needed)
- [ ] Rate limiting
- [ ] Metrics / Prometheus endpoint
- [ ] Docker image
- [ ] Remove Java/PHP after Go verified in production

---

## Dependencies (go.mod)

| Package | Purpose |
|---------|---------|
| `gopkg.in/yaml.v3` | YAML config parsing *(only external dep)* |

Everything else: **Go stdlib only**.
