feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+214
View File
@@ -0,0 +1,214 @@
package admin
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"galaxy/backend/internal/postgres/jet/backend/model"
"galaxy/backend/internal/postgres/jet/backend/table"
"github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
)
// adminAccountsPrimaryKey is the constraint name surfaced on the
// primary-key UNIQUE violation when a duplicate username is inserted.
// Postgres synthesises the constraint name as `<table>_pkey` for
// primary-key constraints, which matches the migration in
// `backend/internal/postgres/migrations/00001_init.sql:199`.
const adminAccountsPrimaryKey = "admin_accounts_pkey"
// Store is the Postgres-backed query surface for the admin package.
// Queries are built through go-jet against the generated table
// bindings under `backend/internal/postgres/jet/backend/table`.
type Store struct {
db *sql.DB
}
// NewStore constructs a Store wrapping db.
func NewStore(db *sql.DB) *Store {
return &Store{db: db}
}
// adminColumnList is the canonical projection used by every read path.
// The slice ordering matches the destination struct fields.
func adminColumnList() postgres.ColumnList {
return postgres.ColumnList{
table.AdminAccounts.Username,
table.AdminAccounts.PasswordHash,
table.AdminAccounts.CreatedAt,
table.AdminAccounts.LastUsedAt,
table.AdminAccounts.DisabledAt,
}
}
// Lookup returns the admin row and its bcrypt hash for username.
// Returns ErrNotFound when no row matches.
func (s *Store) Lookup(ctx context.Context, username string) (Admin, []byte, error) {
stmt := postgres.SELECT(adminColumnList()).
FROM(table.AdminAccounts).
WHERE(table.AdminAccounts.Username.EQ(postgres.String(username))).
LIMIT(1)
var row model.AdminAccounts
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return Admin{}, nil, ErrNotFound
}
return Admin{}, nil, fmt.Errorf("admin store: lookup %q: %w", username, err)
}
admin, hash := modelToAdmin(row)
return admin, hash, nil
}
// ListAll returns every admin row paired with its bcrypt hash, ordered
// by username ASC. Used by Cache.Warm and by the List handler (the
// hashes are dropped before the handler sends a response, but Warm
// needs them so Verify can match without a follow-up query).
func (s *Store) ListAll(ctx context.Context) ([]Admin, [][]byte, error) {
stmt := postgres.SELECT(adminColumnList()).
FROM(table.AdminAccounts).
ORDER_BY(table.AdminAccounts.Username.ASC())
var rows []model.AdminAccounts
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
return nil, nil, fmt.Errorf("admin store: list: %w", err)
}
admins := make([]Admin, 0, len(rows))
hashes := make([][]byte, 0, len(rows))
for _, row := range rows {
admin, hash := modelToAdmin(row)
admins = append(admins, admin)
hashes = append(hashes, hash)
}
return admins, hashes, nil
}
// Insert persists a fresh admin row. Returns ErrUsernameTaken when the
// primary-key UNIQUE constraint is violated (concurrent or repeat
// Create).
func (s *Store) Insert(ctx context.Context, username string, passwordHash []byte) (Admin, error) {
stmt := table.AdminAccounts.
INSERT(table.AdminAccounts.Username, table.AdminAccounts.PasswordHash).
VALUES(username, passwordHash).
RETURNING(adminColumnList())
var row model.AdminAccounts
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
if isUniqueViolation(err, adminAccountsPrimaryKey) {
return Admin{}, ErrUsernameTaken
}
return Admin{}, fmt.Errorf("admin store: insert %q: %w", username, err)
}
admin, _ := modelToAdmin(row)
return admin, nil
}
// UpdatePasswordHash replaces the stored bcrypt hash for username.
// Returns ErrNotFound when no row matches.
func (s *Store) UpdatePasswordHash(ctx context.Context, username string, passwordHash []byte) (Admin, error) {
stmt := table.AdminAccounts.
UPDATE(table.AdminAccounts.PasswordHash).
SET(passwordHash).
WHERE(table.AdminAccounts.Username.EQ(postgres.String(username))).
RETURNING(adminColumnList())
var row model.AdminAccounts
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return Admin{}, ErrNotFound
}
return Admin{}, fmt.Errorf("admin store: update password for %q: %w", username, err)
}
admin, _ := modelToAdmin(row)
return admin, nil
}
// SetDisabledAt patches `disabled_at` for username. Pass `&time` to
// disable, `nil` to re-enable. Returns the refreshed Admin together
// with its bcrypt hash so the cache stays consistent. Returns
// ErrNotFound when no row matches.
func (s *Store) SetDisabledAt(ctx context.Context, username string, disabledAt *time.Time) (Admin, []byte, error) {
var disabledExpr postgres.Expression
if disabledAt != nil {
disabledExpr = postgres.TimestampzT(*disabledAt)
} else {
disabledExpr = postgres.TimestampzExp(postgres.NULL)
}
stmt := table.AdminAccounts.
UPDATE(table.AdminAccounts.DisabledAt).
SET(disabledExpr).
WHERE(table.AdminAccounts.Username.EQ(postgres.String(username))).
RETURNING(adminColumnList())
var row model.AdminAccounts
if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return Admin{}, nil, ErrNotFound
}
return Admin{}, nil, fmt.Errorf("admin store: set disabled_at for %q: %w", username, err)
}
admin, hash := modelToAdmin(row)
return admin, hash, nil
}
// TouchLastUsed bumps last_used_at on a successful Verify. The caller
// runs the update fire-and-forget; errors are returned for logging
// but never propagated to the request.
func (s *Store) TouchLastUsed(ctx context.Context, username string, now time.Time) error {
stmt := table.AdminAccounts.
UPDATE(table.AdminAccounts.LastUsedAt).
SET(postgres.TimestampzT(now)).
WHERE(table.AdminAccounts.Username.EQ(postgres.String(username)))
if _, err := stmt.ExecContext(ctx, s.db); err != nil {
return fmt.Errorf("admin store: touch last_used_at for %q: %w", username, err)
}
return nil
}
// BootstrapInsert inserts the seed admin row when no row with the
// supplied username exists. The boolean reports whether the insert
// happened (true) or was skipped because of an existing row (false).
//
// Idempotent across restarts: subsequent calls with the same username
// return false without modifying the password hash. Operators rotate
// the seed admin's password through `ResetPassword`, not by editing
// env vars and restarting.
func (s *Store) BootstrapInsert(ctx context.Context, username string, passwordHash []byte) (bool, error) {
stmt := table.AdminAccounts.
INSERT(table.AdminAccounts.Username, table.AdminAccounts.PasswordHash).
VALUES(username, passwordHash).
ON_CONFLICT(table.AdminAccounts.Username).
DO_NOTHING()
res, err := stmt.ExecContext(ctx, s.db)
if err != nil {
return false, fmt.Errorf("admin store: bootstrap insert %q: %w", username, err)
}
affected, err := res.RowsAffected()
if err != nil {
return false, fmt.Errorf("admin store: bootstrap rows-affected: %w", err)
}
return affected > 0, nil
}
// modelToAdmin projects a generated model row into the public Admin
// struct plus the raw password hash. The conversion centralises the
// pointer-copy of nullable timestamps so each method stays a one-liner.
func modelToAdmin(row model.AdminAccounts) (Admin, []byte) {
admin := Admin{
Username: row.Username,
CreatedAt: row.CreatedAt,
}
if row.LastUsedAt != nil {
t := *row.LastUsedAt
admin.LastUsedAt = &t
}
if row.DisabledAt != nil {
t := *row.DisabledAt
admin.DisabledAt = &t
}
return admin, row.PasswordHash
}