forked from wrenn/wrenn
Add PUT /v1/admin/users/{id}/admin endpoint and frontend UI for
granting and revoking platform admin status. Uses atomic conditional
SQL (RevokeUserAdmin) to prevent race conditions that could remove
the last admin. Includes idempotency check, audit logging, and
confirmation dialog with self-demotion warning.
526 lines
13 KiB
Go
526 lines
13 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: users.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const countActiveUsers = `-- name: CountActiveUsers :one
|
|
SELECT COUNT(*) FROM users WHERE status = 'active'
|
|
`
|
|
|
|
func (q *Queries) CountActiveUsers(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countActiveUsers)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countUserOwnedTeamsWithOtherMembers = `-- name: CountUserOwnedTeamsWithOtherMembers :one
|
|
SELECT COUNT(DISTINCT ut.team_id)::int
|
|
FROM users_teams ut
|
|
WHERE ut.user_id = $1
|
|
AND ut.role = 'owner'
|
|
AND EXISTS (
|
|
SELECT 1 FROM users_teams ut2
|
|
WHERE ut2.team_id = ut.team_id AND ut2.user_id <> $1
|
|
)
|
|
`
|
|
|
|
func (q *Queries) CountUserOwnedTeamsWithOtherMembers(ctx context.Context, userID pgtype.UUID) (int32, error) {
|
|
row := q.db.QueryRow(ctx, countUserOwnedTeamsWithOtherMembers, userID)
|
|
var column_1 int32
|
|
err := row.Scan(&column_1)
|
|
return column_1, err
|
|
}
|
|
|
|
const countUsers = `-- name: CountUsers :one
|
|
SELECT COUNT(*) FROM users
|
|
`
|
|
|
|
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countUsers)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countUsersAdmin = `-- name: CountUsersAdmin :one
|
|
SELECT COUNT(*)::int AS total
|
|
FROM users
|
|
WHERE status != 'deleted'
|
|
`
|
|
|
|
func (q *Queries) CountUsersAdmin(ctx context.Context) (int32, error) {
|
|
row := q.db.QueryRow(ctx, countUsersAdmin)
|
|
var total int32
|
|
err := row.Scan(&total)
|
|
return total, err
|
|
}
|
|
|
|
const deleteAdminPermission = `-- name: DeleteAdminPermission :exec
|
|
DELETE FROM admin_permissions WHERE user_id = $1 AND permission = $2
|
|
`
|
|
|
|
type DeleteAdminPermissionParams struct {
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
func (q *Queries) DeleteAdminPermission(ctx context.Context, arg DeleteAdminPermissionParams) error {
|
|
_, err := q.db.Exec(ctx, deleteAdminPermission, arg.UserID, arg.Permission)
|
|
return err
|
|
}
|
|
|
|
const getAdminPermissions = `-- name: GetAdminPermissions :many
|
|
SELECT id, user_id, permission, created_at FROM admin_permissions WHERE user_id = $1 ORDER BY permission
|
|
`
|
|
|
|
func (q *Queries) GetAdminPermissions(ctx context.Context, userID pgtype.UUID) ([]AdminPermission, error) {
|
|
rows, err := q.db.Query(ctx, getAdminPermissions, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []AdminPermission
|
|
for rows.Next() {
|
|
var i AdminPermission
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.UserID,
|
|
&i.Permission,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getAdminUsers = `-- name: GetAdminUsers :many
|
|
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE is_admin = TRUE ORDER BY created_at
|
|
`
|
|
|
|
func (q *Queries) GetAdminUsers(ctx context.Context) ([]User, error) {
|
|
rows, err := q.db.Query(ctx, getAdminUsers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []User
|
|
for rows.Next() {
|
|
var i User
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeletedAt,
|
|
&i.Status,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
|
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE email = $1 AND status != 'deleted'
|
|
`
|
|
|
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeletedAt,
|
|
&i.Status,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByID = `-- name: GetUserByID :one
|
|
SELECT id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status FROM users WHERE id = $1 AND status != 'deleted'
|
|
`
|
|
|
|
func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByID, id)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeletedAt,
|
|
&i.Status,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const hardDeleteUser = `-- name: HardDeleteUser :exec
|
|
DELETE FROM users WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) HardDeleteUser(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, hardDeleteUser, id)
|
|
return err
|
|
}
|
|
|
|
const hasAdminPermission = `-- name: HasAdminPermission :one
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM admin_permissions WHERE user_id = $1 AND permission = $2
|
|
) AS has_permission
|
|
`
|
|
|
|
type HasAdminPermissionParams struct {
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
func (q *Queries) HasAdminPermission(ctx context.Context, arg HasAdminPermissionParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, hasAdminPermission, arg.UserID, arg.Permission)
|
|
var has_permission bool
|
|
err := row.Scan(&has_permission)
|
|
return has_permission, err
|
|
}
|
|
|
|
const insertAdminPermission = `-- name: InsertAdminPermission :exec
|
|
INSERT INTO admin_permissions (id, user_id, permission)
|
|
VALUES ($1, $2, $3)
|
|
`
|
|
|
|
type InsertAdminPermissionParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
func (q *Queries) InsertAdminPermission(ctx context.Context, arg InsertAdminPermissionParams) error {
|
|
_, err := q.db.Exec(ctx, insertAdminPermission, arg.ID, arg.UserID, arg.Permission)
|
|
return err
|
|
}
|
|
|
|
const insertUser = `-- name: InsertUser :one
|
|
INSERT INTO users (id, email, password_hash, name)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status
|
|
`
|
|
|
|
type InsertUserParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
PasswordHash pgtype.Text `json:"password_hash"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, insertUser,
|
|
arg.ID,
|
|
arg.Email,
|
|
arg.PasswordHash,
|
|
arg.Name,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeletedAt,
|
|
&i.Status,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertUserInactive = `-- name: InsertUserInactive :one
|
|
INSERT INTO users (id, email, password_hash, name, status)
|
|
VALUES ($1, $2, $3, $4, 'inactive')
|
|
RETURNING id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status
|
|
`
|
|
|
|
type InsertUserInactiveParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
PasswordHash pgtype.Text `json:"password_hash"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (q *Queries) InsertUserInactive(ctx context.Context, arg InsertUserInactiveParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, insertUserInactive,
|
|
arg.ID,
|
|
arg.Email,
|
|
arg.PasswordHash,
|
|
arg.Name,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeletedAt,
|
|
&i.Status,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const insertUserOAuth = `-- name: InsertUserOAuth :one
|
|
INSERT INTO users (id, email, name)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, email, password_hash, name, is_admin, created_at, updated_at, deleted_at, status
|
|
`
|
|
|
|
type InsertUserOAuthParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (q *Queries) InsertUserOAuth(ctx context.Context, arg InsertUserOAuthParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, insertUserOAuth, arg.ID, arg.Email, arg.Name)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.PasswordHash,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeletedAt,
|
|
&i.Status,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listExpiredSoftDeletedUsers = `-- name: ListExpiredSoftDeletedUsers :many
|
|
SELECT id, email FROM users WHERE deleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL '15 days'
|
|
`
|
|
|
|
type ListExpiredSoftDeletedUsersRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
func (q *Queries) ListExpiredSoftDeletedUsers(ctx context.Context) ([]ListExpiredSoftDeletedUsersRow, error) {
|
|
rows, err := q.db.Query(ctx, listExpiredSoftDeletedUsers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListExpiredSoftDeletedUsersRow
|
|
for rows.Next() {
|
|
var i ListExpiredSoftDeletedUsersRow
|
|
if err := rows.Scan(&i.ID, &i.Email); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listUsersAdmin = `-- name: ListUsersAdmin :many
|
|
SELECT
|
|
u.id,
|
|
u.email,
|
|
u.name,
|
|
u.is_admin,
|
|
u.status,
|
|
u.created_at,
|
|
(SELECT COUNT(*) FROM users_teams ut WHERE ut.user_id = u.id)::int AS teams_joined,
|
|
(SELECT COUNT(*) FROM users_teams ut WHERE ut.user_id = u.id AND ut.role = 'owner')::int AS teams_owned
|
|
FROM users u
|
|
WHERE u.status != 'deleted'
|
|
ORDER BY u.created_at DESC
|
|
LIMIT $1 OFFSET $2
|
|
`
|
|
|
|
type ListUsersAdminParams struct {
|
|
Limit int32 `json:"limit"`
|
|
Offset int32 `json:"offset"`
|
|
}
|
|
|
|
type ListUsersAdminRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
Status string `json:"status"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
TeamsJoined int32 `json:"teams_joined"`
|
|
TeamsOwned int32 `json:"teams_owned"`
|
|
}
|
|
|
|
func (q *Queries) ListUsersAdmin(ctx context.Context, arg ListUsersAdminParams) ([]ListUsersAdminRow, error) {
|
|
rows, err := q.db.Query(ctx, listUsersAdmin, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListUsersAdminRow
|
|
for rows.Next() {
|
|
var i ListUsersAdminRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Email,
|
|
&i.Name,
|
|
&i.IsAdmin,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.TeamsJoined,
|
|
&i.TeamsOwned,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const revokeUserAdmin = `-- name: RevokeUserAdmin :execrows
|
|
UPDATE users u SET is_admin = false, updated_at = NOW()
|
|
WHERE u.id = $1
|
|
AND u.is_admin = true
|
|
AND (SELECT COUNT(*) FROM users WHERE is_admin = true AND status != 'deleted') > 1
|
|
`
|
|
|
|
func (q *Queries) RevokeUserAdmin(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, revokeUserAdmin, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const searchUsersByEmailPrefix = `-- name: SearchUsersByEmailPrefix :many
|
|
SELECT id, email FROM users WHERE email LIKE $1 || '%' ORDER BY email LIMIT 10
|
|
`
|
|
|
|
type SearchUsersByEmailPrefixRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
func (q *Queries) SearchUsersByEmailPrefix(ctx context.Context, dollar_1 pgtype.Text) ([]SearchUsersByEmailPrefixRow, error) {
|
|
rows, err := q.db.Query(ctx, searchUsersByEmailPrefix, dollar_1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []SearchUsersByEmailPrefixRow
|
|
for rows.Next() {
|
|
var i SearchUsersByEmailPrefixRow
|
|
if err := rows.Scan(&i.ID, &i.Email); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const setUserAdmin = `-- name: SetUserAdmin :exec
|
|
UPDATE users SET is_admin = $2, updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
type SetUserAdminParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
|
|
func (q *Queries) SetUserAdmin(ctx context.Context, arg SetUserAdminParams) error {
|
|
_, err := q.db.Exec(ctx, setUserAdmin, arg.ID, arg.IsAdmin)
|
|
return err
|
|
}
|
|
|
|
const setUserStatus = `-- name: SetUserStatus :exec
|
|
UPDATE users SET status = $2, updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
type SetUserStatusParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (q *Queries) SetUserStatus(ctx context.Context, arg SetUserStatusParams) error {
|
|
_, err := q.db.Exec(ctx, setUserStatus, arg.ID, arg.Status)
|
|
return err
|
|
}
|
|
|
|
const softDeleteUser = `-- name: SoftDeleteUser :exec
|
|
UPDATE users SET deleted_at = NOW(), status = 'deleted', updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) SoftDeleteUser(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, softDeleteUser, id)
|
|
return err
|
|
}
|
|
|
|
const updateUserName = `-- name: UpdateUserName :exec
|
|
UPDATE users SET name = $2, updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
type UpdateUserNameParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (q *Queries) UpdateUserName(ctx context.Context, arg UpdateUserNameParams) error {
|
|
_, err := q.db.Exec(ctx, updateUserName, arg.ID, arg.Name)
|
|
return err
|
|
}
|
|
|
|
const updateUserPassword = `-- name: UpdateUserPassword :exec
|
|
UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1
|
|
`
|
|
|
|
type UpdateUserPasswordParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
PasswordHash pgtype.Text `json:"password_hash"`
|
|
}
|
|
|
|
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, updateUserPassword, arg.ID, arg.PasswordHash)
|
|
return err
|
|
}
|