Address PR review: graceful shutdown, Role/NewUser, drop SQLite.

This commit is contained in:
2026-08-21 23:40:59 -07:00
parent d167b9216a
commit 3391cce7bd
13 changed files with 152 additions and 337 deletions
+22 -57
View File
@@ -9,14 +9,12 @@ import (
"github.com/alexedwards/scs/v2"
"github.com/google/uuid"
_ "modernc.org/sqlite"
"plumber/internal/pacific"
)
type Store struct {
db *sql.DB
dialect string
sessionStore scs.Store
sessionStopper sessionStopper
}
@@ -25,7 +23,7 @@ type User struct {
ID string
Username string
Name string
Role string
Role Role
AvatarURL string
State string
CreatedAt string
@@ -33,7 +31,7 @@ type User struct {
}
func (u *User) Admin() bool {
return u != nil && u.Role == "admin"
return u != nil && u.Role == RoleAdmin
}
type RankedQuestion struct {
@@ -60,42 +58,6 @@ type Answer struct {
UpdatedAt string
}
func Open(path, schema string) (*Store, error) {
return openSQLite(path, schema, 5*time.Minute)
}
// OpenWithoutSessionCleanup opens SQLite without a session cleanup goroutine (for tests).
func OpenWithoutSessionCleanup(path, schema string) (*Store, error) {
return openSQLite(path, schema, 0)
}
func openSQLite(path, schema string, sessionCleanup time.Duration) (*Store, error) {
dsn := path
if !strings.Contains(dsn, "?") {
dsn += "?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)"
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
if _, err := db.Exec(schema); err != nil {
_ = db.Close()
return nil, fmt.Errorf("apply schema: %w", err)
}
if err := applySessionsSchema(db, dialectSQLite); err != nil {
_ = db.Close()
return nil, fmt.Errorf("apply sessions schema: %w", err)
}
if err := migrateUserProfileColumns(db, dialectSQLite); err != nil {
_ = db.Close()
return nil, fmt.Errorf("migrate profile columns: %w", err)
}
st := &Store{db: db, dialect: dialectSQLite}
st.initSessionStore(sessionCleanup)
return st, nil
}
func (s *Store) Close() error {
if s.sessionStopper != nil {
s.sessionStopper.StopCleanup()
@@ -104,22 +66,21 @@ func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) CreateUser(ctx context.Context, username, passwordHash string, asAdmin bool) (*User, error) {
username = NormalizeUsername(username)
role := "user"
if asAdmin {
role = "admin"
func (s *Store) CreateUser(ctx context.Context, nu NewUser) (*User, error) {
if nu.Role != RoleUser && nu.Role != RoleAdmin {
return nil, fmt.Errorf("invalid role")
}
username := NormalizeUsername(nu.Username)
u := &User{
ID: uuid.NewString(),
Username: username,
Name: username,
Role: role,
PasswordHash: passwordHash,
Role: nu.Role,
PasswordHash: nu.PasswordHash,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
_, err := s.db.ExecContext(ctx, s.q(`INSERT INTO users (id, username, name, password_hash, role, avatar_url, state, created_at) VALUES (?, ?, ?, ?, ?, '', '', ?)`),
u.ID, u.Username, u.Name, u.PasswordHash, u.Role, u.CreatedAt)
u.ID, u.Username, u.Name, u.PasswordHash, string(u.Role), u.CreatedAt)
if err != nil {
return nil, err
}
@@ -128,7 +89,7 @@ func (s *Store) CreateUser(ctx context.Context, username, passwordHash string, a
func (s *Store) CountAdmins(ctx context.Context) (int, error) {
var n int
err := s.db.QueryRowContext(ctx, s.q(`SELECT COUNT(*) FROM users WHERE role = ?`), "admin").Scan(&n)
err := s.db.QueryRowContext(ctx, s.q(`SELECT COUNT(*) FROM users WHERE role = ?`), string(RoleAdmin)).Scan(&n)
return n, err
}
@@ -141,16 +102,18 @@ func (s *Store) ListUsers(ctx context.Context) ([]User, error) {
var out []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Username, &u.Name, &u.Role, &u.AvatarURL, &u.State, &u.CreatedAt); err != nil {
var role string
if err := rows.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt); err != nil {
return nil, err
}
u.Role = Role(role)
out = append(out, u)
}
return out, rows.Err()
}
func (s *Store) SetRole(ctx context.Context, userID, role string) error {
if role != "user" && role != "admin" {
func (s *Store) SetRole(ctx context.Context, userID string, role Role) error {
if role != RoleUser && role != RoleAdmin {
return fmt.Errorf("invalid role")
}
tx, err := s.db.BeginTx(ctx, nil)
@@ -164,16 +127,16 @@ func (s *Store) SetRole(ctx context.Context, userID, role string) error {
if err != nil {
return err
}
if current == "admin" && role == "user" {
if Role(current) == RoleAdmin && role == RoleUser {
var n int
if err := tx.QueryRowContext(ctx, s.q(`SELECT COUNT(*) FROM users WHERE role = ?`), "admin").Scan(&n); err != nil {
if err := tx.QueryRowContext(ctx, s.q(`SELECT COUNT(*) FROM users WHERE role = ?`), string(RoleAdmin)).Scan(&n); err != nil {
return err
}
if n <= 1 {
return ErrLastAdmin
}
}
res, err := tx.ExecContext(ctx, s.q(`UPDATE users SET role = ? WHERE id = ?`), role, userID)
res, err := tx.ExecContext(ctx, s.q(`UPDATE users SET role = ? WHERE id = ?`), string(role), userID)
if err != nil {
return err
}
@@ -197,15 +160,17 @@ func (s *Store) UserByUsername(ctx context.Context, username string) (*User, err
func scanUser(row *sql.Row, withSecrets bool) (*User, error) {
var u User
var role string
var err error
if withSecrets {
err = row.Scan(&u.ID, &u.Username, &u.Name, &u.Role, &u.AvatarURL, &u.State, &u.CreatedAt, &u.PasswordHash)
err = row.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt, &u.PasswordHash)
} else {
err = row.Scan(&u.ID, &u.Username, &u.Name, &u.Role, &u.AvatarURL, &u.State, &u.CreatedAt)
err = row.Scan(&u.ID, &u.Username, &u.Name, &role, &u.AvatarURL, &u.State, &u.CreatedAt)
}
if err != nil {
return nil, err
}
u.Role = Role(role)
return &u, nil
}