Files
Completed/internal/db/query.sql.go
T
SansanandClaude Sonnet 5 384c4a7217 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>
2026-10-03 11:39:21 +02:00

525 lines
11 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: query.sql
package db
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const createGame = `-- name: CreateGame :one
WITH inserted AS (
INSERT INTO game (
user_id, platform_id, name, score, release_date, finished
) VALUES (
$1, $2, $3, $4, $5, $6
)
RETURNING id, platform_id, name, score, release_date, finished
)
SELECT inserted.id, inserted.name, inserted.platform_id, platform.name AS platform_name,
inserted.score, inserted.release_date, inserted.finished
FROM inserted
JOIN platform ON platform.id = inserted.platform_id
`
type CreateGameParams struct {
UserID uuid.UUID
PlatformID uuid.UUID
Name string
Score int32
ReleaseDate pgtype.Date
Finished pgtype.Date
}
type CreateGameRow struct {
ID uuid.UUID
Name string
PlatformID uuid.UUID
PlatformName string
Score int32
ReleaseDate pgtype.Date
Finished pgtype.Date
}
func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (CreateGameRow, error) {
row := q.db.QueryRow(ctx, createGame,
arg.UserID,
arg.PlatformID,
arg.Name,
arg.Score,
arg.ReleaseDate,
arg.Finished,
)
var i CreateGameRow
err := row.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.PlatformName,
&i.Score,
&i.ReleaseDate,
&i.Finished,
)
return i, err
}
const createPlatform = `-- name: CreatePlatform :one
INSERT INTO platform (
name
) VALUES (
$1
)
RETURNING id, name
`
type CreatePlatformRow struct {
ID uuid.UUID
Name string
}
func (q *Queries) CreatePlatform(ctx context.Context, name string) (CreatePlatformRow, error) {
row := q.db.QueryRow(ctx, createPlatform, name)
var i CreatePlatformRow
err := row.Scan(&i.ID, &i.Name)
return i, err
}
const createSession = `-- name: CreateSession :exec
INSERT INTO sessions (
token_hash, user_id, expires_at
) VALUES (
$1, $2, $3
)
`
type CreateSessionParams struct {
TokenHash []byte
UserID uuid.UUID
ExpiresAt pgtype.Timestamptz
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) error {
_, err := q.db.Exec(ctx, createSession, arg.TokenHash, arg.UserID, arg.ExpiresAt)
return err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (
email, password_hash, name, bio, is_admin
) VALUES (
$1, $2, $3, $4, NOT EXISTS (SELECT 1 FROM users)
)
RETURNING id, email, name, bio, is_admin, created_at
`
type CreateUserParams struct {
Email string
PasswordHash string
Name string
Bio pgtype.Text
}
type CreateUserRow struct {
ID uuid.UUID
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,
arg.PasswordHash,
arg.Name,
arg.Bio,
)
var i CreateUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Bio,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const deleteGame = `-- name: DeleteGame :execrows
UPDATE game
SET deleted_at = now()
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL
`
type DeleteGameParams struct {
ID uuid.UUID
UserID uuid.UUID
}
func (q *Queries) DeleteGame(ctx context.Context, arg DeleteGameParams) (int64, error) {
result, err := q.db.Exec(ctx, deleteGame, arg.ID, arg.UserID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deletePlatform = `-- name: DeletePlatform :execrows
UPDATE platform
SET deleted_at = now()
WHERE platform.id = $1 AND platform.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM game
WHERE game.platform_id = platform.id AND game.deleted_at IS NULL
)
`
// Soft delete, so the FK on game.platform_id is never touched and never
// blocks this on its own; the NOT EXISTS guard is what stops a platform
// that still has active games - from any user, since the list is shared -
// from being deleted. Call GetPlatform first to tell "not found" apart from
// "blocked by games" when this affects 0 rows.
func (q *Queries) DeletePlatform(ctx context.Context, id uuid.UUID) (int64, error) {
result, err := q.db.Exec(ctx, deletePlatform, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deleteSession = `-- name: DeleteSession :exec
DELETE FROM sessions
WHERE token_hash = $1
`
func (q *Queries) DeleteSession(ctx context.Context, tokenHash []byte) error {
_, err := q.db.Exec(ctx, deleteSession, tokenHash)
return err
}
const getGame = `-- name: GetGame :one
SELECT game.id, game.name, game.platform_id, platform.name AS platform_name,
game.score, game.release_date, game.finished
FROM game
JOIN platform ON platform.id = game.platform_id
WHERE game.id = $1 AND game.user_id = $2 AND game.deleted_at IS NULL
`
type GetGameParams struct {
ID uuid.UUID
UserID uuid.UUID
}
type GetGameRow struct {
ID uuid.UUID
Name string
PlatformID uuid.UUID
PlatformName string
Score int32
ReleaseDate pgtype.Date
Finished pgtype.Date
}
func (q *Queries) GetGame(ctx context.Context, arg GetGameParams) (GetGameRow, error) {
row := q.db.QueryRow(ctx, getGame, arg.ID, arg.UserID)
var i GetGameRow
err := row.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.PlatformName,
&i.Score,
&i.ReleaseDate,
&i.Finished,
)
return i, err
}
const getPlatform = `-- name: GetPlatform :one
SELECT id, name
FROM platform
WHERE id = $1 AND deleted_at IS NULL
`
type GetPlatformRow struct {
ID uuid.UUID
Name string
}
func (q *Queries) GetPlatform(ctx context.Context, id uuid.UUID) (GetPlatformRow, error) {
row := q.db.QueryRow(ctx, getPlatform, id)
var i GetPlatformRow
err := row.Scan(&i.ID, &i.Name)
return i, err
}
const getSessionAndUser = `-- name: GetSessionAndUser :one
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()
`
type GetSessionAndUserRow struct {
ID uuid.UUID
Email string
Name string
Bio pgtype.Text
IsAdmin bool
ExpiresAt pgtype.Timestamptz
}
func (q *Queries) GetSessionAndUser(ctx context.Context, tokenHash []byte) (GetSessionAndUserRow, error) {
row := q.db.QueryRow(ctx, getSessionAndUser, tokenHash)
var i GetSessionAndUserRow
err := row.Scan(
&i.ID,
&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, is_admin, created_at
FROM users
WHERE email = $1
`
type GetUserByEmailRow struct {
ID uuid.UUID
Email string
PasswordHash string
Name string
Bio pgtype.Text
IsAdmin bool
CreatedAt pgtype.Timestamptz
}
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) {
row := q.db.QueryRow(ctx, getUserByEmail, email)
var i GetUserByEmailRow
err := row.Scan(
&i.ID,
&i.Email,
&i.PasswordHash,
&i.Name,
&i.Bio,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, email, name, bio, is_admin, created_at
FROM users
WHERE id = $1
`
type GetUserByIDRow struct {
ID uuid.UUID
Email string
Name string
Bio pgtype.Text
IsAdmin bool
CreatedAt pgtype.Timestamptz
}
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error) {
row := q.db.QueryRow(ctx, getUserByID, id)
var i GetUserByIDRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.Bio,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const listGames = `-- name: ListGames :many
SELECT game.id, game.name, game.platform_id, platform.name AS platform_name,
game.score, game.release_date, game.finished
FROM game
JOIN platform ON platform.id = game.platform_id
WHERE game.user_id = $1 AND game.deleted_at IS NULL
ORDER BY game.name
`
type ListGamesRow struct {
ID uuid.UUID
Name string
PlatformID uuid.UUID
PlatformName string
Score int32
ReleaseDate pgtype.Date
Finished pgtype.Date
}
func (q *Queries) ListGames(ctx context.Context, userID uuid.UUID) ([]ListGamesRow, error) {
rows, err := q.db.Query(ctx, listGames, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListGamesRow
for rows.Next() {
var i ListGamesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.PlatformName,
&i.Score,
&i.ReleaseDate,
&i.Finished,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlatforms = `-- name: ListPlatforms :many
SELECT id, name
FROM platform
WHERE deleted_at IS NULL
ORDER BY name
`
type ListPlatformsRow struct {
ID uuid.UUID
Name string
}
func (q *Queries) ListPlatforms(ctx context.Context) ([]ListPlatformsRow, error) {
rows, err := q.db.Query(ctx, listPlatforms)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlatformsRow
for rows.Next() {
var i ListPlatformsRow
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const touchSession = `-- name: TouchSession :exec
UPDATE sessions
SET last_seen_at = now()
WHERE token_hash = $1
`
func (q *Queries) TouchSession(ctx context.Context, tokenHash []byte) error {
_, err := q.db.Exec(ctx, touchSession, tokenHash)
return err
}
const updateGame = `-- name: UpdateGame :one
WITH updated AS (
UPDATE game
SET platform_id = $3, name = $4, score = $5, release_date = $6, finished = $7
WHERE game.id = $1 AND game.user_id = $2 AND game.deleted_at IS NULL
RETURNING id, platform_id, name, score, release_date, finished
)
SELECT updated.id, updated.name, updated.platform_id, platform.name AS platform_name,
updated.score, updated.release_date, updated.finished
FROM updated
JOIN platform ON platform.id = updated.platform_id
`
type UpdateGameParams struct {
ID uuid.UUID
UserID uuid.UUID
PlatformID uuid.UUID
Name string
Score int32
ReleaseDate pgtype.Date
Finished pgtype.Date
}
type UpdateGameRow struct {
ID uuid.UUID
Name string
PlatformID uuid.UUID
PlatformName string
Score int32
ReleaseDate pgtype.Date
Finished pgtype.Date
}
func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) (UpdateGameRow, error) {
row := q.db.QueryRow(ctx, updateGame,
arg.ID,
arg.UserID,
arg.PlatformID,
arg.Name,
arg.Score,
arg.ReleaseDate,
arg.Finished,
)
var i UpdateGameRow
err := row.Scan(
&i.ID,
&i.Name,
&i.PlatformID,
&i.PlatformName,
&i.Score,
&i.ReleaseDate,
&i.Finished,
)
return i, err
}
const updatePlatform = `-- name: UpdatePlatform :one
UPDATE platform
SET name = $2
WHERE id = $1 AND deleted_at IS NULL
RETURNING id, name
`
type UpdatePlatformParams struct {
ID uuid.UUID
Name string
}
type UpdatePlatformRow struct {
ID uuid.UUID
Name string
}
func (q *Queries) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (UpdatePlatformRow, error) {
row := q.db.QueryRow(ctx, updatePlatform, arg.ID, arg.Name)
var i UpdatePlatformRow
err := row.Scan(&i.ID, &i.Name)
return i, err
}