ui(users): record last_login on /setup + sortable headers
This commit is contained in:
+43
-8
@@ -49,14 +49,49 @@ func (s *Store) GetUserByID(ctx context.Context, id string) (*User, error) {
|
||||
return scanUser(row.Scan)
|
||||
}
|
||||
|
||||
// ListUsers returns every user, sorted by username. Used by surfaces
|
||||
// that need to render a user-id → username map (audit log filter,
|
||||
// "ack'd by" projections) and the user-management page.
|
||||
func (s *Store) ListUsers(ctx context.Context) ([]User, error) {
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT id, username, password_hash, role, email, disabled_at,
|
||||
must_change_password, created_at, last_login_at
|
||||
FROM users ORDER BY username`)
|
||||
// UserSort selects the column ListUsers orders by. OrderBy is
|
||||
// allowlisted in usersOrderColumn so callers can't inject SQL via
|
||||
// this field. Empty / unknown OrderBy falls back to "username".
|
||||
type UserSort struct {
|
||||
OrderBy string // "username" | "email" | "role" | "last_login_at"
|
||||
OrderAsc bool // false = DESC; true = ASC
|
||||
}
|
||||
|
||||
// usersOrderColumn validates s.OrderBy and returns the SQL fragment.
|
||||
// last_login_at gets a NULL-tail trick so users who've never logged
|
||||
// in sort to the bottom regardless of asc/desc — matches operator
|
||||
// intuition ("show me real activity" not "show me NULLs first").
|
||||
func usersOrderColumn(col string, asc bool) string {
|
||||
dir := "DESC"
|
||||
if asc {
|
||||
dir = "ASC"
|
||||
}
|
||||
switch col {
|
||||
case "email":
|
||||
return fmt.Sprintf("email IS NULL, email %s, username", dir)
|
||||
case "role":
|
||||
return fmt.Sprintf("role %s, username", dir)
|
||||
case "last_login_at":
|
||||
return fmt.Sprintf("last_login_at IS NULL, last_login_at %s, username", dir)
|
||||
default: // username (and unknown)
|
||||
return fmt.Sprintf("username %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
// ListUsers returns users sorted per UserSort. Default (zero value)
|
||||
// is username ASC. Used by the user-management page (sort headers)
|
||||
// and by surfaces that need a user-id → username map (audit log
|
||||
// filter, "ack'd by" projections) — those callers pass UserSort{}.
|
||||
func (s *Store) ListUsers(ctx context.Context, sort UserSort) ([]User, error) {
|
||||
asc := sort.OrderAsc
|
||||
if sort.OrderBy == "" {
|
||||
// Default: username ASC (alphabetical), matching pre-sort behaviour.
|
||||
asc = true
|
||||
}
|
||||
q := `SELECT id, username, password_hash, role, email, disabled_at,
|
||||
must_change_password, created_at, last_login_at
|
||||
FROM users ORDER BY ` + usersOrderColumn(sort.OrderBy, asc)
|
||||
rows, err := s.db.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list users: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user