Follow-up to PR #8 (JWT revocation + refresh), addressing two of the three in-scope follow-ups called out on PR #11:
swagger.yaml pre-dated /api/v1/auth/refresh and /api/v1/auth/logout — client generators couldn't pick them up.
Those handlers were covered by unit tests on the WalletAuth layer and by the make e2e-full Playwright spec, but had no HTTP-level unit tests — regressions at the mux/handler seam (wrong method, missing walletAuth, unregistered route) were invisible to go test ./backend/api/rest.
Changes
backend/api/rest/swagger.yaml
New POST /api/v1/auth/refresh entry under the Auth tag. Uses bearerAuth, returns the existing WalletAuthResponse on 200, 401 via components/responses/Unauthorized, 503 when auth storage or jwt_revocations (migration 0016) is missing. Description calls out that legacy tokens without a jti cannot be refreshed.
New POST /api/v1/auth/logout entry. Same auth requirement; returns {status: ok} on 200; 401 via Unauthorized; 503 when migration 0016 hasn't run. Description names the jwt_revocations table so ops can correlate 503s with the migration.
Both slot in between /auth/wallet and /auth/register so the tag block stays ordered.
backend/api/rest/auth_refresh_internal_test.go (new — 7 tests)
TestHandleAuthRefreshRejectsGet — GET returns 405 method_not_allowed.
TestHandleAuthRefreshReturns503WhenWalletAuthUnconfigured — walletAuth nil, POST with a Bearer header returns 503 rather than panicking.
TestHandleAuthLogoutRejectsGet — symmetric 405 on GET.
TestAuthRefreshRouteRegistered — exercises SetupRoutes and confirms POST /api/v1/auth/refresh and /api/v1/auth/logout are registered (not 404). Catches regressions where a future refactor drops the mux.HandleFunc entries.
TestAuthRefreshRequiresBearerToken + TestAuthLogoutRequiresBearerToken — a POST with no Authorization resolves to 401 or 503 (never 200 or 500).
decodeErrorBody helper extracts ErrorDetail from writeError's {"error":{"code":...,"message":...}} envelope so asserts match the actual wire format.
newServerNoWalletAuth builds a rest.Server with JWT_SECRET set to a 32-byte string so PR #3's fail-fast check is satisfied; nil db pool is fine because the tests don't touch any DB path.
Verification
cd backend && go vet ./... — clean.
cd backend && go test ./api/rest/ — pass.
cd backend && go test ./... — pass.
Out of scope
The live credential rotation follow-up (rotate the L@ker?s?$?2010 passwords in infra) needs database + SSH + deploy-pipeline access and belongs to the operator. docs/SECURITY.md (added in PR #3) lists every asset to rotate.
## Summary
Follow-up to [PR #8](https://gitea.d-bis.org/d-bis/explorer-monorepo/pulls/8) (JWT revocation + refresh), addressing two of the three in-scope follow-ups called out on [PR #11](https://gitea.d-bis.org/d-bis/explorer-monorepo/pulls/11):
1. `swagger.yaml` pre-dated `/api/v1/auth/refresh` and `/api/v1/auth/logout` — client generators couldn't pick them up.
2. Those handlers were covered by unit tests on the `WalletAuth` layer and by the `make e2e-full` Playwright spec, but had **no HTTP-level unit tests** — regressions at the mux/handler seam (wrong method, missing `walletAuth`, unregistered route) were invisible to `go test ./backend/api/rest`.
## Changes
### `backend/api/rest/swagger.yaml`
- New `POST /api/v1/auth/refresh` entry under the `Auth` tag. Uses `bearerAuth`, returns the existing `WalletAuthResponse` on 200, 401 via `components/responses/Unauthorized`, 503 when auth storage or `jwt_revocations` (migration 0016) is missing. Description calls out that legacy tokens without a `jti` cannot be refreshed.
- New `POST /api/v1/auth/logout` entry. Same auth requirement; returns `{status: ok}` on 200; 401 via `Unauthorized`; 503 when migration 0016 hasn't run. Description names the `jwt_revocations` table so ops can correlate 503s with the migration.
- Both slot in between `/auth/wallet` and `/auth/register` so the tag block stays ordered.
### `backend/api/rest/auth_refresh_internal_test.go` (new — 7 tests)
- `TestHandleAuthRefreshRejectsGet` — GET returns 405 `method_not_allowed`.
- `TestHandleAuthRefreshReturns503WhenWalletAuthUnconfigured` — `walletAuth` nil, POST with a Bearer header returns 503 rather than panicking.
- `TestHandleAuthLogoutRejectsGet` — symmetric 405 on GET.
- `TestHandleAuthLogoutReturns503WhenWalletAuthUnconfigured` — symmetric 503.
- `TestAuthRefreshRouteRegistered` — exercises `SetupRoutes` and confirms `POST /api/v1/auth/refresh` and `/api/v1/auth/logout` are registered (not 404). Catches regressions where a future refactor drops the `mux.HandleFunc` entries.
- `TestAuthRefreshRequiresBearerToken` + `TestAuthLogoutRequiresBearerToken` — a POST with no `Authorization` resolves to 401 or 503 (never 200 or 500).
- `decodeErrorBody` helper extracts `ErrorDetail` from `writeError`'s `{"error":{"code":...,"message":...}}` envelope so asserts match the actual wire format.
- `newServerNoWalletAuth` builds a `rest.Server` with `JWT_SECRET` set to a 32-byte string so PR #3's fail-fast check is satisfied; nil db pool is fine because the tests don't touch any DB path.
## Verification
- `cd backend && go vet ./...` — clean.
- `cd backend && go test ./api/rest/` — pass.
- `cd backend && go test ./...` — pass.
## Out of scope
The **live credential rotation** follow-up (rotate the `L@ker?s?$?2010` passwords in infra) needs database + SSH + deploy-pipeline access and belongs to the operator. `docs/SECURITY.md` (added in PR #3) lists every asset to rotate.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Follow-up to PR #8 (JWT revocation + refresh), addressing two of the three in-scope follow-ups called out on PR #11:
swagger.yamlpre-dated/api/v1/auth/refreshand/api/v1/auth/logout— client generators couldn't pick them up.WalletAuthlayer and by themake e2e-fullPlaywright spec, but had no HTTP-level unit tests — regressions at the mux/handler seam (wrong method, missingwalletAuth, unregistered route) were invisible togo test ./backend/api/rest.Changes
backend/api/rest/swagger.yamlPOST /api/v1/auth/refreshentry under theAuthtag. UsesbearerAuth, returns the existingWalletAuthResponseon 200, 401 viacomponents/responses/Unauthorized, 503 when auth storage orjwt_revocations(migration 0016) is missing. Description calls out that legacy tokens without ajticannot be refreshed.POST /api/v1/auth/logoutentry. Same auth requirement; returns{status: ok}on 200; 401 viaUnauthorized; 503 when migration 0016 hasn't run. Description names thejwt_revocationstable so ops can correlate 503s with the migration./auth/walletand/auth/registerso the tag block stays ordered.backend/api/rest/auth_refresh_internal_test.go(new — 7 tests)TestHandleAuthRefreshRejectsGet— GET returns 405method_not_allowed.TestHandleAuthRefreshReturns503WhenWalletAuthUnconfigured—walletAuthnil, POST with a Bearer header returns 503 rather than panicking.TestHandleAuthLogoutRejectsGet— symmetric 405 on GET.TestHandleAuthLogoutReturns503WhenWalletAuthUnconfigured— symmetric 503.TestAuthRefreshRouteRegistered— exercisesSetupRoutesand confirmsPOST /api/v1/auth/refreshand/api/v1/auth/logoutare registered (not 404). Catches regressions where a future refactor drops themux.HandleFuncentries.TestAuthRefreshRequiresBearerToken+TestAuthLogoutRequiresBearerToken— a POST with noAuthorizationresolves to 401 or 503 (never 200 or 500).decodeErrorBodyhelper extractsErrorDetailfromwriteError's{"error":{"code":...,"message":...}}envelope so asserts match the actual wire format.newServerNoWalletAuthbuilds arest.ServerwithJWT_SECRETset to a 32-byte string so PR #3's fail-fast check is satisfied; nil db pool is fine because the tests don't touch any DB path.Verification
cd backend && go vet ./...— clean.cd backend && go test ./api/rest/— pass.cd backend && go test ./...— pass.Out of scope
The live credential rotation follow-up (rotate the
L@ker?s?$?2010passwords in infra) needs database + SSH + deploy-pipeline access and belongs to the operator.docs/SECURITY.md(added in PR #3) lists every asset to rotate.