Skip to content

Commit

Permalink
feat: add sorting for tables
Browse files Browse the repository at this point in the history
  • Loading branch information
stonith404 committed Jan 11, 2025
1 parent 61d18a9 commit fd69830
Show file tree
Hide file tree
Showing 27 changed files with 294 additions and 138 deletions.
12 changes: 8 additions & 4 deletions backend/internal/controller/audit_log_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package controller
import (
"github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/middleware"
"github.com/stonith404/pocket-id/backend/internal/utils"
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/stonith404/pocket-id/backend/internal/service"
Expand All @@ -23,12 +23,16 @@ type AuditLogController struct {
}

func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
var sortedPaginationRequest utils.SortedPaginationRequest
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
c.Error(err)
return
}

userID := c.GetString("userID")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))

// Fetch audit logs for the user
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(userID, page, pageSize)
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(userID, sortedPaginationRequest)
if err != nil {
c.Error(err)
return
Expand Down
11 changes: 7 additions & 4 deletions backend/internal/controller/oidc_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/middleware"
"github.com/stonith404/pocket-id/backend/internal/service"
"github.com/stonith404/pocket-id/backend/internal/utils"
"net/http"
"strconv"
"strings"
)

Expand Down Expand Up @@ -153,11 +153,14 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
}

func (oc *OidcController) listClientsHandler(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
c.Error(err)
return
}

clients, pagination, err := oc.oidcService.ListClients(searchTerm, page, pageSize)
clients, pagination, err := oc.oidcService.ListClients(searchTerm, sortedPaginationRequest)
if err != nil {
c.Error(err)
return
Expand Down
11 changes: 7 additions & 4 deletions backend/internal/controller/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/middleware"
"github.com/stonith404/pocket-id/backend/internal/service"
"github.com/stonith404/pocket-id/backend/internal/utils"
"golang.org/x/time/rate"
"net/http"
"strconv"
"time"
)

Expand Down Expand Up @@ -37,11 +37,14 @@ type UserController struct {
}

func (uc *UserController) listUsersHandler(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
c.Error(err)
return
}

