Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions taco/internal/pagination/pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package pagination

import (
"strconv"

"github.com/labstack/echo/v4"
)

type Params struct {
Page int
PageSize int
}

func (p Params) Offset() int {
if p.Page < 1 {
return 0
}
return (p.Page - 1) * p.PageSize
}

func Parse(c echo.Context, defaultSize, maxSize int) Params {
page := parseInt(c.QueryParam("page"), 1)
size := parseInt(c.QueryParam("page_size"), defaultSize)

if page < 1 {
page = 1
}
if size < 1 {
size = defaultSize
}
if maxSize > 0 && size > maxSize {
size = maxSize
}

return Params{
Page: page,
PageSize: size,
}
}

func parseInt(val string, fallback int) int {
if val == "" {
return fallback
}
if parsed, err := strconv.Atoi(val); err == nil {
return parsed
}
return fallback
}
3 changes: 3 additions & 0 deletions taco/internal/repositories/unit_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ func (r *UnitRepository) List(ctx context.Context, orgID, prefix string) ([]*sto
query = query.Where("name LIKE ?", prefix+"%")
}

// order by name for deterministic paging; tie-break by id
query = query.Order("LOWER(name) ASC").Order("id ASC")

if err := query.Find(&units).Error; err != nil {
return nil, fmt.Errorf("failed to list units: %w", err)
}
Expand Down
27 changes: 21 additions & 6 deletions taco/internal/token_service/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/diggerhq/digger/opentaco/internal/logging"
"github.com/diggerhq/digger/opentaco/internal/pagination"
querytypes "github.com/diggerhq/digger/opentaco/cmd/token_service/query/types"
"github.com/labstack/echo/v4"
)
Expand Down Expand Up @@ -41,6 +42,14 @@ type TokenResponse struct {
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}

type TokenListResponse struct {
Tokens []TokenResponse `json:"tokens"`
Count int `json:"count"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}

// VerifyTokenRequest represents the request to verify a token
type VerifyTokenRequest struct {
Token string `json:"token" validate:"required"`
Expand Down Expand Up @@ -89,25 +98,32 @@ func (h *Handler) CreateToken(c echo.Context) error {
func (h *Handler) ListTokens(c echo.Context) error {
userID := c.QueryParam("user_id")
orgID := c.QueryParam("org_id")
pageParams := pagination.Parse(c, 25, 200)

tokens, err := h.repo.ListTokens(c.Request().Context(), userID, orgID)
tokens, total, err := h.repo.ListTokens(c.Request().Context(), userID, orgID, pageParams.Page, pageParams.PageSize)
if err != nil {
logger := logging.FromContext(c)
logger.Error("Failed to list tokens", "user_id", userID, "org_id", orgID, "error", err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to list tokens"})
}

responses := make([]TokenResponse, len(tokens))
for i, token := range tokens {
responses[i] = toTokenResponseHidden(token) // Hide token hash
responses := make([]TokenResponse, 0, len(tokens))
for _, token := range tokens {
responses = append(responses, toTokenResponseHidden(token)) // Hide token hash
}

// Prevent caching of token list responses
c.Response().Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
c.Response().Header().Set("Pragma", "no-cache")
c.Response().Header().Set("Expires", "0")

return c.JSON(http.StatusOK, responses)
return c.JSON(http.StatusOK, TokenListResponse{
Tokens: responses,
Count: len(responses),
Total: total,
Page: pageParams.Page,
PageSize: pageParams.PageSize,
})
}

// DeleteToken deletes a token by ID
Expand Down Expand Up @@ -206,4 +222,3 @@ func toTokenResponseHidden(token *querytypes.Token) TokenResponse {

return resp
}

26 changes: 19 additions & 7 deletions taco/internal/token_service/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,18 @@ func (r *TokenRepository) CreateToken(ctx context.Context, userID, orgID, name s
return token, nil
}

// ListTokens returns all tokens for a given user ID and org
func (r *TokenRepository) ListTokens(ctx context.Context, userID, orgID string) ([]*types.Token, error) {
// ListTokens returns tokens for a given user ID and org with pagination.
func (r *TokenRepository) ListTokens(ctx context.Context, userID, orgID string, page, pageSize int) ([]*types.Token, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 50
}
offset := (page - 1) * pageSize

var tokens []*types.Token
query := r.db.WithContext(ctx)
query := r.db.WithContext(ctx).Model(&types.Token{})

// Filter by userID if provided
if userID != "" {
Expand All @@ -77,11 +85,16 @@ func (r *TokenRepository) ListTokens(ctx context.Context, userID, orgID string)
query = query.Where("org_id = ?", orgID)
}

if err := query.Order("created_at DESC").Find(&tokens).Error; err != nil {
return nil, fmt.Errorf("failed to list tokens: %w", err)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count tokens: %w", err)
}

return tokens, nil
if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&tokens).Error; err != nil {
return nil, 0, fmt.Errorf("failed to list tokens: %w", err)
}

return tokens, total, nil
}

// DeleteToken deletes a token by ID
Expand Down Expand Up @@ -165,4 +178,3 @@ func hashToken(token string) string {
hash := sha256.Sum256([]byte(token))
return hex.EncodeToString(hash[:])
}

32 changes: 28 additions & 4 deletions taco/internal/unit/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"

Expand All @@ -13,6 +14,7 @@ import (
"github.com/diggerhq/digger/opentaco/internal/deps"
"github.com/diggerhq/digger/opentaco/internal/domain"
"github.com/diggerhq/digger/opentaco/internal/logging"
"github.com/diggerhq/digger/opentaco/internal/pagination"
"github.com/diggerhq/digger/opentaco/internal/query"
"github.com/diggerhq/digger/opentaco/internal/rbac"
"github.com/diggerhq/digger/opentaco/internal/storage"
Expand Down Expand Up @@ -281,6 +283,7 @@ func (h *Handler) ListUnits(c echo.Context) error {
logger := logging.FromContext(c)
ctx := c.Request().Context()
prefix := c.QueryParam("prefix")
pageParams := pagination.Parse(c, 50, 200)

// Get org UUID from domain context (set by middleware for both JWT and webhook routes)
orgCtx, ok := domain.OrgFromContext(ctx)
Expand Down Expand Up @@ -331,15 +334,36 @@ func (h *Handler) ListUnits(c echo.Context) error {
LockInfo: convertLockInfo(u.LockInfo),
})
}
domain.SortUnitsByID(domainUnits)
sort.Slice(domainUnits, func(i, j int) bool {
in := strings.ToLower(domainUnits[i].Name)
jn := strings.ToLower(domainUnits[j].Name)
if in == jn {
return domainUnits[i].ID < domainUnits[j].ID
}
return in < jn
})

total := len(domainUnits)
start := pageParams.Offset()
if start > total {
start = total
}
end := start + pageParams.PageSize
if end > total {
end = total
}
pagedUnits := domainUnits[start:end]

logger.Info("Units listed successfully",
"operation", "list_units",
"count", len(domainUnits),
"count", len(pagedUnits),
)
return c.JSON(http.StatusOK, map[string]interface{}{
"units": domainUnits,
"count": len(domainUnits),
"units": pagedUnits,
"count": len(pagedUnits),
"total": total,
"page": pageParams.Page,
"page_size": pageParams.PageSize,
})
}

Expand Down
12 changes: 9 additions & 3 deletions ui/src/api/statesman_serverFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { createServerFn } from "@tanstack/react-start"
import { createUnit, getUnit, listUnits, getUnitVersions, unlockUnit, lockUnit, getUnitStatus, deleteUnit, downloadLatestState, forcePushState, restoreUnitStateVersion } from "./statesman_units"

export const listUnitsFn = createServerFn({method: 'GET'})
.inputValidator((data : {userId: string, organisationId: string, email: string}) => data)
.inputValidator((data : {userId: string, organisationId: string, email: string, page?: number, pageSize?: number}) => data)
.handler(async ({ data }) => {
const units : any = await listUnits(data.organisationId, data.userId, data.email);
const units : any = await listUnits(
data.organisationId,
data.userId,
data.email,
data.page ?? 1,
data.pageSize ?? 20
);
return units;
})

Expand Down Expand Up @@ -124,4 +130,4 @@ export const deleteUnitFn = createServerFn({method: 'POST'})
.inputValidator((data : {userId: string, organisationId: string, email: string, unitId: string}) => data)
.handler(async ({ data }) => {
await deleteUnit(data.organisationId, data.userId, data.email, data.unitId)
})
})
9 changes: 6 additions & 3 deletions ui/src/api/statesman_units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ function generateRequestId(): string {
return `ui-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}

