Summary
Replace the placeholder cmd/api-proxy/main.go with the full wiring that loads config, opens the SQLite DB, constructs the Jellyfin client, wraps it in an adapter that satisfies auth.JellyfinAuthenticator, builds the auth service, and serves the HTTP handler with graceful shutdown. Also flatten the empty db.DB wrapper struct.
Context
This is the first task that makes the binary actually do something — every prior PR built isolated packages with their own tests. Until this lands the binary still serves the original toy /healthz from cmd/api-proxy/main.go and ignores all the implemented auth / HTTP / DB code.
Two design notes:
- Flatten
internal/db: The type DB struct { *sql.DB } wrapper has no methods and no foreseeable second consumer beyond internal/auth (auth owns SQLite tables; catalog will never persist). Returning *sql.DB directly from Open removes a useless .DB field-access at every call site.
- Adapter direction:
internal/clients/jellyfin/adapter.go must import internal/auth (not the other way around). auth defines JellyfinAuthenticator as an interface and never imports jellyfin, so no cycle.
Scope
- Flatten
internal/db/db.go: delete the DB struct, change Open's signature to func Open(ctx context.Context, path string) (*sql.DB, error). Update test callers in internal/auth/ that pass d.DB to drop the field access.
- Add
internal/clients/jellyfin/adapter.go exporting AsAuthAdapter(*Client) auth.JellyfinAuthenticator. It must:
- Translate result types between the jellyfin and auth packages (
AuthResult ↔ auth.JFAuthResult, QuickConnectInitiation ↔ auth.JFQuickConnectInit).
- Translate sentinel errors:
jellyfin.ErrInvalidCredentials → auth.ErrInvalidCredentials, jellyfin.ErrUpstreamUnavailable → auth.ErrJellyfinUnavailable, jellyfin.ErrQuickConnectPending → auth.ErrQuickConnectPending.
- Include a compile-time assertion:
var _ auth.JellyfinAuthenticator = (*authAdapter)(nil).
- Rewrite
cmd/api-proxy/main.go:
- Load config via
config.LoadFromEnv(), exit non-zero on error.
- Open DB via
db.Open(ctx, cfg.DBPath), defer close.
- Build
jellyfin.New(cfg.JellyfinURL, cfg.JellyfinAPIKey) and wrap with jellyfin.AsAuthAdapter.
- Build
auth.NewService(auth.Options{DB: database, Jellyfin: adapter, SignKey: cfg.JWTSigningKey}).
- Build the HTTP handler via
apihttp.NewServer(authSvc, logger) (already returns http.Handler).
- Serve via
http.Server with ReadHeaderTimeout: 5 * time.Second.
- Graceful shutdown on SIGINT/SIGTERM via
signal.NotifyContext + http.Server.Shutdown.
- Delete
cmd/api-proxy/main_test.go.
Out of scope: Docker image, deploy workflow, host scripts, README.
Acceptance criteria
Notes
internal/http.NewServer already returns http.Handler. Plug it straight into http.Server.Handler — no wrapping needed.
- The package alias
apihttp is needed in main since internal/http would otherwise collide with net/http.
- The adapter file is the only place that bridges the two packages' error vocabularies; do not duplicate the translation in
main.go or anywhere else.
Summary
Replace the placeholder
cmd/api-proxy/main.gowith the full wiring that loads config, opens the SQLite DB, constructs the Jellyfin client, wraps it in an adapter that satisfiesauth.JellyfinAuthenticator, builds the auth service, and serves the HTTP handler with graceful shutdown. Also flatten the emptydb.DBwrapper struct.Context
This is the first task that makes the binary actually do something — every prior PR built isolated packages with their own tests. Until this lands the binary still serves the original toy
/healthzfromcmd/api-proxy/main.goand ignores all the implemented auth / HTTP / DB code.Two design notes:
internal/db: Thetype DB struct { *sql.DB }wrapper has no methods and no foreseeable second consumer beyondinternal/auth(auth owns SQLite tables; catalog will never persist). Returning*sql.DBdirectly fromOpenremoves a useless.DBfield-access at every call site.internal/clients/jellyfin/adapter.gomust importinternal/auth(not the other way around).authdefinesJellyfinAuthenticatoras an interface and never importsjellyfin, so no cycle.Scope
internal/db/db.go: delete theDBstruct, changeOpen's signature tofunc Open(ctx context.Context, path string) (*sql.DB, error). Update test callers ininternal/auth/that passd.DBto drop the field access.internal/clients/jellyfin/adapter.goexportingAsAuthAdapter(*Client) auth.JellyfinAuthenticator. It must:AuthResult↔auth.JFAuthResult,QuickConnectInitiation↔auth.JFQuickConnectInit).jellyfin.ErrInvalidCredentials→auth.ErrInvalidCredentials,jellyfin.ErrUpstreamUnavailable→auth.ErrJellyfinUnavailable,jellyfin.ErrQuickConnectPending→auth.ErrQuickConnectPending.var _ auth.JellyfinAuthenticator = (*authAdapter)(nil).cmd/api-proxy/main.go:config.LoadFromEnv(), exit non-zero on error.db.Open(ctx, cfg.DBPath), defer close.jellyfin.New(cfg.JellyfinURL, cfg.JellyfinAPIKey)and wrap withjellyfin.AsAuthAdapter.auth.NewService(auth.Options{DB: database, Jellyfin: adapter, SignKey: cfg.JWTSigningKey}).apihttp.NewServer(authSvc, logger)(already returnshttp.Handler).http.ServerwithReadHeaderTimeout: 5 * time.Second.signal.NotifyContext+http.Server.Shutdown.cmd/api-proxy/main_test.go.Out of scope: Docker image, deploy workflow, host scripts, README.
Acceptance criteria
grep -rn "db\.DB\b" --include="*.go" .returns no matches in non-test code.go build ./...succeeds with no import-cycle errors.go test -race -count=1 ./...passes.golangci-lint run ./...reports 0 issues.httptest.Serverreturning{AccessToken, User: {Id, Name}}for/Users/AuthenticateByName):POST /auth/loginreturns 200 with a JSON body containingaccess_token,refresh_token, anduser.cmd/api-proxy/main_test.gois deleted.Notes
internal/http.NewServeralready returnshttp.Handler. Plug it straight intohttp.Server.Handler— no wrapping needed.apihttpis needed in main sinceinternal/httpwould otherwise collide withnet/http.main.goor anywhere else.