Feature: Protected article routes and the offline-inclusive listing require a verified account As the maintainer of RenewCMS I want every protected article route, and the "see offline articles too" behavior of GET /v1/article/, to require an account that has completed email verification, not merely a valid jwt session So that a user who registered but never confirmed their email cannot create, read, edit, publish, unpublish, delete or attach images to articles, and cannot see unpublished drafts through the listing endpoint, while a verified user's existing workflow is unchanged Background: Given before this slice, the protected group inside NewArticleRouter (api/router.go) applies only jwtauth.Verifier then jwtauth.Authenticator to GET /v1/article/{id}, POST /v1/article/, DELETE /v1/article/{id}, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, so any caller with a validly signed, unexpired jwt cookie reaches the handler regardless of that account's is_verified status And this slice adds a third middleware, api/middleware.RequireVerifiedUser(getUser *useCases.GetUserUseCase), mounted after jwtauth.Authenticator in that same group, so the order becomes Verifier, Authenticator, RequireVerifiedUser for all six routes above And RequireVerifiedUser reads user_id from the request's jwt claims using a new exported helper, api/pkg/claims.UserId(r *http.Request) (uint32, bool), which replaces the identical private userIdFromClaims method api/handlers/auth.Handler.Verify and CancelRegistration already used (features/account_verification.feature, features/cancel_pending_registration.feature); those two handlers are updated to call the shared helper, and their observable 401 behavior on a missing or non-numeric user_id claim is unchanged And claims.UserId now also rejects a "user_id" claim whose JSON number is not a whole value in the inclusive range 0 to 4294967295 (math.MaxUint32): a negative number, a value with a fractional part (e.g. 7.5), or a value larger than 4294967295 all now return ok=false instead of being silently truncated or wrapped into a uint32 that could coincidentally match a different, real user id And when claims.UserId reports no usable user_id, RequireVerifiedUser answers 401 with apperr.UnauthorizedErrorMessage without calling GetUserUseCase, the same body and status the protected auth routes already use for the same condition And otherwise RequireVerifiedUser calls GetUserUseCase.GetUser(userId); when that returns user.ErrUserNotFound (a token minted for an account since deleted), it answers 401 with apperr.UnauthorizedErrorMessage; when it returns any other error, it answers 500 with body "Internal server error"; when it returns a user whose IsVerified is false, it answers 403 with a new apperr.VerificationRequiredErrorMessage constant, "You must verify your account to access this resource."; when IsVerified is true, it calls next.ServeHTTP unchanged, so the wrapped handler runs exactly as it did before this slice And this slice deletes the dead api/handlers/auth.Handler.IsVerified method: it was never wired to any route, and it performed the bare token.PrivateClaims()["user_id"].(float64) assertion this slice avoids everywhere else; RequireVerifiedUser is the behavior it was meant to provide, so it is removed rather than kept unused And this slice also changes GET /v1/article/, which sits under jwtauth.Verifier only (no Authenticator, per features/public_article_listing.feature) and therefore is never rejected outright: article.Handler gains a GetUserUseCase field, wired from container.GetUserUseCase in api/router.go's NewArticleRouter, and Handler.ListArticles now calls ListAllArticles only when the request carries a resolvable user_id (via the same claims.UserId helper) whose GetUserUseCase.GetUser lookup succeeds and returns IsVerified true; any other case (no jwt, malformed jwt, unresolvable user_id, deleted account, or an account that exists but is not yet verified) falls back to ListOnlineArticles, the same fallback features/public_article_listing.feature already documents for a malformed cookie; this listing route never answers 401 or 403 for an unverified caller, it just narrows the result And a repository failure while ListArticles resolves the caller (GetUserUseCase.GetUser returning an error other than user.ErrUserNotFound) is treated the same as "not verified" for the purpose of choosing which list to show, so the listing still answers 200 with the online-only array rather than failing the request; only a failure inside ListOnlineArticles or ListAllArticles itself still answers 500, unchanged from features/public_article_listing.feature And a later slice, features/public_article_read.feature, moves GET /v1/article/{id} out of the Verifier + Authenticator + RequireVerifiedUser group entirely and into the Verifier-only group next to GET /v1/article/, so that route no longer answers 401 for a missing cookie or 403 for an unverified caller; the "GET /v1/article/1" row is therefore removed from both Examples tables below Scenario Outline: An authenticated but unverified user is refused on every protected article route Given a user repository containing user id 1, is_verified false And a valid jwt cookie for user id 1 And the article repository contains an article with id 1 titled "Backyard Renovation Update" When the client sends with that cookie Then the response status is 403 And the response body is the apperr.VerificationRequiredErrorMessage text Examples: | method | path | | POST | /v1/article/ | | DELETE | /v1/article/1 | | PATCH | /v1/article/1 | | PATCH | /v1/article/1/publish | | PATCH | /v1/article/1/unpublish | Scenario: A verified user still exercises the protected routes exactly as before Given a user repository containing user id 1, is_verified true And a valid jwt cookie for user id 1 When the client sends POST /v1/article/ with that cookie and body {"title": "Valid Title", "body": "Some content"} Then the response status is 201 And the response body is the created article as JSON Scenario Outline: A token that resolves to no usable account still answers 401, never 403 Given a user repository containing user id 1, is_verified false And And the article repository contains an article with id 1 titled "Backyard Renovation Update" When the client sends POST /v1/article/ with that cookie and body {"title": "Valid Title", "body": "Some content"} Then the response status is 401 And the response body is the apperr.UnauthorizedErrorMessage text Examples: | token description | | a validly signed jwt cookie whose claims carry no "user_id" key at all | | a validly signed jwt cookie whose "user_id" claim is the string "one" | | a validly signed jwt cookie whose "user_id" claim is the number 999, which does not match any user | | a validly signed jwt cookie whose "user_id" claim is the number -1 | | a validly signed jwt cookie whose "user_id" claim is the number 4294967296 (one past the uint32 max) | Scenario: A fractional user_id claim is never truncated into impersonating a different real user Given a user repository containing user id 7, is_verified true And a validly signed jwt cookie whose claims are {"user_id": 7.5} When the client sends POST /v1/article/ with that cookie and body {"title": "Valid Title", "body": "Some content"} Then the response status is 401 And the response body is the apperr.UnauthorizedErrorMessage text And no article was created Scenario Outline: A request with no jwt cookie is still refused with 401, never 403 Given no jwt cookie is sent And the article repository contains an article with id 1 titled "Backyard Renovation Update" When the client sends without a cookie Then the response status is 401 Examples: | method | path | | POST | /v1/article/ | Scenario: A repository failure while resolving the caller's verification status answers 500 Given a valid jwt cookie for user id 1 And the user repository fails on its next call to Get with an error other than user.ErrUserNotFound And the article repository contains an article with id 1 titled "Backyard Renovation Update" When the client sends DELETE /v1/article/1 with that cookie Then the response status is 500 And the response body is the text "Internal server error" And the article repository's article with id 1 still exists Scenario: Listing falls back to the online-only list for an authenticated but unverified caller Given a user repository containing user id 1, is_verified false And a valid jwt cookie for user id 1 And the article repository contains an online article titled "Backyard Renovation Update" with id 1 And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2 When the client sends GET /v1/article/ with that cookie Then the response status is 200 And the response body is a JSON array containing exactly the article with id 1 And the article with id 2 is absent from the response body Scenario: Listing still returns every article for an authenticated and verified caller Given a user repository containing user id 1, is_verified true And a valid jwt cookie for user id 1 And the article repository contains an online article titled "Backyard Renovation Update" with id 1 And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2 When the client sends GET /v1/article/ with that cookie Then the response status is 200 And the response body is a JSON array containing both the article with id 1 and the article with id 2 Scenario: Listing falls back to the online-only list when the caller's account no longer exists Given the user repository contains no user with id 999 And a valid jwt cookie for user id 999 And the article repository contains an online article titled "Backyard Renovation Update" with id 1 And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2 When the client sends GET /v1/article/ with that cookie Then the response status is 200 And the response body is a JSON array containing exactly the article with id 1 Scenario: Listing falls back to the online-only list rather than failing when the user lookup errors Given a valid jwt cookie for user id 1 And the user repository fails on its next call to Get with an error other than user.ErrUserNotFound And the article repository contains an online article titled "Backyard Renovation Update" with id 1 And the same repository contains an offline article titled "Draft: Kitchen Remodel" with id 2 When the client sends GET /v1/article/ with that cookie Then the response status is 200 And the response body is a JSON array containing exactly the article with id 1