export async function listUnits(orgId: string, userId: string, email: string) {
const response = await fetch(`${process.env.STATESMAN_BACKEND_URL}/internal/api/units`, {
export async function listUnits(orgId: string, userId: string, email: string, page: number = 1, pageSize: number = 20) {
const url = new URL(`${process.env.STATESMAN_BACKEND_URL}/internal/api/units`);
url.searchParams.set('page', String(page));
url.searchParams.set('page_size', String(pageSize));
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -261,4 +264,4 @@ export async function deleteUnit(orgId: string, userId: string, email: string, u
throw new Error(`Failed to delete unit: ${response.statusText}`);
}

}
}
9 changes: 7 additions & 2 deletions ui/src/api/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ function generateRequestId(): string {
return `ui-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}

export const getTokens = async (organizationId: string, userId: string) => {
const query = new URLSearchParams({ org_id: organizationId, user_id: userId });
export const getTokens = async (organizationId: string, userId: string, page: number = 1, pageSize: number = 20) => {
const query = new URLSearchParams({
org_id: organizationId,
user_id: userId,
page: String(page),
page_size: String(pageSize),
});
const url = `${process.env.TOKENS_SERVICE_BACKEND_URL}/api/v1/tokens?${query.toString()}`;
const response = await fetch(url, {
method: 'GET',
Expand Down
8 changes: 4 additions & 4 deletions ui/src/api/tokens_serverFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { verifyToken } from "./tokens";
import { deleteToken } from "./tokens";

export const getTokensFn = createServerFn({method: 'GET'})
.inputValidator((data: {organizationId: string, userId: string}) => data)
.handler(async ({data: {organizationId, userId}}) => {
return getTokens(organizationId, userId);
.inputValidator((data: {organizationId: string, userId: string, page?: number, pageSize?: number}) => data)
.handler(async ({data: {organizationId, userId, page = 1, pageSize = 20}}) => {
return getTokens(organizationId, userId, page, pageSize);
})

export const createTokenFn = createServerFn({method: 'POST'})
Expand All @@ -25,4 +25,4 @@ export const deleteTokenFn = createServerFn({method: 'POST'})
.inputValidator((data: {organizationId: string, userId: string, tokenId: string}) => data)
.handler(async ({data: {organizationId, userId, tokenId}}) => {
return deleteToken(organizationId, userId, tokenId);
})
})
Loading
Loading