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
This commit is contained in:
parent
1e753b611a
commit
b2284921ea
6 changed files with 881 additions and 1 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
206
backend/internal/admin/cloudflare/client.go
Normal file
206
backend/internal/admin/cloudflare/client.go
Normal file
|
|
@ -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
|
||||
}
|
||||
177
backend/internal/admin/cloudflare/handler.go
Normal file
177
backend/internal/admin/cloudflare/handler.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
253
backend/internal/admin/cpanel/client.go
Normal file
253
backend/internal/admin/cpanel/client.go
Normal file
|
|
@ -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
|
||||
}
|
||||
205
backend/internal/admin/cpanel/handler.go
Normal file
205
backend/internal/admin/cpanel/handler.go
Normal file
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue