Skip to content

Feat: ban users - #74

Draft
khanzadimahdi wants to merge 1 commit into
mainfrom
feat/block-users
Draft

Feat: ban users#74
khanzadimahdi wants to merge 1 commit into
mainfrom
feat/block-users

Conversation

@khanzadimahdi

Copy link
Copy Markdown
Member

An administrator can suspend an account from the dashboard, and the ban takes effect at once — including for access tokens that were handed out before it.

The model

user.BannedAt records when the ban started. IsBanned() and SetBanned(bool) derive from it, and re-saving an already banned user does not move the date. In Mongo it is a nullable timestamp rather than a zero time, so lifting a ban writes an explicit null instead of leaving the old date behind.

Where a ban is enforced

Three places, because each one is a different way back in:

  1. The Authenticate middleware turns away every authenticated request. This is the one that makes a ban immediate — an access token issued a minute before the ban still fails.
  2. Login refuses after the password check, so a wrong password can't be used to probe which accounts are banned. No tokens are issued either way.
  3. Refresh refuses too. The refresh token outlives the access token by days, so a ban applied in between would otherwise let the session renew itself indefinitely.

All three answer 403 with the same body shape:

{"code": "user_banned", "message": ""}

403 rather than 401: the credentials are fine, the account is not, and retrying with a fresh token would change nothing. The code field is what lets a client tell a ban apart from an ordinary permission denial — both are 403s, but a permission denial is a dead end for one action while a ban ends the session.

Notes for review

  • The middleware translates its own message, because it runs before the Localize middleware. It falls back through the explicitly requested language → the user's own LanguageCode → the translator's default. This is why NewAuthenticateMiddleware now takes a translator — that's the one-argument change across all 53 call sites in blog.go, and it's most of this diff's line count.
  • getUser returns banned without omitempty: "not banned" is a meaningful false, not an absent value. banned_at rides along so the dashboard can show since when.
  • updateUser takes banned as intent only; the timestamp is the server's to decide and is never read from the client.
  • The four user.User → domain conversions in the Mongo repository were near-identical copies; they're now one toDomain helper. That's why repository.go shows more deletions than the feature needs.

Relationship to the other PRs

Split out of one working branch alongside contact-us and notes. All three are cut from main and can be merged in any order, but each touches the translation files and infrastructure/ioc/providers/blog.go. This one's blog.go change is the translator argument on every NewAuthenticateMiddleware call — when it conflicts with a sibling PR's new route, the resolution is to keep the new route and add translator to it. Re-run go generate after merging.

Verification

go build ./..., go vet ./... and go test ./... all pass on this branch alone.

🤖 Generated with Claude Code

An administrator can suspend an account from the dashboard, and the ban
takes effect at once — including for access tokens handed out before it.

- user.BannedAt records when the ban started; IsBanned/SetBanned derive
  from it, and re-saving an already banned user does not move the date.
  Stored as a nullable timestamp so lifting a ban writes an explicit null
- The Authenticate middleware turns away every request from a banned user
  with 403 and a body carrying code "user_banned", so a client can tell a
  ban apart from an ordinary permission denial. It translates the message
  itself: it runs before the Localize middleware, so it falls back through
  the requested language, the user's own, then the default
- Login refuses after the password check, so a wrong password cannot be
  used to probe which accounts are banned. Refresh refuses too, since the
  refresh token outlives the access token by days
- The dashboard's user endpoints read and write the flag; the moment of
  the ban is the server's to decide and is never taken from the client

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant