edit: unique code shortlink ambil dari tabel shortcodes
This commit is contained in:
54
cmd/seed_shortcodes/seed_shortcodes.go
Normal file
54
cmd/seed_shortcodes/seed_shortcodes.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
/*
|
||||
* HOW TO USE:
|
||||
* `go run cmd/seed_shortcodes/seed_shortcodes.go -dsn="user:password@tcp(host_db:port)/nama_db_ohif_proxy"`
|
||||
*
|
||||
* Ini akan seeding DB dengan 3333 data (sesuai var count)
|
||||
*/
|
||||
|
||||
// Parse command line flags
|
||||
count := flag.Int("count", 3333, "Number of shortcodes to generate")
|
||||
dsn := flag.String("dsn", "", "Database connection string (required)")
|
||||
flag.Parse()
|
||||
|
||||
// Check if DSN is provided
|
||||
if *dsn == "" {
|
||||
fmt.Println("Error: Database connection string (DSN) is required")
|
||||
fmt.Println("Usage: go run seed_shortcodes.go -dsn=user:password@tcp(localhost:3306)/database_name -count=1000")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize database connection
|
||||
err := database.Initialize(*dsn, 10, 5, 60*time.Minute)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to connect to database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Create a shortcode repository
|
||||
shortCodeRepo := repository.NewShortCodeRepository()
|
||||
|
||||
// Seed the shortcodes
|
||||
fmt.Printf("Seeding %d shortcodes...\n", *count)
|
||||
err = shortCodeRepo.SeedShortCodes(*count)
|
||||
if err != nil {
|
||||
fmt.Printf("Error seeding shortcodes: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Shortcode seeding completed successfully!")
|
||||
}
|
||||
@@ -127,3 +127,38 @@ func (h *ShortLinkHandler) ShortLinkAuth(w http.ResponseWriter, r *http.Request)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// RecycleShortcodes handles requests to recycle shortcodes from expired shortlinks
|
||||
func (h *ShortLinkHandler) RecycleShortcodes(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow admin role to recycle shortcodes
|
||||
userRole, ok := r.Context().Value(middleware.UserRoleKey).(string)
|
||||
if !ok || userRole != "admin" {
|
||||
h.logger.Warn("Unauthorized attempt to recycle shortcodes",
|
||||
zap.String("role", userRole))
|
||||
http.Error(w, "Only admin can recycle shortcodes", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service to recycle shortcodes
|
||||
count, err := h.shortLinkService.CleanupExpiredShortcodes()
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to recycle shortcodes", zap.Error(err))
|
||||
http.Error(w, "Failed to recycle shortcodes", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log successful recycling
|
||||
h.logger.Info("Shortcodes recycled successfully",
|
||||
zap.Int("count", count))
|
||||
|
||||
// Return response
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": "Shortcodes recycled successfully",
|
||||
"count": count,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
218
internal/api/repository/shortcode.go
Normal file
218
internal/api/repository/shortcode.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// DBShortCode represents a shortcode from the database
|
||||
type DBShortCode struct {
|
||||
ID int `db:"id"`
|
||||
Shortcode string `db:"Shortcode"`
|
||||
ShortcodeIsUsed string `db:"ShortcodeIsUsed"`
|
||||
ShortcodeIsUsedBy_ShortlinkID *int `db:"ShortcodeIsUsedBy_ShortlinkID"`
|
||||
ShortcodeCreatedAt time.Time `db:"ShortcodeCreatedAt"`
|
||||
ShortcodeUpdatedAt time.Time `db:"ShortcodeUpdatedAt"`
|
||||
}
|
||||
|
||||
// ShortCodeRepository handles database operations related to shortcodes
|
||||
type ShortCodeRepository struct {
|
||||
*Repository
|
||||
}
|
||||
|
||||
// NewShortCodeRepository creates a new shortcode repository
|
||||
func NewShortCodeRepository() *ShortCodeRepository {
|
||||
return &ShortCodeRepository{
|
||||
Repository: NewRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetUnusedShortCode retrieves an unused shortcode from the database
|
||||
func (r *ShortCodeRepository) GetUnusedShortCode() (*string, error) {
|
||||
var shortcode string
|
||||
|
||||
query := `SELECT Shortcode FROM shortcodes
|
||||
WHERE ShortcodeIsUsed = 'N'
|
||||
ORDER BY RAND()
|
||||
LIMIT 1`
|
||||
|
||||
err := database.DB.Get(&shortcode, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database error getting unused shortcode: %w", err)
|
||||
}
|
||||
|
||||
return &shortcode, nil
|
||||
}
|
||||
|
||||
// MarkShortCodeAsUsed marks a shortcode as used by a specific shortlink
|
||||
func (r *ShortCodeRepository) MarkShortCodeAsUsed(tx *sqlx.Tx, shortcode string, shortlinkID int) error {
|
||||
query := `UPDATE shortcodes
|
||||
SET ShortcodeIsUsed = 'Y',
|
||||
ShortcodeIsUsedBy_ShortlinkID = ?
|
||||
WHERE Shortcode = ?`
|
||||
|
||||
_, err := tx.Exec(query, shortlinkID, shortcode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error marking shortcode as used: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkShortCodeAsUnused marks a shortcode as unused when shortlink expires or is deleted
|
||||
func (r *ShortCodeRepository) MarkShortCodeAsUnused(shortcode string) error {
|
||||
query := `UPDATE shortcodes
|
||||
SET ShortcodeIsUsed = 'N',
|
||||
ShortcodeIsUsedBy_ShortlinkID = NULL
|
||||
WHERE Shortcode = ?`
|
||||
|
||||
_, err := database.DB.Exec(query, shortcode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error marking shortcode as unused: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkShortCodeAsUnusedByShortlinkID marks a shortcode as unused by shortlink ID
|
||||
func (r *ShortCodeRepository) MarkShortCodeAsUnusedByShortlinkID(shortlinkID int) error {
|
||||
query := `UPDATE shortcodes
|
||||
SET ShortcodeIsUsed = 'N',
|
||||
ShortcodeIsUsedBy_ShortlinkID = NULL
|
||||
WHERE ShortcodeIsUsedBy_ShortlinkID = ?`
|
||||
|
||||
_, err := database.DB.Exec(query, shortlinkID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database error marking shortcode as unused by shortlink ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkExpiredShortlinkShortcodesAsUnused finds all shortcodes associated with expired shortlinks
|
||||
// and marks them as unused
|
||||
func (r *ShortCodeRepository) MarkExpiredShortlinkShortcodesAsUnused() (int, error) {
|
||||
query := `
|
||||
UPDATE shortcodes sc
|
||||
INNER JOIN shortlink sl ON sc.ShortcodeIsUsedBy_ShortlinkID = sl.ShortlinkID
|
||||
SET sc.ShortcodeIsUsed = 'N',
|
||||
sc.ShortcodeIsUsedBy_ShortlinkID = NULL
|
||||
WHERE (sl.ShortlinkExpiredAt < NOW() OR sl.ShortlinkIsRevoked = TRUE)
|
||||
AND sc.ShortcodeIsUsed = 'Y'
|
||||
`
|
||||
|
||||
result, err := database.DB.Exec(query)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("database error freeing expired shortlink shortcodes: %w", err)
|
||||
}
|
||||
|
||||
count, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error getting affected rows count: %w", err)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
// GenerateUniqueShortCode generates a unique 5-character capital letter shortcode
|
||||
func GenerateUniqueShortCode() string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const codeLength = 5
|
||||
|
||||
code := make([]byte, codeLength)
|
||||
for i := range code {
|
||||
code[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
|
||||
return string(code)
|
||||
}
|
||||
|
||||
// SeedShortCodes seeds the shortcodes table with n unique 5-character codes
|
||||
func (r *ShortCodeRepository) SeedShortCodes(n int) error {
|
||||
// Initialize random seed
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Use a transaction for better performance
|
||||
tx, err := database.DB.Beginx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start transaction: %w", err)
|
||||
}
|
||||
|
||||
// Set up deferred rollback that will be canceled if we commit
|
||||
defer func() {
|
||||
if tx != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Check how many shortcodes already exist
|
||||
var count int
|
||||
err = tx.Get(&count, "SELECT COUNT(*) FROM shortcodes")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to count existing shortcodes: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d existing shortcodes, generating %d more\n", count, n)
|
||||
|
||||
// Track already generated codes to avoid duplicates
|
||||
generatedCodes := make(map[string]bool)
|
||||
|
||||
// Get existing codes to avoid duplicates
|
||||
existingCodes := []string{}
|
||||
err = tx.Select(&existingCodes, "SELECT Shortcode FROM shortcodes")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve existing shortcodes: %w", err)
|
||||
}
|
||||
|
||||
// Add existing codes to the map
|
||||
for _, code := range existingCodes {
|
||||
generatedCodes[code] = true
|
||||
}
|
||||
|
||||
// Prepare the insert statement
|
||||
stmt, err := tx.Prepare("INSERT INTO shortcodes (Shortcode, ShortcodeIsUsed) VALUES (?, 'N')")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// Generate and insert the shortcodes
|
||||
inserted := 0
|
||||
attempts := 0
|
||||
maxAttempts := n * 10 // Limiting attempts to avoid infinite loop
|
||||
|
||||
for inserted < n && attempts < maxAttempts {
|
||||
attempts++
|
||||
code := GenerateUniqueShortCode()
|
||||
|
||||
if !generatedCodes[code] {
|
||||
_, err := stmt.Exec(code)
|
||||
if err != nil {
|
||||
continue // Skip if insertion fails and try another code
|
||||
}
|
||||
|
||||
generatedCodes[code] = true
|
||||
inserted++
|
||||
|
||||
// Print progress every 100 codes
|
||||
if inserted%100 == 0 {
|
||||
fmt.Printf("Generated %d/%d codes\n", inserted, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
// Clear the tx to prevent the deferred rollback
|
||||
tx = nil
|
||||
|
||||
fmt.Printf("Successfully generated %d unique shortcodes\n", inserted)
|
||||
return nil
|
||||
}
|
||||
@@ -108,6 +108,9 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
shortLinkHandler := handlers.NewShortLinkHandler(logger, shortLinkService)
|
||||
r.Post("/generate-link", shortLinkHandler.GenerateShortLink)
|
||||
|
||||
// Shortcode recycling - only for admin role (role check is in the handler)
|
||||
r.Post("/recycle-shortcode", shortLinkHandler.RecycleShortcodes)
|
||||
|
||||
// DICOM Web routes
|
||||
r.Route("/dicomWeb", func(r chi.Router) {
|
||||
// Add audit logging middleware to DICOM routes
|
||||
|
||||
@@ -24,8 +24,8 @@ const (
|
||||
// DefaultMaxTries is the default number of login attempts allowed for a shortlink
|
||||
DefaultMaxTries = 5
|
||||
|
||||
// ShortTokenLength is the length of the generated short token
|
||||
ShortTokenLength = 8
|
||||
// ShortTokenLength is the length of the generated short token. 5 digit kapital semua
|
||||
ShortTokenLength = 5
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,6 +36,7 @@ var (
|
||||
ErrCreationFailed = errors.New("failed to create short link")
|
||||
ErrInvalidStudyUID = errors.New("invalid or missing StudyInstanceUID")
|
||||
ErrAdminRoleRequired = errors.New("admin role required to generate shortlinks")
|
||||
ErrNoShortcodes = errors.New("no unused shortcodes available")
|
||||
)
|
||||
|
||||
// ShortLinkService handles operations related to short links
|
||||
@@ -43,6 +44,7 @@ type ShortLinkService struct {
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
shortLinkRepo *repository.ShortLinkRepository
|
||||
shortCodeRepo *repository.ShortCodeRepository
|
||||
patientRepo *repository.PatientRepository
|
||||
// Configuration settings
|
||||
baseURL string
|
||||
@@ -69,6 +71,7 @@ func NewShortLinkService(jwtManager *auth.JWTManager, logger *zap.Logger, baseUR
|
||||
jwtManager: jwtManager,
|
||||
logger: logger,
|
||||
shortLinkRepo: repository.NewShortLinkRepository(),
|
||||
shortCodeRepo: repository.NewShortCodeRepository(),
|
||||
patientRepo: repository.NewPatientRepository(),
|
||||
baseURL: baseURL,
|
||||
defaultExpiryTime: time.Duration(defaultExpiryHours) * time.Hour,
|
||||
@@ -127,31 +130,10 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
||||
}
|
||||
expiresAt := time.Now().Add(expiresIn)
|
||||
|
||||
// Generate a secure random token
|
||||
token, err := generateSecureToken(ShortTokenLength)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate secure token", zap.Error(err))
|
||||
return nil, ErrCreationFailed
|
||||
}
|
||||
|
||||
// Hash the DOB for secure storage
|
||||
hashedDOB := hashDOB(dob)
|
||||
|
||||
// Create the short link record
|
||||
shortLink := &models.ShortLink{
|
||||
ID: generateID(),
|
||||
Token: token,
|
||||
PatientID: req.PatientID,
|
||||
StudyUID: req.StudyUID,
|
||||
HashedDOB: hashedDOB,
|
||||
ExpiresAt: expiresAt.Format(time.RFC3339),
|
||||
IsRevoked: false,
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
CreatedByID: creatorID,
|
||||
RemainingTries: s.maxAttempts,
|
||||
}
|
||||
|
||||
// Start a transaction for creating the shortlink
|
||||
// Start a transaction for creating the shortlink and updating the shortcode
|
||||
tx, err := database.DB.Beginx()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to start transaction", zap.Error(err))
|
||||
@@ -165,6 +147,31 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
||||
}
|
||||
}()
|
||||
|
||||
// Get an unused shortcode from the bank
|
||||
unusedShortcode, err := s.shortCodeRepo.GetUnusedShortCode()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get unused shortcode", zap.Error(err))
|
||||
return nil, ErrNoShortcodes
|
||||
}
|
||||
|
||||
if unusedShortcode == nil {
|
||||
s.logger.Error("No unused shortcodes available in the bank")
|
||||
return nil, ErrNoShortcodes
|
||||
}
|
||||
|
||||
// Create the short link record
|
||||
shortLink := &models.ShortLink{
|
||||
Token: *unusedShortcode,
|
||||
PatientID: req.PatientID,
|
||||
StudyUID: req.StudyUID,
|
||||
HashedDOB: hashedDOB,
|
||||
ExpiresAt: expiresAt.Format(time.RFC3339),
|
||||
IsRevoked: false,
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
CreatedByID: creatorID,
|
||||
RemainingTries: s.maxAttempts,
|
||||
}
|
||||
|
||||
// Store the short link in the database using transaction
|
||||
err = s.shortLinkRepo.CreateShortLinkTx(tx, shortLink)
|
||||
if err != nil {
|
||||
@@ -172,6 +179,21 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
||||
return nil, ErrCreationFailed
|
||||
}
|
||||
|
||||
// Get the ID of the created shortlink
|
||||
var shortlinkID int
|
||||
err = tx.Get(&shortlinkID, "SELECT ShortlinkID FROM shortlink WHERE ShortlinkCode = ?", shortLink.Token)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get shortlink ID", zap.Error(err))
|
||||
return nil, ErrCreationFailed
|
||||
}
|
||||
|
||||
// Mark the shortcode as used
|
||||
err = s.shortCodeRepo.MarkShortCodeAsUsed(tx, shortLink.Token, shortlinkID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to mark shortcode as used", zap.Error(err))
|
||||
return nil, ErrCreationFailed
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||
@@ -182,10 +204,10 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
||||
tx = nil
|
||||
|
||||
// Generate the full URL using the configured base URL
|
||||
fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, token)
|
||||
fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, shortLink.Token)
|
||||
|
||||
return &models.GenerateShortLinkResponse{
|
||||
ShortToken: token,
|
||||
ShortToken: shortLink.Token,
|
||||
FullURL: fullURL,
|
||||
ExpiresAt: shortLink.ExpiresAt,
|
||||
IsExisting: false,
|
||||
@@ -367,6 +389,21 @@ func (s *ShortLinkService) AuthenticateWithShortLink(req *models.ShortLinkAuthRe
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CleanupExpiredShortcodes checks for expired shortlinks and frees their shortcodes
|
||||
func (s *ShortLinkService) CleanupExpiredShortcodes() (int, error) {
|
||||
count, err := s.shortCodeRepo.MarkExpiredShortlinkShortcodesAsUnused()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to clean up expired shortcodes", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
s.logger.Info("Freed shortcodes from expired shortlinks", zap.Int("count", count))
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// hashDOB creates a secure hash of a date of birth
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@baseUrl = http://localhost:5555
|
||||
@adminToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJ1c2VyX25hbWUiOiJBZG1pbiBVc2VyIiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImhvbWVfdXJsIjoiLyIsInN0dWR5X2xpc3QiOiJlbmFibGVkIiwiZXhwIjoxNzQ3MzY0NjgxLCJpYXQiOjE3NDcyNzgyODF9.bkzxV8f26wN_r6uI5T6o58TNX4U0z-Wel1hCAl5-8ag
|
||||
|
||||
### 1. Generate Short Link Single Study
|
||||
### 1. Generate Short Link Single Study (Tidak ganti untuk 1 patient sampai expired)
|
||||
POST {{baseUrl}}/generate-link
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{adminToken}}
|
||||
@@ -43,8 +43,8 @@ POST {{baseUrl}}/auth/shortlink
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"short_token": "aePWCTrU",
|
||||
"dob": "1980-01-01"
|
||||
"short_token": "DZDWF",
|
||||
"dob": "2001-10-07"
|
||||
}
|
||||
|
||||
### Authenticate with Incorrect DOB
|
||||
@@ -56,9 +56,12 @@ Content-Type: application/json
|
||||
"dob": "2001-10-00"
|
||||
}
|
||||
|
||||
|
||||
### 4. Access DICOM data with the JWT from successful shortlink auth
|
||||
# Replace with token from successful auth in request #2
|
||||
GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.20180713036
|
||||
### Recycle shortcode from expired or revoked token
|
||||
POST {{baseUrl}}/recycle-shortcode
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2hvcnRsaW5rXzIiLCJlbWFpbCI6InBhdGllbnRfMkBzaG9ydGxpbmsubG9jYWwiLCJyb2xlIjoicGF0aWVudCIsInVzZXJfbmFtZSI6IlBhdGllbnQiLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwicGF0aWVudF9pZCI6IjAwMjExNjIyIiwicGF0aWVudF9uYW1lIjoiUGF0aWVudCIsInN0dWR5X2l1aWRzIjpbIjEuMi44MjYuMC4xLjM2ODAwNDMuOS43MzA3LjEuMjAxODA3MTMwMzYiXSwiaG9tZV91cmwiOiJ2aWV3ZXI_U3R1ZHlJbnN0YW5jZVVJRHM9MS4yLjgyNi4wLjEuMzY4MDA0My45LjczMDcuMS4yMDE4MDcxMzAzNiIsInN0dWR5X2xpc3QiOiJkaXNhYmxlZCIsImV4cCI6MTc0NzM2OTc1NSwiaWF0IjoxNzQ3MjgzMzU1fQ.rj1Xr7StW_O3rE2Pwq6L-WGBAW1tFcUq8bt5nu4u050
|
||||
### Check the response from recycling
|
||||
# The response should include:
|
||||
# - status: "success"
|
||||
# - message: "Shortcodes recycled successfully"
|
||||
# - count: number of recycled shortcodes (integer)
|
||||
|
||||
Reference in New Issue
Block a user