Restrict platform-list edits to admins
Anyone authenticated can still read the shared platform list, but only an
admin may add, rename or delete from it.
- users.is_admin boolean, defaulting to false. The very first account ever
registered becomes an admin automatically (CreateUser's INSERT uses
NOT EXISTS (SELECT 1 FROM users) for is_admin), so there's always at
least one without manual setup; promoting anyone beyond that is a manual
UPDATE for now (documented in the migration's comment on the column).
- GetSessionAndUser, GetUserByEmail and GetUserByID now also select
is_admin, and it flows through to model.User (and so to
/api/auth/{register,login,me}'s response) and to the auth middleware's
authedUser.
- New requireAdminMiddleware + addAdminAuth, layered on top of the existing
auth middleware. Wired onto create/update/delete-platform; list/get stay
on plain addAuth, since reading the list only requires being logged in.
Verified end to end: the first registered user comes back is_admin:true,
the second false; the non-admin gets 403 on create/update/delete-platform
but 200 on list; the admin can do all of it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fbd403d30
commit
384c4a7217
12 files changed
+110
-35
No files matched your search
@@ -64,7 +64,7 @@ func newAuthMiddleware(api huma.API, queries db.Querier) func(huma.Context, func
|
||||
// Best-effort; a failure to record activity shouldn't fail the request.
|
||||
_ = queries.TouchSession(ctx.Context(), auth.HashSessionToken(token))
|
||||
|
||||
user := authedUser{ID: row.ID, Email: row.Email, Name: row.Name, Bio: row.Bio.String}
|
||||
user := authedUser{ID: row.ID, Email: row.Email, Name: row.Name, Bio: row.Bio.String, IsAdmin: row.IsAdmin}
|
||||
newCtx := huma.WithValue(ctx, userCtxKey, user)
|
||||
newCtx = huma.WithValue(newCtx, tokenCtxKey, token)
|
||||
next(newCtx)
|
||||
@@ -77,6 +77,30 @@ func addAuth(op *huma.Operation, api huma.API, queries db.Querier) {
|
||||
op.Middlewares = append(op.Middlewares, newAuthMiddleware(api, queries))
|
||||
}
|
||||
|
||||
// requireAdminMiddleware runs after newAuthMiddleware (see addAdminAuth) and
|
||||
// rejects the request unless the authenticated user is an admin. It reads
|
||||
// the context value that middleware set directly, rather than calling
|
||||
// userFromContext, so a wiring mistake (this middleware attached without
|
||||
// the auth one ahead of it) fails the request with 403 instead of panicking.
|
||||
func requireAdminMiddleware(api huma.API) func(huma.Context, func(huma.Context)) {
|
||||
return func(ctx huma.Context, next func(huma.Context)) {
|
||||
u, ok := ctx.Context().Value(userCtxKey).(authedUser)
|
||||
if !ok || !u.IsAdmin {
|
||||
huma.WriteErr(api, ctx, 403, "admin access required")
|
||||
return
|
||||
}
|
||||
next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// addAdminAuth attaches both the auth and the admin-only middleware to op,
|
||||
// for routes that mutate data every user can see (the shared platform
|
||||
// list) but only an admin should be able to change.
|
||||
func addAdminAuth(op *huma.Operation, api huma.API, queries db.Querier) {
|
||||
addAuth(op, api, queries)
|
||||
op.Middlewares = append(op.Middlewares, requireAdminMiddleware(api))
|
||||
}
|
||||
|
||||
// tokenCtxKey holds the raw bearer/cookie token for the current request, so
|
||||
// the logout handler can hash and delete the exact session that was used
|
||||
// without re-parsing the request.
|
||||
|
||||
Reference in new issue
Block a user