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:
SansanandClaude Sonnet 5 committed 2026-10-03 11:39:21 +02:00
1 parent 7fbd403d30
commit 384c4a7217
12 files changed
+110 -35

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"components":{"schemas":{"AuthResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/AuthResponse.json"],"format":"uri","readOnly":true,"type":"string"},"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}},"required":["token","user"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ErrorModel.json"],"format":"uri","readOnly":true,"type":"string"},"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"Game":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Game.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"id":{"type":"string"},"name":{"type":"string"},"platform_id":{"type":"string"},"platform_name":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","type":"integer"}},"required":["id","name","platform_id","platform_name","score","release_date"],"type":"object"},"GameInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/GameInput.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"name":{"minLength":1,"type":"string"},"platform_id":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","maximum":100,"minimum":0,"type":"integer"}},"required":["name","platform_id","score","release_date"],"type":"object"},"Get-helloResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Get-helloResponse.json"],"format":"uri","readOnly":true,"type":"string"},"message":{"type":"string"}},"required":["message"],"type":"object"},"LoginInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/LoginInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"password":{"type":"string"}},"required":["email","password"],"type":"object"},"Platform":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Platform.json"],"format":"uri","readOnly":true,"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"PlatformInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PlatformInput.json"],"format":"uri","readOnly":true,"type":"string"},"name":{"description":"Name of the platform","minLength":1,"type":"string"}},"required":["name"],"type":"object"},"RegisterInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RegisterInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"name":{"minLength":1,"type":"string"},"password":{"minLength":8,"type":"string"}},"required":["email","password","name"],"type":"object"},"User":{"additionalProperties":false,"properties":{"$schema":{"description":"Line truncated
{"components":{"schemas":{"AuthResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/AuthResponse.json"],"format":"uri","readOnly":true,"type":"string"},"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}},"required":["token","user"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ErrorModel.json"],"format":"uri","readOnly":true,"type":"string"},"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"Game":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Game.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"id":{"type":"string"},"name":{"type":"string"},"platform_id":{"type":"string"},"platform_name":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","type":"integer"}},"required":["id","name","platform_id","platform_name","score","release_date"],"type":"object"},"GameInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/GameInput.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"name":{"minLength":1,"type":"string"},"platform_id":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","maximum":100,"minimum":0,"type":"integer"}},"required":["name","platform_id","score","release_date"],"type":"object"},"Get-helloResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Get-helloResponse.json"],"format":"uri","readOnly":true,"type":"string"},"message":{"type":"string"}},"required":["message"],"type":"object"},"LoginInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/LoginInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"password":{"type":"string"}},"required":["email","password"],"type":"object"},"Platform":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Platform.json"],"format":"uri","readOnly":true,"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"PlatformInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PlatformInput.json"],"format":"uri","readOnly":true,"type":"string"},"name":{"description":"Name of the platform","minLength":1,"type":"string"}},"required":["name"],"type":"object"},"RegisterInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RegisterInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"name":{"minLength":1,"type":"string"},"password":{"minLength":8,"type":"string"}},"required":["email","password","name"],"type":"object"},"User":{"additionalProperties":false,"properties":{"$schema":{"description":"Line truncated
+7
View File
@@ -14,6 +14,13 @@ CREATE TABLE users (
password_hash text NOT NULL,
name text NOT NULL,
bio text,
-- Only admins may add/rename/delete from the shared platform list (see
-- the platform table below); everyone can read it. The first account
-- ever registered becomes an admin automatically (see CreateUser in
-- db/query.sql) so there's always at least one without manual setup;
-- promoting anyone beyond that is a manual
-- `UPDATE users SET is_admin = true WHERE email = ...` for now.
is_admin boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
+9 -6
View File
@@ -1,18 +1,21 @@
-- name: CreateUser :one
-- 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.
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;
-- 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;
-- name: GetUserByID :one
SELECT id, email, name, bio, created_at
SELECT id, email, name, bio, is_admin, created_at
FROM users
WHERE id = $1;
@@ -24,7 +27,7 @@ INSERT INTO sessions (
);
-- 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();
+2 -2
View File
@@ -93,7 +93,7 @@ func registerAuthRoutes(api huma.API, queries db.Querier) {
SetCookie: sessionCookie(token),
Body: model.AuthResponse{
Token: token,
User: model.User{ID: u.ID.String(), Email: u.Email, Name: u.Name},
User: model.User{ID: u.ID.String(), Email: u.Email, Name: u.Name, IsAdmin: u.IsAdmin},
},
}, nil
})
@@ -126,7 +126,7 @@ func registerAuthRoutes(api huma.API, queries db.Querier) {
SetCookie: sessionCookie(token),
Body: model.AuthResponse{
Token: token,
User: model.User{ID: u.ID.String(), Email: u.Email, Name: u.Name},
User: model.User{ID: u.ID.String(), Email: u.Email, Name: u.Name, IsAdmin: u.IsAdmin},
},
}, nil
})
+6 -5
View File
@@ -11,10 +11,11 @@ import (
// authedUser is what AuthMiddleware stores in the request context after
// validating a session token. Handlers read it with userFromContext.
type authedUser struct {
ID uuid.UUID
Email string
Name string
Bio string
ID uuid.UUID
Email string
Name string
Bio string
IsAdmin bool
}
type ctxKey string
@@ -22,7 +23,7 @@ type ctxKey string
const userCtxKey ctxKey = "completed.user"
func (u authedUser) toModel() model.User {
return model.User{ID: u.ID.String(), Email: u.Email, Name: u.Name, Bio: u.Bio}
return model.User{ID: u.ID.String(), Email: u.Email, Name: u.Name, Bio: u.Bio, IsAdmin: u.IsAdmin}
}
// userFromContext returns the authenticated user set by AuthMiddleware. It
+25 -1
View File
@@ -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.
+9 -7
View File
@@ -42,10 +42,12 @@ func RegisterRoutes(api huma.API, queries db.Querier) {
// --- Platform Routes ---
//
// Platforms are a shared list: every authenticated user reads and writes
// the same rows, unlike games, which are private per user. Routes still
// require auth (so the list isn't public), but none of the queries below
// filter by user_id, because platform has no such column.
// Platforms are a shared list: every authenticated user reads the same
// rows, unlike games, which are private per user. None of the queries
// below filter by user_id, because platform has no such column. Reading
// the list only requires being logged in (addAuth); adding, renaming or
// deleting a platform requires being an admin (addAdminAuth), since that
// changes what everyone else sees.
func registerPlatformRoutes(api huma.API, queries db.Querier) {
listOp := huma.Operation{
@@ -90,7 +92,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
Path: "/api/platforms",
Summary: "Create Platform",
}
addAuth(&createOp, api, queries)
addAdminAuth(&createOp, api, queries)
huma.Register(api, createOp, func(ctx context.Context, input *struct{ Body model.PlatformInput }) (*struct{ Body model.Platform }, error) {
platform, err := queries.CreatePlatform(ctx, input.Body.Name)
if err != nil {
@@ -105,7 +107,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
Path: "/api/platforms/{id}",
Summary: "Update Platform",
}
addAuth(&updateOp, api, queries)
addAdminAuth(&updateOp, api, queries)
huma.Register(api, updateOp, func(ctx context.Context, input *struct {
ID uuid.UUID `path:"id"`
Body model.PlatformInput
@@ -126,7 +128,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) {
Path: "/api/platforms/{id}",
Summary: "Delete Platform",
}
addAuth(&deleteOp, api, queries)
addAdminAuth(&deleteOp, api, queries)
huma.Register(api, deleteOp, func(ctx context.Context, input *struct {
ID uuid.UUID `path:"id"`
}) (*struct{}, error) {
+25 -3
View File
@@ -43,6 +43,16 @@ func authedMock() *MockQuerier {
}
}
// adminMock is authedMock for a session belonging to an admin, for the
// platform-mutation routes that require one.
func adminMock() *MockQuerier {
m := authedMock()
m.GetSessionAndUserFunc = func(ctx context.Context, tokenHash []byte) (db.GetSessionAndUserRow, error) {
return db.GetSessionAndUserRow{ID: testUserID, Email: "a@example.com", Name: "Alice", IsAdmin: true}, nil
}
return m
}
func authedRequest(method, path, body string) *http.Request {
var req *http.Request
if body == "" {
@@ -118,7 +128,7 @@ func TestDeletePlatformConflictWhenReferencedByGames(t *testing.T) {
// db/query.sql's DeletePlatform comment), so the handler has to notice
// the 0-rows result itself and turn it into 409, not just pass through
// whatever error (if any) the query returns.
mockDB := authedMock()
mockDB := adminMock()
mockDB.GetPlatformFunc = func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return db.GetPlatformRow{ID: id, Name: "PC"}, nil
}
@@ -134,7 +144,7 @@ func TestDeletePlatformConflictWhenReferencedByGames(t *testing.T) {
}
func TestDeletePlatformNotFound(t *testing.T) {
mockDB := authedMock()
mockDB := adminMock()
mockDB.GetPlatformFunc = func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return db.GetPlatformRow{}, pgx.ErrNoRows
}
@@ -160,7 +170,7 @@ func TestCreateGameRejectsScoreOutOfRange(t *testing.T) {
}
func TestCreatePlatform(t *testing.T) {
mockDB := authedMock()
mockDB := adminMock()
mockDB.CreatePlatformFunc = func(ctx context.Context, name string) (db.CreatePlatformRow, error) {
return db.CreatePlatformRow{ID: uuid.New(), Name: name}, nil
}
@@ -173,6 +183,18 @@ func TestCreatePlatform(t *testing.T) {
assert.Contains(t, rec.Body.String(), "Sega Genesis")
}
func TestCreatePlatformForbiddenForNonAdmin(t *testing.T) {
mockDB := authedMock()
// No CreatePlatformFunc set: the admin check must reject the request
// before the handler ever calls the database, or this test would panic.
e, _ := newTestAPI(mockDB)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, authedRequest(http.MethodPost, "/api/platforms", `{"name": "Sega Genesis"}`))
assert.Equal(t, http.StatusForbidden, rec.Code)
}
func TestListPlatformsIsSharedAcrossUsers(t *testing.T) {
// Unlike games, the platform list has no user_id to scope by: every
// authenticated user sees the same rows. ListPlatformsFunc taking no
+1
View File
@@ -44,6 +44,7 @@ type User struct {
PasswordHash string
Name string
Bio pgtype.Text
IsAdmin bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
+3
View File
@@ -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
+17 -6
View File
@@ -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
+5 -4
View File
@@ -5,10 +5,11 @@ package model
// User is the public view of an account; it never carries the password hash.
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Bio string `json:"bio,omitempty"`
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Bio string `json:"bio,omitempty"`
IsAdmin bool `json:"is_admin"`
}
type RegisterInput struct {