package store import ( "context" "errors" "time" ) const ( RoleSuperadmin = "superadmin" RoleBlogger = "blogger" ) type User struct { ID int64 Username string PasswordHash string Role string Disabled bool TokenVersion int CreatedAt time.Time } func (u *User) IsSuperadmin() bool { return u.Role == RoleSuperadmin } const userCols = `id, username, password_hash, role, disabled, token_version, created_at` func scanUser(row interface{ Scan(...any) error }) (*User, error) { var u User err := row.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.Disabled, &u.TokenVersion, &u.CreatedAt) if err != nil { return nil, wrap(err) } return &u, nil } func (s *Store) CreateUser(ctx context.Context, username, passwordHash, role string) (*User, error) { row := s.db.QueryRow(ctx, `INSERT INTO users (username, password_hash, role) VALUES ($1,$2,$3) RETURNING `+userCols, username, passwordHash, role) return scanUser(row) } func (s *Store) UserByID(ctx context.Context, id int64) (*User, error) { return scanUser(s.db.QueryRow(ctx, `SELECT `+userCols+` FROM users WHERE id=$1`, id)) } func (s *Store) UserByUsername(ctx context.Context, username string) (*User, error) { return scanUser(s.db.QueryRow(ctx, `SELECT `+userCols+` FROM users WHERE username=$1`, username)) } // FirstSuperadmin returns the oldest superadmin account. func (s *Store) FirstSuperadmin(ctx context.Context) (*User, error) { return scanUser(s.db.QueryRow(ctx, `SELECT `+userCols+` FROM users WHERE role=$1 ORDER BY id LIMIT 1`, RoleSuperadmin)) } func (s *Store) CountSuperadmins(ctx context.Context) (int, error) { var n int err := s.db.QueryRow(ctx, `SELECT count(*) FROM users WHERE role=$1`, RoleSuperadmin).Scan(&n) return n, err } // SetPassword replaces the hash and bumps token_version so existing sessions die. func (s *Store) SetPassword(ctx context.Context, id int64, passwordHash string) error { _, err := s.db.Exec(ctx, `UPDATE users SET password_hash=$2, token_version=token_version+1 WHERE id=$1`, id, passwordHash) return err } func (s *Store) SetUserDisabled(ctx context.Context, id int64, disabled bool) error { _, err := s.db.Exec(ctx, `UPDATE users SET disabled=$2, token_version=token_version+1 WHERE id=$1`, id, disabled) return err } // DeleteUser removes the user, their registry row and their blog database. // The registry goes first: a stale blog database is harmless and can be // dropped by hand, a registry row without a database is not. func (s *Store) DeleteUser(ctx context.Context, id int64) error { b, err := s.BlogByOwner(ctx, id) if err != nil && !errors.Is(err, ErrNotFound) { return err } if _, err := s.db.Exec(ctx, `DELETE FROM users WHERE id=$1`, id); err != nil { return err } if b != nil { return s.cluster.DropBlogDB(ctx, b.DBName) } return nil } // UserWithBlog is a row for the admin overview. type UserWithBlog struct { User BlogID *int64 Subdomain *string } func (s *Store) ListUsers(ctx context.Context) ([]UserWithBlog, error) { rows, err := s.db.Query(ctx, `SELECT u.id, u.username, u.password_hash, u.role, u.disabled, u.token_version, u.created_at, b.id, b.subdomain FROM users u LEFT JOIN blogs b ON b.owner_id = u.id ORDER BY u.role, u.username`) if err != nil { return nil, err } defer rows.Close() var out []UserWithBlog for rows.Next() { var r UserWithBlog if err := rows.Scan(&r.ID, &r.Username, &r.PasswordHash, &r.Role, &r.Disabled, &r.TokenVersion, &r.CreatedAt, &r.BlogID, &r.Subdomain); err != nil { return nil, err } out = append(out, r) } return out, rows.Err() }