users, pagination, err := uc.UserService.ListUsers(searchTerm, page, pageSize)
users, pagination, err := uc.UserService.ListUsers(searchTerm, sortedPaginationRequest)
if err != nil {
c.Error(err)
return
Expand Down
15 changes: 9 additions & 6 deletions backend/internal/controller/user_group_controller.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package controller

import (
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/middleware"
"github.com/stonith404/pocket-id/backend/internal/service"
"github.com/stonith404/pocket-id/backend/internal/utils"
"net/http"
)

func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) {
Expand All @@ -28,16 +27,20 @@ type UserGroupController struct {
}

func (ugc *UserGroupController) list(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
c.Error(err)
return
}

groups, pagination, err := ugc.UserGroupService.List(searchTerm, page, pageSize)
groups, pagination, err := ugc.UserGroupService.List(searchTerm, sortedPaginationRequest)
if err != nil {
c.Error(err)
return
}

// Map the user groups to DTOs. The user count can't be mapped directly, so we have to do it manually.
var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups))
for i, group := range groups {
var groupDto dto.UserGroupDtoWithUserCount
Expand Down
10 changes: 5 additions & 5 deletions backend/internal/model/audit_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
type AuditLog struct {
Base

Event AuditLogEvent
IpAddress string
Country string
City string
UserAgent string
Event AuditLogEvent `sortable:"true"`
IpAddress string `sortable:"true"`
Country string `sortable:"true"`
City string `sortable:"true"`
UserAgent string `sortable:"true"`
UserID string
Data AuditLogData
}
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/model/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (

// Base contains common columns for all tables.
type Base struct {
ID string `gorm:"primaryKey;not null"`
CreatedAt model.DateTime
ID string `gorm:"primaryKey;not null"`
CreatedAt model.DateTime `sortable:"true"`
}

func (b *Base) BeforeCreate(_ *gorm.DB) (err error) {
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/model/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type OidcAuthorizationCode struct {
type OidcClient struct {
Base

Name string
Name string `sortable:"true"`
Secret string
CallbackURLs CallbackURLs
ImageType *string
Expand Down
10 changes: 5 additions & 5 deletions backend/internal/model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
type User struct {
Base

Username string
Email string
FirstName string
LastName string
IsAdmin bool
Username string `sortable:"true"`
Email string `sortable:"true"`
FirstName string `sortable:"true"`
LastName string `sortable:"true"`
IsAdmin bool `sortable:"true"`

CustomClaims []CustomClaim
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/model/user_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package model

type UserGroup struct {
Base
FriendlyName string
Name string `gorm:"unique"`
FriendlyName string `sortable:"true"`
Name string `gorm:"unique" sortable:"true"`
Users []User `gorm:"many2many:user_groups_users;"`
CustomClaims []CustomClaim
}
6 changes: 3 additions & 3 deletions backend/internal/service/audit_log_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
}

// ListAuditLogsForUser retrieves all audit logs for a given user ID
func (s *AuditLogService) ListAuditLogsForUser(userID string, page int, pageSize int) ([]model.AuditLog, utils.PaginationResponse, error) {
func (s *AuditLogService) ListAuditLogsForUser(userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.AuditLog, utils.PaginationResponse, error) {
var logs []model.AuditLog
query := s.db.Model(&model.AuditLog{}).Where("user_id = ?", userID).Order("created_at desc")
query := s.db.Model(&model.AuditLog{}).Where("user_id = ?", userID)

pagination, err := utils.Paginate(page, pageSize, query, &logs)
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
return logs, pagination, err
}

Expand Down
4 changes: 2 additions & 2 deletions backend/internal/service/oidc_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (s *OidcService) GetClient(clientID string) (model.OidcClient, error) {
return client, nil
}

func (s *OidcService) ListClients(searchTerm string, page int, pageSize int) ([]model.OidcClient, utils.PaginationResponse, error) {
func (s *OidcService) ListClients(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.OidcClient, utils.PaginationResponse, error) {
var clients []model.OidcClient

query := s.db.Preload("CreatedBy").Model(&model.OidcClient{})
Expand All @@ -176,7 +176,7 @@ func (s *OidcService) ListClients(searchTerm string, page int, pageSize int) ([]
query = query.Where("name LIKE ?", searchPattern)
}

pagination, err := utils.Paginate(page, pageSize, query, &clients)
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &clients)
if err != nil {
return nil, utils.PaginationResponse{}, err
}
Expand Down
16 changes: 14 additions & 2 deletions backend/internal/service/user_group_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,26 @@ func NewUserGroupService(db *gorm.DB) *UserGroupService {
return &UserGroupService{db: db}
}

func (s *UserGroupService) List(name string, page int, pageSize int) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
func (s *UserGroupService) List(name string, sortedPaginationRequest utils.SortedPaginationRequest) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
query := s.db.Preload("CustomClaims").Model(&model.UserGroup{})

if name != "" {
query = query.Where("name LIKE ?", "%"+name+"%")
}

response, err = utils.Paginate(page, pageSize, query, &groups)
// As userCount is not a column we need to manually sort it
isValidSortDirection := sortedPaginationRequest.Sort.Direction == "asc" || sortedPaginationRequest.Sort.Direction == "desc"
if sortedPaginationRequest.Sort.Column == "userCount" && isValidSortDirection {
query = query.Select("user_groups.*, COUNT(user_groups_users.user_id)").
Joins("LEFT JOIN user_groups_users ON user_groups.id = user_groups_users.user_group_id").
Group("user_groups.id").
Order("COUNT(user_groups_users.user_id) " + sortedPaginationRequest.Sort.Direction)

response, err := utils.Paginate(sortedPaginationRequest.Pagination.Page, sortedPaginationRequest.Pagination.Limit, query, &groups)
return groups, response, err
}

response, err = utils.PaginateAndSort(sortedPaginationRequest, query, &groups)
return groups, response, err
}

Expand Down
4 changes: 2 additions & 2 deletions backend/internal/service/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
return &UserService{db: db, jwtService: jwtService, auditLogService: auditLogService}
}

func (s *UserService) ListUsers(searchTerm string, page int, pageSize int) ([]model.User, utils.PaginationResponse, error) {
func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
var users []model.User
query := s.db.Model(&model.User{})

Expand All @@ -30,7 +30,7 @@ func (s *UserService) ListUsers(searchTerm string, page int, pageSize int) ([]mo
query = query.Where("email LIKE ? OR first_name LIKE ? OR username LIKE ?", searchPattern, searchPattern, searchPattern)
}

pagination, err := utils.Paginate(page, pageSize, query, &users)
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users)
return users, pagination, err
}

Expand Down
36 changes: 33 additions & 3 deletions backend/internal/utils/paging_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package utils

import (
"gorm.io/gorm"
"reflect"
)

type PaginationResponse struct {
Expand All @@ -11,7 +12,36 @@ type PaginationResponse struct {
ItemsPerPage int `json:"itemsPerPage"`
}

func Paginate(page int, pageSize int, db *gorm.DB, result interface{}) (PaginationResponse, error) {
type SortedPaginationRequest struct {
Pagination struct {
Page int `form:"pagination[page]"`
Limit int `form:"pagination[limit]"`
} `form:"pagination"`
Sort struct {
Column string `form:"sort[column]"`
Direction string `form:"sort[direction]"`
} `form:"sort"`
}

func PaginateAndSort(sortedPaginationRequest SortedPaginationRequest, query *gorm.DB, result interface{}) (PaginationResponse, error) {
pagination := sortedPaginationRequest.Pagination
sort := sortedPaginationRequest.Sort

capitalizedSortColumn := CapitalizeFirstLetter(sort.Column)

sortField, sortFieldFound := reflect.TypeOf(result).Elem().Elem().FieldByName(capitalizedSortColumn)
isSortable := sortField.Tag.Get("sortable") == "true"
isValidSortOrder := sort.Direction == "asc" || sort.Direction == "desc"

if sortFieldFound && isSortable && isValidSortOrder {
query = query.Order(CamelCaseToSnakeCase(sort.Column) + " " + sort.Direction)
}

return Paginate(pagination.Page, pagination.Limit, query, result)

}

func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
if page < 1 {
page = 1
}
Expand All @@ -25,11 +55,11 @@ func Paginate(page int, pageSize int, db *gorm.DB, result interface{}) (Paginati
offset := (page - 1) * pageSize

var totalItems int64
if err := db.Count(&totalItems).Error; err != nil {
if err := query.Count(&totalItems).Error; err != nil {
return PaginationResponse{}, err
}

if err := db.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
return PaginationResponse{}, err
}

Expand Down
21 changes: 21 additions & 0 deletions backend/internal/utils/string_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math/big"
"net/url"
"unicode"
)

// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
Expand Down Expand Up @@ -41,3 +42,23 @@ func GetHostnameFromURL(rawURL string) string {
func StringPointer(s string) *string {
return &s
}

func CapitalizeFirstLetter(s string) string {
if s == "" {
return s
}
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}

func CamelCaseToSnakeCase(s string) string {
var result []rune
for i, r := range s {
if unicode.IsUpper(r) && i > 0 {
result = append(result, '_')
}
result = append(result, unicode.ToLower(r))
}
return string(result)
}
Loading

0 comments on commit fd69830

Please sign in to comment.