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
@@ -44,6 +44,7 @@ type User struct {
|
||||
PasswordHash string
|
||||
Name string
|
||||
Bio pgtype.Text
|
||||
IsAdmin bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
@@ -14,6 +14,9 @@ type Querier interface {
|
||||
CreateGame(ctx context.Context, arg CreateGameParams) (CreateGameRow, error)
|
||||
CreatePlatform(ctx context.Context, name string) (CreatePlatformRow, error)
|
||||
CreateSession(ctx context.Context, arg CreateSessionParams) error
|
||||
// The first account ever registered becomes an admin automatically; see
|
||||
// the is_admin column's comment in db/migrations for why and the race this
|
||||
// accepts. Everyone after that registers as a non-admin.
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error)
|
||||
DeleteGame(ctx context.Context, arg DeleteGameParams) (int64, error)
|
||||
// Soft delete, so the FK on game.platform_id is never touched and never
|
||||
|
||||
@@ -110,11 +110,11 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) er
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
email, password_hash, name, bio
|
||||
email, password_hash, name, bio, is_admin
|
||||
) VALUES (
|
||||
$1, $2, $3, $4
|
||||
$1, $2, $3, $4, NOT EXISTS (SELECT 1 FROM users)
|
||||
)
|
||||
RETURNING id, email, name, bio, created_at
|
||||
RETURNING id, email, name, bio, is_admin, created_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
@@ -129,9 +129,13 @@ type CreateUserRow struct {
|
||||
Email string
|
||||
Name string
|
||||
Bio pgtype.Text
|
||||
IsAdmin bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
// The first account ever registered becomes an admin automatically; see
|
||||
// the is_admin column's comment in db/migrations for why and the race this
|
||||
// accepts. Everyone after that registers as a non-admin.
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
|
||||
row := q.db.QueryRow(ctx, createUser,
|
||||
arg.Email,
|
||||
@@ -145,6 +149,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
|
||||
&i.Email,
|
||||
&i.Name,
|
||||
&i.Bio,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
@@ -259,7 +264,7 @@ func (q *Queries) GetPlatform(ctx context.Context, id uuid.UUID) (GetPlatformRow
|
||||
}
|
||||
|
||||
const getSessionAndUser = `-- name: GetSessionAndUser :one
|
||||
SELECT users.id, users.email, users.name, users.bio, sessions.expires_at
|
||||
SELECT users.id, users.email, users.name, users.bio, users.is_admin, sessions.expires_at
|
||||
FROM sessions
|
||||
JOIN users ON users.id = sessions.user_id
|
||||
WHERE sessions.token_hash = $1 AND sessions.expires_at > now()
|
||||
@@ -270,6 +275,7 @@ type GetSessionAndUserRow struct {
|
||||
Email string
|
||||
Name string
|
||||
Bio pgtype.Text
|
||||
IsAdmin bool
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
@@ -281,13 +287,14 @@ func (q *Queries) GetSessionAndUser(ctx context.Context, tokenHash []byte) (GetS
|
||||
&i.Email,
|
||||
&i.Name,
|
||||
&i.Bio,
|
||||
&i.IsAdmin,
|
||||
&i.ExpiresAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, password_hash, name, bio, created_at
|
||||
SELECT id, email, password_hash, name, bio, is_admin, created_at
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
`
|
||||
@@ -298,6 +305,7 @@ type GetUserByEmailRow struct {
|
||||
PasswordHash string
|
||||
Name string
|
||||
Bio pgtype.Text
|
||||
IsAdmin bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
@@ -310,13 +318,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
&i.PasswordHash,
|
||||
&i.Name,
|
||||
&i.Bio,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, name, bio, created_at
|
||||
SELECT id, email, name, bio, is_admin, created_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
@@ -326,6 +335,7 @@ type GetUserByIDRow struct {
|
||||
Email string
|
||||
Name string
|
||||
Bio pgtype.Text
|
||||
IsAdmin bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
@@ -337,6 +347,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow
|
||||
&i.Email,
|
||||
&i.Name,
|
||||
&i.Bio,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
|
||||
Reference in new issue
Block a user