feat(migration): move admin cloudflare routes to backoffice and cleanup backend
This commit is contained in:
parent
1caeb72d7c
commit
ce31ab8e67
11 changed files with 192 additions and 875 deletions
|
|
@ -1,206 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -22,8 +22,6 @@ import (
|
||||||
legacyMiddleware "github.com/rede5/gohorsejobs/backend/internal/middleware"
|
legacyMiddleware "github.com/rede5/gohorsejobs/backend/internal/middleware"
|
||||||
|
|
||||||
// Admin Imports
|
// 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
|
_ "github.com/rede5/gohorsejobs/backend/docs" // Import generated docs
|
||||||
httpSwagger "github.com/swaggo/http-swagger/v2"
|
httpSwagger "github.com/swaggo/http-swagger/v2"
|
||||||
|
|
@ -66,10 +64,6 @@ func NewRouter() http.Handler {
|
||||||
jobHandler := handlers.NewJobHandler(jobService)
|
jobHandler := handlers.NewJobHandler(jobService)
|
||||||
applicationHandler := handlers.NewApplicationHandler(applicationService)
|
applicationHandler := handlers.NewApplicationHandler(applicationService)
|
||||||
|
|
||||||
// Initialize Admin Handlers
|
|
||||||
cloudflareHandler := cloudflare.NewHandler()
|
|
||||||
cpanelHandler := cpanel.NewHandler()
|
|
||||||
|
|
||||||
// cachedPublicIP stores the public IP to avoid repeated external calls
|
// cachedPublicIP stores the public IP to avoid repeated external calls
|
||||||
var cachedPublicIP string
|
var cachedPublicIP string
|
||||||
|
|
||||||
|
|
@ -162,23 +156,6 @@ func NewRouter() http.Handler {
|
||||||
log.Println("S3 storage routes registered successfully")
|
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
|
// Swagger Route - available at /docs
|
||||||
mux.HandleFunc("/docs/", httpSwagger.WrapHandler)
|
mux.HandleFunc("/docs/", httpSwagger.WrapHandler)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,12 @@ DATABASE_URL=postgresql://user:password@localhost:5432/gohorse_backoffice
|
||||||
# JWT
|
# JWT
|
||||||
JWT_SECRET=your-super-secret-jwt-key
|
JWT_SECRET=your-super-secret-jwt-key
|
||||||
JWT_EXPIRATION=7d
|
JWT_EXPIRATION=7d
|
||||||
|
|
||||||
|
# Cloudflare
|
||||||
|
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
||||||
|
CLOUDFLARE_ZONE_ID=your-zone-id
|
||||||
|
|
||||||
|
# cPanel
|
||||||
|
CPANEL_HOST=https://cpanel.yourdomain.com:2083
|
||||||
|
CPANEL_USERNAME=your-cpanel-username
|
||||||
|
CPANEL_API_TOKEN=your-cpanel-api-token
|
||||||
|
|
|
||||||
59
backoffice/package-lock.json
generated
59
backoffice/package-lock.json
generated
|
|
@ -9,11 +9,13 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/swagger": "^11.2.3",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
|
@ -2276,6 +2278,17 @@
|
||||||
"@tybys/wasm-util": "^0.10.0"
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/axios": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
|
"axios": "^1.3.1",
|
||||||
|
"rxjs": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/cli": {
|
"node_modules/@nestjs/cli": {
|
||||||
"version": "11.0.14",
|
"version": "11.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.14.tgz",
|
||||||
|
|
@ -5041,9 +5054,20 @@
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
|
||||||
|
|
@ -5750,7 +5774,6 @@
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
|
|
@ -5967,7 +5990,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
|
|
@ -6161,7 +6183,6 @@
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -6853,6 +6874,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fork-ts-checker-webpack-plugin": {
|
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz",
|
||||||
|
|
@ -6941,7 +6982,6 @@
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
|
|
@ -6958,7 +6998,6 @@
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
|
|
@ -6968,7 +7007,6 @@
|
||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
|
|
@ -7238,7 +7276,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
|
|
@ -9821,6 +9858,12 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,13 @@
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/swagger": "^11.2.3",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
|
import { CloudflareService } from './cloudflare.service';
|
||||||
|
import { CloudflareController } from './cloudflare.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [AdminService],
|
imports: [HttpModule],
|
||||||
controllers: [AdminController],
|
providers: [AdminService, CloudflareService],
|
||||||
exports: [AdminService],
|
controllers: [AdminController, CloudflareController],
|
||||||
|
exports: [AdminService, CloudflareService],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|
|
||||||
44
backoffice/src/admin/cloudflare.controller.ts
Normal file
44
backoffice/src/admin/cloudflare.controller.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { CloudflareService } from './cloudflare.service';
|
||||||
|
|
||||||
|
// Add Auth Guard if you have one, e.g. @UseGuards(JwtAuthGuard)
|
||||||
|
@ApiTags('Cloudflare')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('admin/cloudflare')
|
||||||
|
export class CloudflareController {
|
||||||
|
constructor(private readonly cloudflareService: CloudflareService) {}
|
||||||
|
|
||||||
|
@Get('zones')
|
||||||
|
@ApiOperation({ summary: 'List Cloudflare Zones' })
|
||||||
|
async listZones() {
|
||||||
|
return this.cloudflareService.listZones();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('cache/purge-all')
|
||||||
|
@ApiOperation({ summary: 'Purge All Cache' })
|
||||||
|
async purgeAll() {
|
||||||
|
return this.cloudflareService.purgeCache({ purge_everything: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('cache/purge-urls')
|
||||||
|
@ApiOperation({ summary: 'Purge Cache by URLs' })
|
||||||
|
@ApiBody({ schema: { example: { urls: ['https://example.com/image.png'] } } })
|
||||||
|
async purgeUrls(@Body('urls') urls: string[]) {
|
||||||
|
return this.cloudflareService.purgeCache({ files: urls });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('cache/purge-tags')
|
||||||
|
@ApiOperation({ summary: 'Purge Cache by Tags' })
|
||||||
|
@ApiBody({ schema: { example: { tags: ['blog-posts'] } } })
|
||||||
|
async purgeTags(@Body('tags') tags: string[]) {
|
||||||
|
return this.cloudflareService.purgeCache({ tags });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('cache/purge-hosts')
|
||||||
|
@ApiOperation({ summary: 'Purge Cache by Hosts' })
|
||||||
|
@ApiBody({ schema: { example: { hosts: ['api.example.com'] } } })
|
||||||
|
async purgeHosts(@Body('hosts') hosts: string[]) {
|
||||||
|
return this.cloudflareService.purgeCache({ hosts });
|
||||||
|
}
|
||||||
|
}
|
||||||
79
backoffice/src/admin/cloudflare.service.ts
Normal file
79
backoffice/src/admin/cloudflare.service.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { firstValueFrom, catchError } from 'rxjs';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CloudflareService {
|
||||||
|
private readonly logger = new Logger(CloudflareService.name);
|
||||||
|
private readonly apiToken: string;
|
||||||
|
private readonly zoneId: string;
|
||||||
|
private readonly baseUrl = 'https://api.cloudflare.com/client/v4';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.apiToken =
|
||||||
|
this.configService.get<string>('CLOUDFLARE_API_TOKEN') || '';
|
||||||
|
this.zoneId = this.configService.get<string>('CLOUDFLARE_ZONE_ID') || '';
|
||||||
|
|
||||||
|
if (!this.apiToken) {
|
||||||
|
this.logger.warn('CLOUDFLARE_API_TOKEN is not set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHeaders() {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${this.apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async listZones(): Promise<any> {
|
||||||
|
const url = `${this.baseUrl}/zones`;
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(url, { headers: this.getHeaders() }).pipe(
|
||||||
|
catchError((error: AxiosError) => {
|
||||||
|
this.logger.error(error.response?.data || error.message);
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to fetch Cloudflare zones',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async purgeCache(params: {
|
||||||
|
purge_everything?: boolean;
|
||||||
|
files?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
hosts?: string[];
|
||||||
|
}): Promise<any> {
|
||||||
|
if (!this.zoneId) {
|
||||||
|
throw new HttpException(
|
||||||
|
'CLOUDFLARE_ZONE_ID is not configured',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/zones/${this.zoneId}/purge_cache`;
|
||||||
|
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.post(url, params, { headers: this.getHeaders() }).pipe(
|
||||||
|
catchError((error: AxiosError) => {
|
||||||
|
this.logger.error(error.response?.data || error.message);
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to purge Cloudflare cache',
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue