package handler import ( "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/johnzastrow/actalog/internal/service" "github.com/johnzastrow/actalog/pkg/logger" "github.com/johnzastrow/actalog/pkg/middleware" ) type OrganizationHandler struct { orgService *service.OrganizationService logger *logger.Logger } func NewOrganizationHandler(orgService *service.OrganizationService, logger *logger.Logger) *OrganizationHandler { return &OrganizationHandler{ orgService: orgService, logger: logger, } } // CreateOrganization handles POST /api/admin/organizations // @Summary Create organization (Admin) // @Description Create a new organization (admin only) // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param request body object true "Organization name and description" // @Success 201 {object} map[string]interface{} "Created organization" // @Failure 400 {object} ErrorResponse "Invalid request" // @Failure 409 {object} ErrorResponse "Organization name exists" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/organizations [post] func (h *OrganizationHandler) CreateOrganization(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } var req struct { Name string `json:"name"` Description *string `json:"description"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.Name == "" { respondError(w, http.StatusBadRequest, "Organization name is required") return } org, err := h.orgService.Create(adminUserID, req.Name, req.Description) if err != nil { if err == service.ErrOrganizationNameExists { respondError(w, http.StatusConflict, "Organization name already exists") return } h.logger.Error("Failed to create organization: %v", err) respondError(w, http.StatusInternalServerError, "Failed to create organization") return } respondJSON(w, http.StatusCreated, org) } // ListOrganizations handles GET /api/admin/organizations // @Summary List organizations (Admin) // @Description Get a paginated list of all organizations // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param limit query int false "Max results (default 50)" // @Param offset query int false "Skip N results (default 0)" // @Success 200 {object} map[string]interface{} "Organizations list with total count" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/organizations [get] func (h *OrganizationHandler) ListOrganizations(w http.ResponseWriter, r *http.Request) { limit := 50 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { limit = parsedLimit } } offset := 0 if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" { if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { offset = parsedOffset } } orgs, total, err := h.orgService.List(limit, offset) if err != nil { h.logger.Error("Failed to list organizations: %v", err) respondError(w, http.StatusInternalServerError, "Failed to list organizations") return } response := map[string]interface{}{ "organizations": orgs, "total": total, "limit": limit, "offset": offset, } respondJSON(w, http.StatusOK, response) } // GetOrganization handles GET /api/admin/organizations/:id // @Summary Get organization (Admin) // @Description Get details of a specific organization // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Organization ID" // @Success 200 {object} map[string]interface{} "Organization details" // @Failure 400 {object} ErrorResponse "Invalid organization ID" // @Failure 404 {object} ErrorResponse "Organization not found" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/organizations/{id} [get] func (h *OrganizationHandler) GetOrganization(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid organization ID") return } org, err := h.orgService.GetByID(id) if err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } h.logger.Error("Failed to get organization: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get organization") return } respondJSON(w, http.StatusOK, org) } // UpdateOrganization handles PUT /api/admin/organizations/:id // @Summary Update organization (Admin) // @Description Update an existing organization's details // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Organization ID" // @Param request body object true "Updated organization details" // @Success 200 {object} map[string]interface{} "Updated organization" // @Failure 400 {object} ErrorResponse "Invalid request" // @Failure 404 {object} ErrorResponse "Organization not found" // @Failure 409 {object} ErrorResponse "Organization name exists" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/organizations/{id} [put] func (h *OrganizationHandler) UpdateOrganization(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid organization ID") return } var req struct { Name string `json:"name"` Description *string `json:"description"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } org, err := h.orgService.Update(adminUserID, id, req.Name, req.Description) if err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } if err == service.ErrOrganizationNameExists { respondError(w, http.StatusConflict, "Organization name already exists") return } h.logger.Error("Failed to update organization: %v", err) respondError(w, http.StatusInternalServerError, "Failed to update organization") return } respondJSON(w, http.StatusOK, org) } // DeleteOrganization handles DELETE /api/admin/organizations/:id // @Summary Delete organization (Admin) // @Description Delete an organization (must have no assigned users) // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Organization ID" // @Success 200 {object} MessageResponse "Success message" // @Failure 400 {object} ErrorResponse "Invalid organization ID" // @Failure 404 {object} ErrorResponse "Organization not found" // @Failure 409 {object} ErrorResponse "Organization has assigned users" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/organizations/{id} [delete] func (h *OrganizationHandler) DeleteOrganization(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid organization ID") return } err = h.orgService.Delete(adminUserID, id) if err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } if err == service.ErrOrganizationHasUsers { respondError(w, http.StatusConflict, "Cannot delete organization with assigned users") return } h.logger.Error("Failed to delete organization: %v", err) respondError(w, http.StatusInternalServerError, "Failed to delete organization") return } respondJSON(w, http.StatusOK, map[string]string{ "message": "Organization deleted successfully", }) } // AssignUserToOrganization handles POST /api/admin/users/:id/organization // @Summary Assign user to organization (Admin) // @Description Assign a user to an organization // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Param request body object true "Organization ID to assign" // @Success 200 {object} MessageResponse "Success message" // @Failure 400 {object} ErrorResponse "Invalid request" // @Failure 404 {object} ErrorResponse "User or organization not found" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/users/{id}/organization [post] func (h *OrganizationHandler) AssignUserToOrganization(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } userIDStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid user ID") return } var req struct { OrganizationID int64 `json:"organization_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.OrganizationID == 0 { respondError(w, http.StatusBadRequest, "Organization ID is required") return } err = h.orgService.AssignUserToOrganization(adminUserID, userID, req.OrganizationID) if err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } if err == service.ErrUserNotFound { respondError(w, http.StatusNotFound, "User not found") return } h.logger.Error("Failed to assign user to organization: %v", err) respondError(w, http.StatusInternalServerError, "Failed to assign user") return } respondJSON(w, http.StatusOK, map[string]string{ "message": "User assigned to organization successfully", }) } // RemoveUserFromOrganization handles DELETE /api/admin/users/:id/organization/:org_id // @Summary Remove user from organization (Admin) // @Description Remove a user from an organization // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Param org_id path int true "Organization ID" // @Success 200 {object} MessageResponse "Success message" // @Failure 400 {object} ErrorResponse "Invalid ID" // @Failure 404 {object} ErrorResponse "User or organization not found" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/users/{id}/organization/{org_id} [delete] func (h *OrganizationHandler) RemoveUserFromOrganization(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } userIDStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid user ID") return } orgIDStr := chi.URLParam(r, "org_id") orgID, err := strconv.ParseInt(orgIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid organization ID") return } err = h.orgService.RemoveUserFromOrganization(adminUserID, userID, orgID) if err != nil { if err == service.ErrUserNotFound { respondError(w, http.StatusNotFound, "User not found") return } if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } h.logger.Error("Failed to remove user from organization: %v", err) respondError(w, http.StatusInternalServerError, "Failed to remove user") return } respondJSON(w, http.StatusOK, map[string]string{ "message": "User removed from organization successfully", }) } // GetUserOrganizations handles GET /api/admin/users/:id/organizations // @Summary Get user organizations (Admin) // @Description Get all organizations a user belongs to // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "User ID" // @Success 200 {object} map[string]interface{} "List of organizations" // @Failure 400 {object} ErrorResponse "Invalid user ID" // @Failure 404 {object} ErrorResponse "User not found" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/users/{id}/organizations [get] func (h *OrganizationHandler) GetUserOrganizations(w http.ResponseWriter, r *http.Request) { userIDStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid user ID") return } orgs, err := h.orgService.GetUserOrganizations(userID) if err != nil { if err == service.ErrUserNotFound { respondError(w, http.StatusNotFound, "User not found") return } h.logger.Error("Failed to get user organizations: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get organizations") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "organizations": orgs, "count": len(orgs), }) } // GetOrganizationUsers handles GET /api/admin/organizations/:id/users // @Summary Get organization users (Admin) // @Description Get all users belonging to an organization // @Tags organizations // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Organization ID" // @Success 200 {object} map[string]interface{} "List of users" // @Failure 400 {object} ErrorResponse "Invalid organization ID" // @Failure 404 {object} ErrorResponse "Organization not found" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /admin/organizations/{id}/users [get] func (h *OrganizationHandler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) { orgIDStr := chi.URLParam(r, "id") orgID, err := strconv.ParseInt(orgIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid organization ID") return } users, err := h.orgService.GetOrganizationUsers(orgID) if err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } h.logger.Error("Failed to get organization users: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get users") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "users": users, "count": len(users), }) }