package iam import ( "database/sql" "fmt" "net/http" "strings" "github.com/atlasos/calypso/internal/common/config" "github.com/atlasos/calypso/internal/common/database" "github.com/atlasos/calypso/internal/common/logger" "github.com/atlasos/calypso/internal/common/password" "github.com/gin-gonic/gin" ) // Handler handles IAM-related requests type Handler struct { db *database.DB config *config.Config logger *logger.Logger } // NewHandler creates a new IAM handler func NewHandler(db *database.DB, cfg *config.Config, log *logger.Logger) *Handler { return &Handler{ db: db, config: cfg, logger: log, } } // ListUsers lists all users func (h *Handler) ListUsers(c *gin.Context) { query := ` SELECT id, username, email, full_name, is_active, is_system, created_at, updated_at, last_login_at FROM users ORDER BY username ` rows, err := h.db.Query(query) if err != nil { h.logger.Error("Failed to list users", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list users"}) return } defer rows.Close() var users []map[string]interface{} for rows.Next() { var u struct { ID string Username string Email string FullName string IsActive bool IsSystem bool CreatedAt string UpdatedAt string LastLoginAt *string } if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.FullName, &u.IsActive, &u.IsSystem, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt); err != nil { h.logger.Error("Failed to scan user", "error", err) continue } roles, _ := GetUserRoles(h.db, u.ID) permissions, _ := GetUserPermissions(h.db, u.ID) groups, _ := GetUserGroups(h.db, u.ID) users = append(users, map[string]interface{}{ "id": u.ID, "username": u.Username, "email": u.Email, "full_name": u.FullName, "is_active": u.IsActive, "is_system": u.IsSystem, "roles": roles, "permissions": permissions, "groups": groups, "created_at": u.CreatedAt, "updated_at": u.UpdatedAt, "last_login_at": u.LastLoginAt, }) } c.JSON(http.StatusOK, gin.H{"users": users}) } // GetUser retrieves a single user // Permission: User can view their own profile, or admin can view any profile func (h *Handler) GetUser(c *gin.Context) { userID := c.Param("id") // Get current authenticated user from context authUser, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"}) return } currentUser, ok := authUser.(*User) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user context"}) return } // Check permission: user can view own profile, or admin can view any profile canView := false if currentUser.ID == userID { canView = true } else { // Check if current user is admin roles, err := GetUserRoles(h.db, currentUser.ID) if err == nil { for _, role := range roles { if role == "admin" { canView = true break } } } } if !canView { c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"}) return } user, err := GetUserByID(h.db, userID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } roles, _ := GetUserRoles(h.db, userID) permissions, _ := GetUserPermissions(h.db, userID) groups, _ := GetUserGroups(h.db, userID) c.JSON(http.StatusOK, gin.H{ "id": user.ID, "username": user.Username, "email": user.Email, "full_name": user.FullName, "is_active": user.IsActive, "is_system": user.IsSystem, "roles": roles, "permissions": permissions, "groups": groups, "created_at": user.CreatedAt, "updated_at": user.UpdatedAt, "last_login_at": user.LastLoginAt, }) } // CreateUser creates a new user func (h *Handler) CreateUser(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` FullName string `json:"full_name"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Hash password with Argon2id passwordHash, err := password.HashPassword(req.Password, h.config.Auth.Argon2Params) if err != nil { h.logger.Error("Failed to hash password", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) return } query := ` INSERT INTO users (username, email, password_hash, full_name) VALUES ($1, $2, $3, $4) RETURNING id ` var userID string err = h.db.QueryRow(query, req.Username, req.Email, passwordHash, req.FullName).Scan(&userID) if err != nil { h.logger.Error("Failed to create user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) return } h.logger.Info("User created", "user_id", userID, "username", req.Username) c.JSON(http.StatusCreated, gin.H{"id": userID, "username": req.Username}) } // UpdateUser updates an existing user func (h *Handler) UpdateUser(c *gin.Context) { userID := c.Param("id") var req struct { Email *string `json:"email"` FullName *string `json:"full_name"` IsActive *bool `json:"is_active"` Roles *[]string `json:"roles"` Groups *[]string `json:"groups"` } if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Failed to bind JSON", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } h.logger.Info("UpdateUser request received", "user_id", userID, "email", req.Email, "full_name", req.FullName, "is_active", req.IsActive, "roles", req.Roles, "groups", req.Groups) // Build update query dynamically updates := []string{"updated_at = NOW()"} args := []interface{}{} argPos := 1 if req.Email != nil { updates = append(updates, fmt.Sprintf("email = $%d", argPos)) args = append(args, *req.Email) argPos++ } if req.FullName != nil { updates = append(updates, fmt.Sprintf("full_name = $%d", argPos)) args = append(args, *req.FullName) argPos++ } if req.IsActive != nil { updates = append(updates, fmt.Sprintf("is_active = $%d", argPos)) args = append(args, *req.IsActive) argPos++ } // Allow update if roles or groups are provided, even if no other fields are updated if len(updates) == 1 && req.Roles == nil && req.Groups == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } // Update user basic info if there are any changes if len(updates) > 1 { args = append(args, userID) query := "UPDATE users SET " + strings.Join(updates, ", ") + fmt.Sprintf(" WHERE id = $%d", argPos) _, err := h.db.Exec(query, args...) if err != nil { h.logger.Error("Failed to update user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update user"}) return } } // Get current user ID from context for audit authUser, _ := c.Get("user") currentUser := authUser.(*User) // Update roles if provided if req.Roles != nil { h.logger.Info("Updating user roles", "user_id", userID, "roles", *req.Roles) currentRoles, err := GetUserRoles(h.db, userID) if err != nil { h.logger.Error("Failed to get current roles for user", "user_id", userID, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process user roles"}) return } rolesToAdd := []string{} rolesToRemove := []string{} // Find roles to add for _, newRole := range *req.Roles { found := false for _, currentRole := range currentRoles { if newRole == currentRole { found = true break } } if !found { rolesToAdd = append(rolesToAdd, newRole) } } // Find roles to remove for _, currentRole := range currentRoles { found := false for _, newRole := range *req.Roles { if currentRole == newRole { found = true break } } if !found { rolesToRemove = append(rolesToRemove, currentRole) } } // Add new roles for _, roleName := range rolesToAdd { roleID, err := GetRoleIDByName(h.db, roleName) if err != nil { if err == sql.ErrNoRows { h.logger.Warn("Attempted to add non-existent role to user", "user_id", userID, "role_name", roleName) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("role '%s' not found", roleName)}) return } h.logger.Error("Failed to get role ID by name", "role_name", roleName, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process roles"}) return } if err := AddUserRole(h.db, userID, roleID, currentUser.ID); err != nil { h.logger.Error("Failed to add role to user", "user_id", userID, "role_id", roleID, "error", err) // Don't return early, continue with other roles continue } h.logger.Info("Role added to user", "user_id", userID, "role_name", roleName) } // Remove old roles for _, roleName := range rolesToRemove { roleID, err := GetRoleIDByName(h.db, roleName) if err != nil { // This case should be rare, but handle it defensively h.logger.Error("Failed to get role ID for role to be removed", "role_name", roleName, "error", err) continue } if err := RemoveUserRole(h.db, userID, roleID); err != nil { h.logger.Error("Failed to remove role from user", "user_id", userID, "role_id", roleID, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove role"}) return } h.logger.Info("Role removed from user", "user_id", userID, "role_name", roleName) } } // Update groups if provided if req.Groups != nil { h.logger.Info("Updating user groups", "user_id", userID, "groups", *req.Groups) currentGroups, err := GetUserGroups(h.db, userID) if err != nil { h.logger.Error("Failed to get current groups for user", "user_id", userID, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process user groups"}) return } groupsToAdd := []string{} groupsToRemove := []string{} // Find groups to add for _, newGroup := range *req.Groups { found := false for _, currentGroup := range currentGroups { if newGroup == currentGroup { found = true break } } if !found { groupsToAdd = append(groupsToAdd, newGroup) } } // Find groups to remove for _, currentGroup := range currentGroups { found := false for _, newGroup := range *req.Groups { if currentGroup == newGroup { found = true break } } if !found { groupsToRemove = append(groupsToRemove, currentGroup) } } // Add new groups for _, groupName := range groupsToAdd { group, err := GetGroupByName(h.db, groupName) if err != nil { if err == sql.ErrNoRows { h.logger.Warn("Attempted to add user to non-existent group", "user_id", userID, "group_name", groupName) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("group '%s' not found", groupName)}) return } h.logger.Error("Failed to get group by name", "group_name", groupName, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process groups"}) return } if err := AddUserToGroup(h.db, userID, group.ID, currentUser.ID); err != nil { h.logger.Error("Failed to add user to group", "user_id", userID, "group_id", group.ID, "error", err) // Don't return early, continue with other groups continue } h.logger.Info("User added to group", "user_id", userID, "group_name", groupName) } // Remove old groups for _, groupName := range groupsToRemove { group, err := GetGroupByName(h.db, groupName) if err != nil { // This case should be rare, but handle it defensively h.logger.Error("Failed to get group ID for group to be removed", "group_name", groupName, "error", err) continue } if err := RemoveUserFromGroup(h.db, userID, group.ID); err != nil { h.logger.Error("Failed to remove user from group", "user_id", userID, "group_id", group.ID, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove user from group"}) return } h.logger.Info("User removed from group", "user_id", userID, "group_name", groupName) } } h.logger.Info("User updated", "user_id", userID) c.JSON(http.StatusOK, gin.H{"message": "user updated successfully"}) } // DeleteUser deletes a user func (h *Handler) DeleteUser(c *gin.Context) { userID := c.Param("id") // Check if user is system user var isSystem bool err := h.db.QueryRow("SELECT is_system FROM users WHERE id = $1", userID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if isSystem { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system user"}) return } _, err = h.db.Exec("DELETE FROM users WHERE id = $1", userID) if err != nil { h.logger.Error("Failed to delete user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"}) return } h.logger.Info("User deleted", "user_id", userID) c.JSON(http.StatusOK, gin.H{"message": "user deleted successfully"}) } // ListGroups lists all groups func (h *Handler) ListGroups(c *gin.Context) { query := ` SELECT g.id, g.name, g.description, g.is_system, g.created_at, g.updated_at, COUNT(DISTINCT ug.user_id) as user_count, COUNT(DISTINCT gr.role_id) as role_count FROM groups g LEFT JOIN user_groups ug ON g.id = ug.group_id LEFT JOIN group_roles gr ON g.id = gr.group_id GROUP BY g.id, g.name, g.description, g.is_system, g.created_at, g.updated_at ORDER BY g.name ` rows, err := h.db.Query(query) if err != nil { h.logger.Error("Failed to list groups", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list groups"}) return } defer rows.Close() var groups []map[string]interface{} for rows.Next() { var g struct { ID string Name string Description sql.NullString IsSystem bool CreatedAt string UpdatedAt string UserCount int RoleCount int } if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.IsSystem, &g.CreatedAt, &g.UpdatedAt, &g.UserCount, &g.RoleCount); err != nil { h.logger.Error("Failed to scan group", "error", err) continue } groups = append(groups, map[string]interface{}{ "id": g.ID, "name": g.Name, "description": g.Description.String, "is_system": g.IsSystem, "user_count": g.UserCount, "role_count": g.RoleCount, "created_at": g.CreatedAt, "updated_at": g.UpdatedAt, }) } c.JSON(http.StatusOK, gin.H{"groups": groups}) } // GetGroup retrieves a single group func (h *Handler) GetGroup(c *gin.Context) { groupID := c.Param("id") group, err := GetGroupByID(h.db, groupID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } users, _ := GetGroupUsers(h.db, groupID) roles, _ := GetGroupRoles(h.db, groupID) c.JSON(http.StatusOK, gin.H{ "id": group.ID, "name": group.Name, "description": group.Description, "is_system": group.IsSystem, "user_count": group.UserCount, "role_count": group.RoleCount, "users": users, "roles": roles, "created_at": group.CreatedAt, "updated_at": group.UpdatedAt, }) } // CreateGroup creates a new group func (h *Handler) CreateGroup(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid request to create group", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()}) return } // Trim whitespace req.Name = strings.TrimSpace(req.Name) if req.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } // Handle empty description description := strings.TrimSpace(req.Description) if description == "" { description = "" } query := ` INSERT INTO groups (name, description) VALUES ($1, $2) RETURNING id ` var groupID string err := h.db.QueryRow(query, req.Name, description).Scan(&groupID) if err != nil { // Check if it's a unique constraint violation if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique constraint") { h.logger.Error("Group name already exists", "name", req.Name, "error", err) c.JSON(http.StatusConflict, gin.H{"error": "group name already exists"}) return } h.logger.Error("Failed to create group", "error", err, "name", req.Name) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create group: " + err.Error()}) return } h.logger.Info("Group created successfully", "group_id", groupID, "name", req.Name) c.JSON(http.StatusCreated, gin.H{"id": groupID, "name": req.Name}) } // UpdateGroup updates an existing group func (h *Handler) UpdateGroup(c *gin.Context) { groupID := c.Param("id") // Check if group is system group var isSystem bool err := h.db.QueryRow("SELECT is_system FROM groups WHERE id = $1", groupID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } var req struct { Name string `json:"name"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Build update query dynamically var updates []string var args []interface{} argIndex := 1 if req.Name != "" { updates = append(updates, fmt.Sprintf("name = $%d", argIndex)) args = append(args, req.Name) argIndex++ } if req.Description != "" { updates = append(updates, fmt.Sprintf("description = $%d", argIndex)) args = append(args, req.Description) argIndex++ } if len(updates) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } updates = append(updates, "updated_at = NOW()") args = append(args, groupID) query := fmt.Sprintf("UPDATE groups SET %s WHERE id = $%d", strings.Join(updates, ", "), argIndex) _, err = h.db.Exec(query, args...) if err != nil { h.logger.Error("Failed to update group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update group"}) return } h.logger.Info("Group updated", "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "group updated successfully"}) } // DeleteGroup deletes a group func (h *Handler) DeleteGroup(c *gin.Context) { groupID := c.Param("id") // Check if group is system group var isSystem bool err := h.db.QueryRow("SELECT is_system FROM groups WHERE id = $1", groupID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } if isSystem { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system group"}) return } _, err = h.db.Exec("DELETE FROM groups WHERE id = $1", groupID) if err != nil { h.logger.Error("Failed to delete group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete group"}) return } h.logger.Info("Group deleted", "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "group deleted successfully"}) } // AddUserToGroup adds a user to a group func (h *Handler) AddUserToGroup(c *gin.Context) { groupID := c.Param("id") var req struct { UserID string `json:"user_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get current user ID from context authUser, _ := c.Get("user") currentUser := authUser.(*User) err := AddUserToGroup(h.db, req.UserID, groupID, currentUser.ID) if err != nil { h.logger.Error("Failed to add user to group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add user to group"}) return } h.logger.Info("User added to group", "user_id", req.UserID, "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "user added to group successfully"}) } // RemoveUserFromGroup removes a user from a group func (h *Handler) RemoveUserFromGroup(c *gin.Context) { groupID := c.Param("id") userID := c.Param("user_id") err := RemoveUserFromGroup(h.db, userID, groupID) if err != nil { h.logger.Error("Failed to remove user from group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove user from group"}) return } h.logger.Info("User removed from group", "user_id", userID, "group_id", groupID) c.JSON(http.StatusOK, gin.H{"message": "user removed from group successfully"}) } // AssignRoleToUser assigns a role to a user func (h *Handler) AssignRoleToUser(c *gin.Context) { userID := c.Param("id") var req struct { RoleName string `json:"role_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get role ID by name roleID, err := GetRoleIDByName(h.db, req.RoleName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } h.logger.Error("Failed to get role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign role"}) return } // Get current user ID from context authUser, _ := c.Get("user") currentUser := authUser.(*User) err = AddUserRole(h.db, userID, roleID, currentUser.ID) if err != nil { h.logger.Error("Failed to assign role to user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign role"}) return } h.logger.Info("Role assigned to user", "user_id", userID, "role", req.RoleName) c.JSON(http.StatusOK, gin.H{"message": "role assigned successfully"}) } // RemoveRoleFromUser removes a role from a user func (h *Handler) RemoveRoleFromUser(c *gin.Context) { userID := c.Param("id") // Get role_name from query parameter roleName := c.Query("role_name") if roleName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "role_name is required"}) return } // Get role ID by name roleID, err := GetRoleIDByName(h.db, roleName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } h.logger.Error("Failed to get role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove role"}) return } err = RemoveUserRole(h.db, userID, roleID) if err != nil { h.logger.Error("Failed to remove role from user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove role"}) return } h.logger.Info("Role removed from user", "user_id", userID, "role", roleName) c.JSON(http.StatusOK, gin.H{"message": "role removed successfully"}) } // AssignGroupToUser assigns a group to a user func (h *Handler) AssignGroupToUser(c *gin.Context) { userID := c.Param("id") var req struct { GroupName string `json:"group_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get group ID by name group, err := GetGroupByName(h.db, req.GroupName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } h.logger.Error("Failed to get group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign group"}) return } groupID := group.ID // Get current user ID from context authUser, _ := c.Get("user") currentUser := authUser.(*User) err = AddUserToGroup(h.db, userID, groupID, currentUser.ID) if err != nil { h.logger.Error("Failed to assign group to user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign group"}) return } h.logger.Info("Group assigned to user", "user_id", userID, "group", req.GroupName) c.JSON(http.StatusOK, gin.H{"message": "group assigned successfully"}) } // RemoveGroupFromUser removes a group from a user func (h *Handler) RemoveGroupFromUser(c *gin.Context) { userID := c.Param("id") // Get group_name from query parameter groupName := c.Query("group_name") if groupName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "group_name is required"}) return } // Get group ID by name group, err := GetGroupByName(h.db, groupName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "group not found"}) return } h.logger.Error("Failed to get group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove group"}) return } groupID := group.ID err = RemoveUserFromGroup(h.db, userID, groupID) if err != nil { h.logger.Error("Failed to remove group from user", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove group"}) return } h.logger.Info("Group removed from user", "user_id", userID, "group", groupName) c.JSON(http.StatusOK, gin.H{"message": "group removed successfully"}) } // ListRoles lists all available roles func (h *Handler) ListRoles(c *gin.Context) { roles, err := ListRoles(h.db) if err != nil { h.logger.Error("Failed to list roles", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list roles"}) return } var result []map[string]interface{} for _, role := range roles { // Get user count for this role userCount := 0 userIDs, _ := GetRoleUsers(h.db, role.ID) userCount = len(userIDs) result = append(result, map[string]interface{}{ "id": role.ID, "name": role.Name, "description": role.Description, "is_system": role.IsSystem, "user_count": userCount, "created_at": role.CreatedAt, "updated_at": role.UpdatedAt, }) } c.JSON(http.StatusOK, gin.H{"roles": result}) } // GetRole retrieves a single role func (h *Handler) GetRole(c *gin.Context) { roleID := c.Param("id") role, err := GetRoleByID(h.db, roleID) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } h.logger.Error("Failed to get role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get role"}) return } // Get user count userIDs, _ := GetRoleUsers(h.db, role.ID) c.JSON(http.StatusOK, gin.H{ "id": role.ID, "name": role.Name, "description": role.Description, "is_system": role.IsSystem, "user_count": len(userIDs), "created_at": role.CreatedAt, "updated_at": role.UpdatedAt, }) } // CreateRole creates a new role func (h *Handler) CreateRole(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Invalid request to create role", "error", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()}) return } // Trim whitespace req.Name = strings.TrimSpace(req.Name) if req.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"}) return } // Handle empty description description := strings.TrimSpace(req.Description) if description == "" { description = "" } role, err := CreateRole(h.db, req.Name, description) if err != nil { // Check if it's a unique constraint violation if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique constraint") { h.logger.Error("Role name already exists", "name", req.Name, "error", err) c.JSON(http.StatusConflict, gin.H{"error": "role name already exists"}) return } h.logger.Error("Failed to create role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create role"}) return } h.logger.Info("Role created", "role_id", role.ID, "name", role.Name) c.JSON(http.StatusCreated, gin.H{"id": role.ID, "name": role.Name}) } // UpdateRole updates an existing role func (h *Handler) UpdateRole(c *gin.Context) { roleID := c.Param("id") // Check if role is system role var isSystem bool err := h.db.QueryRow("SELECT is_system FROM roles WHERE id = $1", roleID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } if isSystem { c.JSON(http.StatusForbidden, gin.H{"error": "cannot modify system role"}) return } var req struct { Name *string `json:"name"` Description *string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Build update query dynamically updates := []string{"updated_at = NOW()"} args := []interface{}{} argIndex := 1 if req.Name != nil { name := strings.TrimSpace(*req.Name) if name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name cannot be empty"}) return } updates = append(updates, fmt.Sprintf("name = $%d", argIndex)) args = append(args, name) argIndex++ } if req.Description != nil { description := strings.TrimSpace(*req.Description) updates = append(updates, fmt.Sprintf("description = $%d", argIndex)) args = append(args, description) argIndex++ } if len(updates) == 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } args = append(args, roleID) query := "UPDATE roles SET " + strings.Join(updates, ", ") + fmt.Sprintf(" WHERE id = $%d", argIndex) _, err = h.db.Exec(query, args...) if err != nil { // Check if it's a unique constraint violation if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique constraint") { h.logger.Error("Role name already exists", "error", err) c.JSON(http.StatusConflict, gin.H{"error": "role name already exists"}) return } h.logger.Error("Failed to update role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"}) return } h.logger.Info("Role updated", "role_id", roleID) c.JSON(http.StatusOK, gin.H{"message": "role updated successfully"}) } // DeleteRole deletes a role func (h *Handler) DeleteRole(c *gin.Context) { roleID := c.Param("id") // Check if role is system role var isSystem bool err := h.db.QueryRow("SELECT is_system FROM roles WHERE id = $1", roleID).Scan(&isSystem) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "role not found"}) return } if isSystem { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system role"}) return } _, err = h.db.Exec("DELETE FROM roles WHERE id = $1", roleID) if err != nil { h.logger.Error("Failed to delete role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete role"}) return } h.logger.Info("Role deleted", "role_id", roleID) c.JSON(http.StatusOK, gin.H{"message": "role deleted successfully"}) } // GetRolePermissions retrieves all permissions for a role func (h *Handler) GetRolePermissions(c *gin.Context) { roleID := c.Param("id") permissions, err := GetRolePermissions(h.db, roleID) if err != nil { h.logger.Error("Failed to get role permissions", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get role permissions"}) return } c.JSON(http.StatusOK, gin.H{"permissions": permissions}) } // AssignPermissionToRole assigns a permission to a role func (h *Handler) AssignPermissionToRole(c *gin.Context) { roleID := c.Param("id") var req struct { PermissionName string `json:"permission_name" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get permission ID by name permissionID, err := GetPermissionIDByName(h.db, req.PermissionName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "permission not found"}) return } h.logger.Error("Failed to get permission", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign permission"}) return } err = AddPermissionToRole(h.db, roleID, permissionID) if err != nil { h.logger.Error("Failed to assign permission to role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign permission"}) return } h.logger.Info("Permission assigned to role", "role_id", roleID, "permission", req.PermissionName) c.JSON(http.StatusOK, gin.H{"message": "permission assigned successfully"}) } // RemovePermissionFromRole removes a permission from a role func (h *Handler) RemovePermissionFromRole(c *gin.Context) { roleID := c.Param("id") // For DELETE requests, we can get permission_name from query param or body var req struct { PermissionName string `json:"permission_name"` } // Try to get from query param first permissionName := c.Query("permission_name") if permissionName == "" { // Try to get from body if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "permission_name is required"}) return } permissionName = req.PermissionName } if permissionName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "permission_name is required"}) return } // Get permission ID by name permissionID, err := GetPermissionIDByName(h.db, permissionName) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "permission not found"}) return } h.logger.Error("Failed to get permission", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove permission"}) return } err = RemovePermissionFromRole(h.db, roleID, permissionID) if err != nil { h.logger.Error("Failed to remove permission from role", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove permission"}) return } h.logger.Info("Permission removed from role", "role_id", roleID, "permission", permissionName) c.JSON(http.StatusOK, gin.H{"message": "permission removed successfully"}) } // ListPermissions lists all available permissions func (h *Handler) ListPermissions(c *gin.Context) { permissions, err := ListPermissions(h.db) if err != nil { h.logger.Error("Failed to list permissions", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list permissions"}) return } c.JSON(http.StatusOK, gin.H{"permissions": permissions}) }