package store import ( "context" "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)) } 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 } func (s *Store) DeleteUser(ctx context.Context, id int64) error { _, err := s.db.Exec(ctx, `DELETE FROM users WHERE id=$1`, id) return err } // UserWithBlog is a row for the admin overview. type UserWithBlog struct { User BlogID *int64 Subdomain *string BlogTitle *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, b.title 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, &r.BlogTitle); err != nil { return nil, err } out = append(out, r) } return out, rows.Err() }