Distinguish auth/lookup failures, make votes idempotent on visible questions, bound shutdown, page admin users, LRU throttle, trusted-proxy CIDRs, avatar cleanup, versioned migrations, and session cleanup logging.
148 lines
3.6 KiB
Go
148 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"plumber/internal/store/sqlc"
|
|
)
|
|
|
|
// Postgres implements Store against a sqlc-backed database.
|
|
type Postgres struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewPostgres wraps db as a Store.
|
|
func NewPostgres(db *sql.DB) *Postgres {
|
|
return &Postgres{db: db}
|
|
}
|
|
|
|
func (p *Postgres) CreateUser(ctx context.Context, u *User) error {
|
|
if u == nil {
|
|
return fmt.Errorf("user: nil")
|
|
}
|
|
if u.Role != RoleUser && u.Role != RoleAdmin {
|
|
return fmt.Errorf("invalid role")
|
|
}
|
|
u.Username = NormalizeUsername(u.Username)
|
|
if u.ID == "" {
|
|
u.ID = uuid.NewString()
|
|
}
|
|
if u.Name == "" {
|
|
u.Name = u.Username
|
|
}
|
|
if u.CreatedAt == "" {
|
|
u.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
if u.Role != RoleAdmin {
|
|
u.db = p.db
|
|
return u.Create(ctx)
|
|
}
|
|
|
|
// Bootstrap admin: serialize count+insert so two setup-secret registers
|
|
// cannot both observe zero admins.
|
|
tx, err := p.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock($1)`, adminRoleLockKey); err != nil {
|
|
return err
|
|
}
|
|
q := sqlc.New(tx)
|
|
n, err := q.CountAdmins(ctx, string(RoleAdmin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
role := RoleAdmin
|
|
if n > 0 {
|
|
role = RoleUser
|
|
}
|
|
if err := q.CreateUser(ctx, sqlc.CreateUserParams{
|
|
ID: u.ID,
|
|
Username: u.Username,
|
|
Name: u.Name,
|
|
PasswordHash: u.PasswordHash,
|
|
Role: string(role),
|
|
CreatedAt: u.CreatedAt,
|
|
}); err != nil {
|
|
return mapUniqueViolation(err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
u.Role = role
|
|
u.db = p.db
|
|
return nil
|
|
}
|
|
|
|
func (p *Postgres) UserByID(ctx context.Context, id string) (*User, error) {
|
|
return UserByID(ctx, p.db, id)
|
|
}
|
|
|
|
func (p *Postgres) UserByUsername(ctx context.Context, username string) (*User, error) {
|
|
return UserByUsername(ctx, p.db, username)
|
|
}
|
|
|
|
func (p *Postgres) ListUsers(ctx context.Context, q ListUsersQuery) ([]User, string, string, error) {
|
|
return ListUsers(ctx, p.db, q)
|
|
}
|
|
|
|
func (p *Postgres) CountAdmins(ctx context.Context) (int, error) {
|
|
return CountAdmins(ctx, p.db)
|
|
}
|
|
|
|
func (p *Postgres) SetUserRole(ctx context.Context, id string, role Role) error {
|
|
u := &User{ID: id, db: p.db}
|
|
return u.SetRole(ctx, role)
|
|
}
|
|
|
|
func (p *Postgres) SaveUserProfile(ctx context.Context, u *User) error {
|
|
u.db = p.db
|
|
return u.SaveProfile(ctx)
|
|
}
|
|
|
|
func (p *Postgres) CreateQuestion(ctx context.Context, q *RankedQuestion) error {
|
|
q.db = p.db
|
|
return q.Create(ctx)
|
|
}
|
|
|
|
func (p *Postgres) GetQuestion(ctx context.Context, id, viewerID string) (*RankedQuestion, error) {
|
|
return GetQuestion(ctx, p.db, id, viewerID)
|
|
}
|
|
|
|
func (p *Postgres) ListHunt(ctx context.Context, huntDate, viewerID string) ([]RankedQuestion, error) {
|
|
return ListHunt(ctx, p.db, huntDate, viewerID)
|
|
}
|
|
|
|
func (p *Postgres) ListQuestionsByAuthor(ctx context.Context, authorID string) ([]RankedQuestion, error) {
|
|
return ListQuestionsByAuthor(ctx, p.db, authorID)
|
|
}
|
|
|
|
func (p *Postgres) ListQuestionsAnsweredBy(ctx context.Context, adminID string) ([]RankedQuestion, error) {
|
|
return ListQuestionsAnsweredBy(ctx, p.db, adminID)
|
|
}
|
|
|
|
func (p *Postgres) HideQuestion(ctx context.Context, id string) error {
|
|
q := &RankedQuestion{ID: id, db: p.db}
|
|
return q.Hide(ctx)
|
|
}
|
|
|
|
func (p *Postgres) GetAnswer(ctx context.Context, questionID string) (*Answer, error) {
|
|
return GetAnswer(ctx, p.db, questionID)
|
|
}
|
|
|
|
func (p *Postgres) UpsertAnswer(ctx context.Context, a *Answer) error {
|
|
a.db = p.db
|
|
return a.Upsert(ctx)
|
|
}
|
|
|
|
func (p *Postgres) Vote(ctx context.Context, userID, questionID string, value int) error {
|
|
return Vote(ctx, p.db, userID, questionID, value)
|
|
}
|