44 "encoding/json"
55 "net/http"
66 "path/filepath"
7+ "strconv"
78
89 "crowdsec-manager/internal/config"
910 "crowdsec-manager/internal/database"
@@ -69,8 +70,18 @@ func SaveCaptchaConfig(db *database.Database) gin.HandlerFunc {
6970 }
7071}
7172
73+ // captchaApplyStep is a named step in the captcha apply pipeline.
74+ type captchaApplyStep struct {
75+ Num int
76+ Name string
77+ Run func (req models.CaptchaSetupRequest ) error
78+ }
79+
7280// ApplyCaptchaConfig reads the captcha configuration saved by SaveCaptchaConfig and applies it
7381// to all systems: captcha HTML, Traefik dynamic config, CrowdSec profiles, and container restarts.
82+ //
83+ // Supports an optional "step" query parameter to re-run a single step (e.g. ?step=4).
84+ // When step is omitted, all steps run sequentially.
7485func ApplyCaptchaConfig (dockerClient * docker.Client , db * database.Database , cfg * config.Config ) gin.HandlerFunc {
7586 return func (c * gin.Context ) {
7687 dockerClient = resolveDockerClient (c , dockerClient )
@@ -94,91 +105,119 @@ func ApplyCaptchaConfig(dockerClient *docker.Client, db *database.Database, cfg
94105 return
95106 }
96107
97- steps := []gin.H {}
98-
99- // Step 1: Write captcha.html to host filesystem.
100- htmlErr := createCaptchaHTML (cfg , req .Provider , req .SiteKey )
101- steps = append (steps , gin.H {
102- "step" : 1 ,
103- "name" : "Create captcha HTML page" ,
104- "success" : htmlErr == nil ,
105- "error" : errString (htmlErr ),
106- })
108+ // Parse optional step filter.
109+ var onlyStep int
110+ if stepStr := c .Query ("step" ); stepStr != "" {
111+ parsed , parseErr := strconv .Atoi (stepStr )
112+ if parseErr != nil || parsed < 1 || parsed > 6 {
113+ c .JSON (http .StatusBadRequest , models.Response {
114+ Success : false ,
115+ Error : "Invalid step parameter. Must be 1-6." ,
116+ })
117+ return
118+ }
119+ onlyStep = parsed
120+ }
107121
108- // Step 2: Update Traefik dynamic_config.yml .
122+ // Define the pipeline of steps .
109123 traefikConfigDir := filepath .Join (cfg .ConfigDir , "traefik" )
110- traefikErr := updateTraefikCaptchaConfig (dockerClient , cfg , req , traefikConfigDir )
111- steps = append (steps , gin.H {
112- "step" : 2 ,
113- "name" : "Update Traefik dynamic config" ,
114- "success" : traefikErr == nil ,
115- "error" : errString (traefikErr ),
116- })
117124
118- // Step 3: Update CrowdSec profiles.yaml.
119- profilesErr := updateCrowdSecProfiles (dockerClient , cfg )
120- steps = append (steps , gin.H {
121- "step" : 3 ,
122- "name" : "Update CrowdSec profiles" ,
123- "success" : profilesErr == nil ,
124- "error" : errString (profilesErr ),
125- })
125+ pipeline := []captchaApplyStep {
126+ {
127+ Num : 1 ,
128+ Name : "Create captcha HTML page" ,
129+ Run : func (r models.CaptchaSetupRequest ) error {
130+ return createCaptchaHTML (cfg , r .Provider , r .SiteKey )
131+ },
132+ },
133+ {
134+ Num : 2 ,
135+ Name : "Update Traefik dynamic config" ,
136+ Run : func (r models.CaptchaSetupRequest ) error {
137+ return updateTraefikCaptchaConfig (dockerClient , cfg , r , traefikConfigDir )
138+ },
139+ },
140+ {
141+ Num : 3 ,
142+ Name : "Update CrowdSec profiles" ,
143+ Run : func (_ models.CaptchaSetupRequest ) error {
144+ return updateCrowdSecProfiles (dockerClient , cfg )
145+ },
146+ },
147+ {
148+ Num : 4 ,
149+ Name : "Restart Traefik" ,
150+ Run : func (_ models.CaptchaSetupRequest ) error {
151+ return restartTraefikContainer (dockerClient , cfg )
152+ },
153+ },
154+ {
155+ Num : 5 ,
156+ Name : "Restart CrowdSec" ,
157+ Run : func (_ models.CaptchaSetupRequest ) error {
158+ return restartCrowdSecContainer (dockerClient , cfg )
159+ },
160+ },
161+ {
162+ Num : 6 ,
163+ Name : "Verify setup" ,
164+ Run : func (_ models.CaptchaSetupRequest ) error {
165+ // Verification is implicit — if we got here, previous steps ran.
166+ return nil
167+ },
168+ },
169+ }
126170
127- // Step 4: Restart Traefik (stop + start for clean config reload).
128- traefikRestartErr := restartTraefikContainer (dockerClient , cfg )
129- steps = append (steps , gin.H {
130- "step" : 4 ,
131- "name" : "Restart Traefik" ,
132- "success" : traefikRestartErr == nil ,
133- "error" : errString (traefikRestartErr ),
134- })
171+ // Execute steps.
172+ steps := []gin.H {}
173+ allOK := true
135174
136- // Step 5: Restart CrowdSec.
137- csRestartErr := restartCrowdSecContainer (dockerClient , cfg )
138- steps = append (steps , gin.H {
139- "step" : 5 ,
140- "name" : "Restart CrowdSec" ,
141- "success" : csRestartErr == nil ,
142- "error" : errString (csRestartErr ),
143- })
175+ for _ , s := range pipeline {
176+ if onlyStep > 0 && s .Num != onlyStep {
177+ continue
178+ }
144179
145- // Step 6: Verify the full setup.
146- verified := htmlErr == nil && traefikErr == nil && profilesErr == nil
147- steps = append (steps , gin.H {
148- "step" : 6 ,
149- "name" : "Verify setup" ,
150- "success" : verified ,
151- })
180+ stepErr := s .Run (req )
181+ success := stepErr == nil
182+ if ! success {
183+ allOK = false
184+ }
152185
153- // Mark as applied in DB when all critical steps (HTML + Traefik + profiles) passed.
154- allCriticalOK := htmlErr == nil && traefikErr == nil && profilesErr == nil
155- if allCriticalOK {
186+ steps = append (steps , gin.H {
187+ "step" : s .Num ,
188+ "name" : s .Name ,
189+ "success" : success ,
190+ "error" : errString (stepErr ),
191+ })
192+ }
193+
194+ // Mark as applied in DB when all steps pass in a full run.
195+ if onlyStep == 0 && allOK {
156196 if markErr := db .MarkFeatureApplied ("captcha" ); markErr != nil {
157197 logger .Warn ("Failed to mark captcha as applied in DB" , "error" , markErr )
158198 }
159199 }
160200
161201 message := "Captcha applied successfully"
162- if ! allCriticalOK {
202+ if ! allOK {
163203 message = "Captcha applied with some errors — check step details"
164204 }
205+ if onlyStep > 0 {
206+ if allOK {
207+ message = "Step re-run succeeded"
208+ } else {
209+ message = "Step re-run failed — check details"
210+ }
211+ }
165212
166213 c .JSON (http .StatusOK , models.Response {
167- Success : allCriticalOK ,
214+ Success : allOK ,
168215 Message : message ,
169216 Data : gin.H {
170217 "steps" : steps ,
171- "applied" : allCriticalOK ,
218+ "applied" : allOK ,
172219 "provider" : req .Provider ,
173220 },
174221 })
175222 }
176223}
177-
178- // errString safely converts an error to a string, returning "" for nil.
179- func errString (err error ) string {
180- if err == nil {
181- return ""
182- }
183- return err .Error ()
184- }
0 commit comments