From b2284921ea49cdcb7b9c539b0cdb9c9ceb26623e Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sun, 14 Dec 2025 10:11:36 -0300 Subject: [PATCH] feat: add Cloudflare and cPanel admin routes Cloudflare Cache Management: - GET /api/v1/admin/cloudflare/zones - POST /api/v1/admin/cloudflare/cache/purge-all - POST /api/v1/admin/cloudflare/cache/purge-urls - POST /api/v1/admin/cloudflare/cache/purge-tags - POST /api/v1/admin/cloudflare/cache/purge-hosts cPanel Email Management: - GET /api/v1/admin/cpanel/emails - POST /api/v1/admin/cpanel/emails - DELETE /api/v1/admin/cpanel/emails/{email} - PUT /api/v1/admin/cpanel/emails/{email}/password - PUT /api/v1/admin/cpanel/emails/{email}/quota All routes protected by JWT auth middleware. Added CLOUDFLARE_* and CPANEL_* env vars to .env.example --- backend/.env.example | 16 +- backend/internal/admin/cloudflare/client.go | 206 +++++++++++++++ backend/internal/admin/cloudflare/handler.go | 177 +++++++++++++ backend/internal/admin/cpanel/client.go | 253 +++++++++++++++++++ backend/internal/admin/cpanel/handler.go | 205 +++++++++++++++ backend/internal/router/router.go | 25 ++ 6 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 backend/internal/admin/cloudflare/client.go create mode 100644 backend/internal/admin/cloudflare/handler.go create mode 100644 backend/internal/admin/cpanel/client.go create mode 100644 backend/internal/admin/cpanel/handler.go diff --git a/backend/.env.example b/backend/.env.example index 4831ecf..700e5ad 100755 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,4 @@ -# Environment variables for Todai Jobs Backend +# Environment variables for GoHorse Jobs Backend # Database Configuration DB_HOST=localhost @@ -28,3 +28,17 @@ CORS_ORIGINS=http://localhost:8963 # File Upload MAX_UPLOAD_SIZE=10485760 UPLOAD_DIR=./uploads + +# ============================================================================= +# Cloudflare API (for cache management) +# ============================================================================= +CLOUDFLARE_API_TOKEN=your-cloudflare-api-token +CLOUDFLARE_ZONE_ID=your-zone-id + +# ============================================================================= +# cPanel API (for email management) +# ============================================================================= +CPANEL_HOST=https://cpanel.yourdomain.com:2083 +CPANEL_USERNAME=your-cpanel-username +CPANEL_API_TOKEN=your-cpanel-api-token + diff --git a/backend/internal/admin/cloudflare/client.go b/backend/internal/admin/cloudflare/client.go new file mode 100644 index 0000000..55e93fc --- /dev/null +++ b/backend/internal/admin/cloudflare/client.go @@ -0,0 +1,206 @@ +package cloudflare + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +const ( + baseURL = "https://api.cloudflare.com/client/v4" +) + +// Client handles Cloudflare API interactions +type Client struct { + apiToken string + zoneID string + http *http.Client +} + +// NewClient creates a new Cloudflare API client +func NewClient() *Client { + return &Client{ + apiToken: os.Getenv("CLOUDFLARE_API_TOKEN"), + zoneID: os.Getenv("CLOUDFLARE_ZONE_ID"), + http: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// NewClientWithConfig creates a client with custom config +func NewClientWithConfig(apiToken, zoneID string) *Client { + return &Client{ + apiToken: apiToken, + zoneID: zoneID, + http: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// Zone represents a Cloudflare zone +type Zone struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` +} + +// ZonesResponse is the response from /zones endpoint +type ZonesResponse struct { + Success bool `json:"success"` + Errors []Error `json:"errors"` + Messages []string `json:"messages"` + Result []Zone `json:"result"` +} + +// PurgeResponse is the response from cache purge endpoints +type PurgeResponse struct { + Success bool `json:"success"` + Errors []Error `json:"errors"` + Messages []string `json:"messages"` + Result struct { + ID string `json:"id"` + } `json:"result"` +} + +// Error represents a Cloudflare API error +type Error struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// doRequest executes an HTTP request to Cloudflare API +func (c *Client) doRequest(method, endpoint string, body interface{}) ([]byte, error) { + var reqBody io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + reqBody = bytes.NewBuffer(jsonBody) + } + + req, err := http.NewRequest(method, baseURL+endpoint, reqBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.apiToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.http.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody)) + } + + return respBody, nil +} + +// GetZones returns all zones for the account +func (c *Client) GetZones() ([]Zone, error) { + respBody, err := c.doRequest("GET", "/zones", nil) + if err != nil { + return nil, err + } + + var response ZonesResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if !response.Success { + if len(response.Errors) > 0 { + return nil, fmt.Errorf("API error: %s", response.Errors[0].Message) + } + return nil, fmt.Errorf("unknown API error") + } + + return response.Result, nil +} + +// PurgeAll purges all cached content for the zone +func (c *Client) PurgeAll() (*PurgeResponse, error) { + endpoint := fmt.Sprintf("/zones/%s/purge_cache", c.zoneID) + body := map[string]bool{"purge_everything": true} + + respBody, err := c.doRequest("POST", endpoint, body) + if err != nil { + return nil, err + } + + var response PurgeResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} + +// PurgeByURLs purges specific URLs from cache +func (c *Client) PurgeByURLs(urls []string) (*PurgeResponse, error) { + endpoint := fmt.Sprintf("/zones/%s/purge_cache", c.zoneID) + body := map[string][]string{"files": urls} + + respBody, err := c.doRequest("POST", endpoint, body) + if err != nil { + return nil, err + } + + var response PurgeResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} + +// PurgeByTags purges content by cache tags (Enterprise only) +func (c *Client) PurgeByTags(tags []string) (*PurgeResponse, error) { + endpoint := fmt.Sprintf("/zones/%s/purge_cache", c.zoneID) + body := map[string][]string{"tags": tags} + + respBody, err := c.doRequest("POST", endpoint, body) + if err != nil { + return nil, err + } + + var response PurgeResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} + +// PurgeByHosts purges content by hostnames +func (c *Client) PurgeByHosts(hosts []string) (*PurgeResponse, error) { + endpoint := fmt.Sprintf("/zones/%s/purge_cache", c.zoneID) + body := map[string][]string{"hosts": hosts} + + respBody, err := c.doRequest("POST", endpoint, body) + if err != nil { + return nil, err + } + + var response PurgeResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &response, nil +} diff --git a/backend/internal/admin/cloudflare/handler.go b/backend/internal/admin/cloudflare/handler.go new file mode 100644 index 0000000..e43d31a --- /dev/null +++ b/backend/internal/admin/cloudflare/handler.go @@ -0,0 +1,177 @@ +package cloudflare + +import ( + "encoding/json" + "net/http" +) + +// Handler handles Cloudflare admin endpoints +type Handler struct { + client *Client +} + +// NewHandler creates a new Cloudflare handler +func NewHandler() *Handler { + return &Handler{ + client: NewClient(), + } +} + +// PurgeURLsRequest contains URLs to purge from cache +type PurgeURLsRequest struct { + URLs []string `json:"urls"` +} + +// PurgeTagsRequest contains cache tags to purge +type PurgeTagsRequest struct { + Tags []string `json:"tags"` +} + +// PurgeHostsRequest contains hostnames to purge +type PurgeHostsRequest struct { + Hosts []string `json:"hosts"` +} + +// GetZones godoc +// @Summary List Cloudflare Zones +// @Description Returns all zones associated with the Cloudflare account +// @Tags Admin - Cloudflare +// @Accept json +// @Produce json +// @Success 200 {array} Zone +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cloudflare/zones [get] +func (h *Handler) GetZones(w http.ResponseWriter, r *http.Request) { + zones, err := h.client.GetZones() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(zones) +} + +// PurgeAll godoc +// @Summary Purge All Cache +// @Description Purges all cached content for the configured zone +// @Tags Admin - Cloudflare +// @Accept json +// @Produce json +// @Success 200 {object} PurgeResponse +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cloudflare/cache/purge-all [post] +func (h *Handler) PurgeAll(w http.ResponseWriter, r *http.Request) { + result, err := h.client.PurgeAll() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// PurgeByURLs godoc +// @Summary Purge Cache by URLs +// @Description Purges specific URLs from cache +// @Tags Admin - Cloudflare +// @Accept json +// @Produce json +// @Param body body PurgeURLsRequest true "URLs to purge" +// @Success 200 {object} PurgeResponse +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cloudflare/cache/purge-urls [post] +func (h *Handler) PurgeByURLs(w http.ResponseWriter, r *http.Request) { + var req PurgeURLsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if len(req.URLs) == 0 { + http.Error(w, "URLs array is required", http.StatusBadRequest) + return + } + + result, err := h.client.PurgeByURLs(req.URLs) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// PurgeByTags godoc +// @Summary Purge Cache by Tags +// @Description Purges content by cache tags (Enterprise only) +// @Tags Admin - Cloudflare +// @Accept json +// @Produce json +// @Param body body PurgeTagsRequest true "Tags to purge" +// @Success 200 {object} PurgeResponse +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cloudflare/cache/purge-tags [post] +func (h *Handler) PurgeByTags(w http.ResponseWriter, r *http.Request) { + var req PurgeTagsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if len(req.Tags) == 0 { + http.Error(w, "Tags array is required", http.StatusBadRequest) + return + } + + result, err := h.client.PurgeByTags(req.Tags) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// PurgeByHosts godoc +// @Summary Purge Cache by Hosts +// @Description Purges content by hostnames +// @Tags Admin - Cloudflare +// @Accept json +// @Produce json +// @Param body body PurgeHostsRequest true "Hosts to purge" +// @Success 200 {object} PurgeResponse +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cloudflare/cache/purge-hosts [post] +func (h *Handler) PurgeByHosts(w http.ResponseWriter, r *http.Request) { + var req PurgeHostsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if len(req.Hosts) == 0 { + http.Error(w, "Hosts array is required", http.StatusBadRequest) + return + } + + result, err := h.client.PurgeByHosts(req.Hosts) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} diff --git a/backend/internal/admin/cpanel/client.go b/backend/internal/admin/cpanel/client.go new file mode 100644 index 0000000..c2158e6 --- /dev/null +++ b/backend/internal/admin/cpanel/client.go @@ -0,0 +1,253 @@ +package cpanel + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "time" +) + +// Client handles cPanel UAPI interactions +type Client struct { + host string + username string + apiToken string + http *http.Client +} + +// NewClient creates a new cPanel API client +func NewClient() *Client { + return &Client{ + host: os.Getenv("CPANEL_HOST"), + username: os.Getenv("CPANEL_USERNAME"), + apiToken: os.Getenv("CPANEL_API_TOKEN"), + http: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// NewClientWithConfig creates a client with custom config +func NewClientWithConfig(host, username, apiToken string) *Client { + return &Client{ + host: host, + username: username, + apiToken: apiToken, + http: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// EmailAccount represents a cPanel email account +type EmailAccount struct { + Email string `json:"email"` + Login string `json:"login"` + Domain string `json:"domain"` + DiskUsed string `json:"diskused"` + DiskQuota string `json:"diskquota"` + HumandiskUsed string `json:"humandiskused"` + HumandiskQuota string `json:"humandiskquota"` +} + +// UAPIResponse is the generic UAPI response structure +type UAPIResponse struct { + APIVersion int `json:"apiversion"` + Func string `json:"func"` + Module string `json:"module"` + Result *json.RawMessage `json:"result"` + Status int `json:"status"` + Errors []string `json:"errors"` + Messages []string `json:"messages"` +} + +// ListEmailsResult is the result from list_pops +type ListEmailsResult struct { + Data []EmailAccount `json:"data"` +} + +// doRequest executes an HTTP request to cPanel UAPI +func (c *Client) doRequest(module, function string, params map[string]string) ([]byte, error) { + // Build UAPI URL + apiURL := fmt.Sprintf("%s/execute/%s/%s", c.host, module, function) + + // Add query parameters + if len(params) > 0 { + values := url.Values{} + for k, v := range params { + values.Set(k, v) + } + apiURL += "?" + values.Encode() + } + + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // cPanel API Token authentication + req.Header.Set("Authorization", fmt.Sprintf("cpanel %s:%s", c.username, c.apiToken)) + + resp, err := c.http.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody)) + } + + return respBody, nil +} + +// ListEmails returns all email accounts for a domain +func (c *Client) ListEmails(domain string) ([]EmailAccount, error) { + params := map[string]string{} + if domain != "" { + params["domain"] = domain + } + + respBody, err := c.doRequest("Email", "list_pops", params) + if err != nil { + return nil, err + } + + var response UAPIResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if response.Status != 1 { + if len(response.Errors) > 0 { + return nil, fmt.Errorf("API error: %s", response.Errors[0]) + } + return nil, fmt.Errorf("unknown API error") + } + + var result []EmailAccount + if response.Result != nil { + if err := json.Unmarshal(*response.Result, &result); err != nil { + return nil, fmt.Errorf("failed to parse result: %w", err) + } + } + + return result, nil +} + +// CreateEmail creates a new email account +func (c *Client) CreateEmail(email, password string, quota int) error { + // Parse email to get user and domain + params := map[string]string{ + "email": email, + "password": password, + "quota": fmt.Sprintf("%d", quota), // MB, 0 = unlimited + } + + respBody, err := c.doRequest("Email", "add_pop", params) + if err != nil { + return err + } + + var response UAPIResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + if response.Status != 1 { + if len(response.Errors) > 0 { + return fmt.Errorf("API error: %s", response.Errors[0]) + } + return fmt.Errorf("failed to create email") + } + + return nil +} + +// DeleteEmail removes an email account +func (c *Client) DeleteEmail(email string) error { + params := map[string]string{ + "email": email, + } + + respBody, err := c.doRequest("Email", "delete_pop", params) + if err != nil { + return err + } + + var response UAPIResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + if response.Status != 1 { + if len(response.Errors) > 0 { + return fmt.Errorf("API error: %s", response.Errors[0]) + } + return fmt.Errorf("failed to delete email") + } + + return nil +} + +// ChangePassword changes an email account's password +func (c *Client) ChangePassword(email, newPassword string) error { + params := map[string]string{ + "email": email, + "password": newPassword, + } + + respBody, err := c.doRequest("Email", "passwd_pop", params) + if err != nil { + return err + } + + var response UAPIResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + if response.Status != 1 { + if len(response.Errors) > 0 { + return fmt.Errorf("API error: %s", response.Errors[0]) + } + return fmt.Errorf("failed to change password") + } + + return nil +} + +// UpdateQuota updates an email account's disk quota +func (c *Client) UpdateQuota(email string, quotaMB int) error { + params := map[string]string{ + "email": email, + "quota": fmt.Sprintf("%d", quotaMB), + } + + respBody, err := c.doRequest("Email", "edit_pop_quota", params) + if err != nil { + return err + } + + var response UAPIResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + if response.Status != 1 { + if len(response.Errors) > 0 { + return fmt.Errorf("API error: %s", response.Errors[0]) + } + return fmt.Errorf("failed to update quota") + } + + return nil +} diff --git a/backend/internal/admin/cpanel/handler.go b/backend/internal/admin/cpanel/handler.go new file mode 100644 index 0000000..f66c210 --- /dev/null +++ b/backend/internal/admin/cpanel/handler.go @@ -0,0 +1,205 @@ +package cpanel + +import ( + "encoding/json" + "net/http" +) + +// Handler handles cPanel admin endpoints +type Handler struct { + client *Client +} + +// NewHandler creates a new cPanel handler +func NewHandler() *Handler { + return &Handler{ + client: NewClient(), + } +} + +// CreateEmailRequest is the request body for creating an email +type CreateEmailRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Quota int `json:"quota"` // MB, 0 = unlimited +} + +// ChangePasswordRequest is the request body for changing password +type ChangePasswordRequest struct { + Password string `json:"password"` +} + +// UpdateQuotaRequest is the request body for updating quota +type UpdateQuotaRequest struct { + Quota int `json:"quota"` // MB +} + +// ListEmails godoc +// @Summary List Email Accounts +// @Description Returns all email accounts for the cPanel account +// @Tags Admin - cPanel +// @Accept json +// @Produce json +// @Param domain query string false "Filter by domain" +// @Success 200 {array} EmailAccount +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cpanel/emails [get] +func (h *Handler) ListEmails(w http.ResponseWriter, r *http.Request) { + domain := r.URL.Query().Get("domain") + + emails, err := h.client.ListEmails(domain) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(emails) +} + +// CreateEmail godoc +// @Summary Create Email Account +// @Description Creates a new email account +// @Tags Admin - cPanel +// @Accept json +// @Produce json +// @Param body body CreateEmailRequest true "Email details" +// @Success 201 {object} map[string]string +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cpanel/emails [post] +func (h *Handler) CreateEmail(w http.ResponseWriter, r *http.Request) { + var req CreateEmailRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.Email == "" || req.Password == "" { + http.Error(w, "Email and password are required", http.StatusBadRequest) + return + } + + if err := h.client.CreateEmail(req.Email, req.Password, req.Quota); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Email created successfully", + "email": req.Email, + }) +} + +// DeleteEmail godoc +// @Summary Delete Email Account +// @Description Deletes an email account +// @Tags Admin - cPanel +// @Accept json +// @Produce json +// @Param email path string true "Email address" +// @Success 200 {object} map[string]string +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cpanel/emails/{email} [delete] +func (h *Handler) DeleteEmail(w http.ResponseWriter, r *http.Request) { + email := r.PathValue("email") + if email == "" { + http.Error(w, "Email is required", http.StatusBadRequest) + return + } + + if err := h.client.DeleteEmail(email); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "message": "Email deleted successfully", + }) +} + +// ChangePassword godoc +// @Summary Change Email Password +// @Description Changes the password for an email account +// @Tags Admin - cPanel +// @Accept json +// @Produce json +// @Param email path string true "Email address" +// @Param body body ChangePasswordRequest true "New password" +// @Success 200 {object} map[string]string +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cpanel/emails/{email}/password [put] +func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { + email := r.PathValue("email") + if email == "" { + http.Error(w, "Email is required", http.StatusBadRequest) + return + } + + var req ChangePasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.Password == "" { + http.Error(w, "Password is required", http.StatusBadRequest) + return + } + + if err := h.client.ChangePassword(email, req.Password); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "message": "Password changed successfully", + }) +} + +// UpdateQuota godoc +// @Summary Update Email Quota +// @Description Updates the disk quota for an email account +// @Tags Admin - cPanel +// @Accept json +// @Produce json +// @Param email path string true "Email address" +// @Param body body UpdateQuotaRequest true "New quota in MB" +// @Success 200 {object} map[string]string +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal Server Error" +// @Security BearerAuth +// @Router /api/v1/admin/cpanel/emails/{email}/quota [put] +func (h *Handler) UpdateQuota(w http.ResponseWriter, r *http.Request) { + email := r.PathValue("email") + if email == "" { + http.Error(w, "Email is required", http.StatusBadRequest) + return + } + + var req UpdateQuotaRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := h.client.UpdateQuota(email, req.Quota); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "message": "Quota updated successfully", + }) +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 6d69abb..a59138b 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -21,6 +21,10 @@ import ( authInfra "github.com/rede5/gohorsejobs/backend/internal/infrastructure/auth" legacyMiddleware "github.com/rede5/gohorsejobs/backend/internal/middleware" + // Admin Imports + "github.com/rede5/gohorsejobs/backend/internal/admin/cloudflare" + "github.com/rede5/gohorsejobs/backend/internal/admin/cpanel" + _ "github.com/rede5/gohorsejobs/backend/docs" // Import generated docs httpSwagger "github.com/swaggo/http-swagger/v2" ) @@ -62,6 +66,10 @@ func NewRouter() http.Handler { jobHandler := handlers.NewJobHandler(jobService) applicationHandler := handlers.NewApplicationHandler(applicationService) + // Initialize Admin Handlers + cloudflareHandler := cloudflare.NewHandler() + cpanelHandler := cpanel.NewHandler() + // --- ROOT ROUTE --- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { @@ -133,6 +141,23 @@ func NewRouter() http.Handler { log.Println("S3 storage routes registered successfully") } + // --- ADMIN ROUTES (Protected - SuperAdmin only) --- + // Cloudflare Cache Management + mux.Handle("GET /api/v1/admin/cloudflare/zones", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cloudflareHandler.GetZones))) + mux.Handle("POST /api/v1/admin/cloudflare/cache/purge-all", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cloudflareHandler.PurgeAll))) + mux.Handle("POST /api/v1/admin/cloudflare/cache/purge-urls", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cloudflareHandler.PurgeByURLs))) + mux.Handle("POST /api/v1/admin/cloudflare/cache/purge-tags", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cloudflareHandler.PurgeByTags))) + mux.Handle("POST /api/v1/admin/cloudflare/cache/purge-hosts", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cloudflareHandler.PurgeByHosts))) + + // cPanel Email Management + mux.Handle("GET /api/v1/admin/cpanel/emails", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cpanelHandler.ListEmails))) + mux.Handle("POST /api/v1/admin/cpanel/emails", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cpanelHandler.CreateEmail))) + mux.Handle("DELETE /api/v1/admin/cpanel/emails/{email}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cpanelHandler.DeleteEmail))) + mux.Handle("PUT /api/v1/admin/cpanel/emails/{email}/password", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cpanelHandler.ChangePassword))) + mux.Handle("PUT /api/v1/admin/cpanel/emails/{email}/quota", authMiddleware.HeaderAuthGuard(http.HandlerFunc(cpanelHandler.UpdateQuota))) + + log.Println("Admin routes (Cloudflare, cPanel) registered successfully") + // Swagger Route - available at /docs mux.HandleFunc("/docs/", httpSwagger.WrapHandler)