From 4c6c199d72d9d985a97b0a26113886e4e0fadac2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Nov 2025 04:06:49 +0000 Subject: [PATCH 1/2] fix(api): handle NULL columns in repositories query to prevent silent failures Add COALESCE for nullable columns (name, branch, auth_type, template_count, status) in ListRepositories handler. Without this, rows with NULL values fail to scan and are silently skipped, causing repositories to not appear in the UI despite existing in the database. --- api/internal/api/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/internal/api/handlers.go b/api/internal/api/handlers.go index 7633cb9f..27b1b3af 100644 --- a/api/internal/api/handlers.go +++ b/api/internal/api/handlers.go @@ -1400,7 +1400,7 @@ func (h *Handler) ListRepositories(c *gin.Context) { ctx := c.Request.Context() rows, err := h.db.DB().QueryContext(ctx, ` - SELECT id, name, url, branch, COALESCE(type, 'template'), auth_type, last_sync, template_count, status, error_message, created_at, updated_at + SELECT id, COALESCE(name, ''), url, COALESCE(branch, 'main'), COALESCE(type, 'template'), COALESCE(auth_type, 'none'), last_sync, COALESCE(template_count, 0), COALESCE(status, 'pending'), error_message, created_at, updated_at FROM repositories ORDER BY name ASC `) From 2499f79edd79ae9893227d1211ba2efc5c854fbc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Nov 2025 04:12:10 +0000 Subject: [PATCH 2/2] fix(api): handle NULL columns and improve user creation validation - Add COALESCE for nullable columns in ListUsers query (full_name, role, provider, active) to prevent scan failures on NULL values - Add COALESCE for type column in ListGroups query to handle NULL values - Move password validation from binding tag to handler for conditional validation (only required for local auth) - Add explicit password validation in CreateUser handler with proper error messages These fixes ensure admin user appears in users list and all_users group appears in groups list by preventing silent scan failures on NULL columns. --- api/internal/db/groups.go | 2 +- api/internal/db/users.go | 2 +- api/internal/handlers/users.go | 18 ++++++++++++++++++ api/internal/models/user.go | 6 +++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/internal/db/groups.go b/api/internal/db/groups.go index bc9a472f..653262dc 100644 --- a/api/internal/db/groups.go +++ b/api/internal/db/groups.go @@ -193,7 +193,7 @@ func (g *GroupDB) GetGroupByName(ctx context.Context, name string) (*models.Grou func (g *GroupDB) ListGroups(ctx context.Context, groupType string, parentID *string) ([]*models.Group, error) { query := ` SELECT g.id, g.name, COALESCE(g.display_name, '') as display_name, - COALESCE(g.description, '') as description, g.type, g.parent_id, + COALESCE(g.description, '') as description, COALESCE(g.type, 'team'), g.parent_id, g.created_at, g.updated_at, COUNT(gm.user_id) as member_count FROM groups g LEFT JOIN group_memberships gm ON g.id = gm.group_id diff --git a/api/internal/db/users.go b/api/internal/db/users.go index e9187c84..1891ade3 100644 --- a/api/internal/db/users.go +++ b/api/internal/db/users.go @@ -269,7 +269,7 @@ func (u *UserDB) GetUserByEmail(ctx context.Context, email string) (*models.User // ListUsers retrieves all users with optional filtering func (u *UserDB) ListUsers(ctx context.Context, role, provider string, activeOnly bool) ([]*models.User, error) { query := ` - SELECT id, username, email, full_name, role, provider, active, created_at, updated_at, last_login + SELECT id, username, email, COALESCE(full_name, ''), COALESCE(role, 'user'), COALESCE(provider, 'local'), COALESCE(active, true), created_at, updated_at, last_login FROM users WHERE 1=1 ` diff --git a/api/internal/handlers/users.go b/api/internal/handlers/users.go index c8a6eb87..85181c2d 100644 --- a/api/internal/handlers/users.go +++ b/api/internal/handlers/users.go @@ -167,6 +167,24 @@ func (h *UserHandler) CreateUser(c *gin.Context) { return } + // Validate password for local auth users + if req.Provider == "" || req.Provider == "local" { + if req.Password == "" { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Invalid request", + Message: "Password is required for local authentication", + }) + return + } + if len(req.Password) < 8 { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Invalid request", + Message: "Password must be at least 8 characters", + }) + return + } + } + user, err := h.userDB.CreateUser(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse{ diff --git a/api/internal/models/user.go b/api/internal/models/user.go index 711819db..183c737e 100644 --- a/api/internal/models/user.go +++ b/api/internal/models/user.go @@ -414,9 +414,9 @@ type CreateUserRequest struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required,email"` FullName string `json:"fullName" binding:"required"` - Password string `json:"password" binding:"required,min=8"` // Only for local auth - Role string `json:"role"` // user, admin, operator - Provider string `json:"provider"` // local, saml, oidc + Password string `json:"password"` // Required for local auth, validated in handler + Role string `json:"role"` // user, admin, operator + Provider string `json:"provider"` // local, saml, oidc } // UpdateUserRequest represents a request to update an existing user.