1173 lines
34 KiB
Go
1173 lines
34 KiB
Go
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})
|
|
}
|