github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/instances/fixers.go (about) 1 package instances 2 3 import ( 4 "encoding/json" 5 "errors" 6 "net/http" 7 8 "github.com/cozy/cozy-stack/model/account" 9 "github.com/cozy/cozy-stack/model/app" 10 "github.com/cozy/cozy-stack/model/bitwarden/settings" 11 "github.com/cozy/cozy-stack/model/instance" 12 "github.com/cozy/cozy-stack/model/instance/lifecycle" 13 "github.com/cozy/cozy-stack/model/job" 14 "github.com/cozy/cozy-stack/model/stack" 15 "github.com/cozy/cozy-stack/pkg/consts" 16 "github.com/cozy/cozy-stack/pkg/couchdb" 17 "github.com/labstack/echo/v4" 18 ) 19 20 func passwordDefinedFixer(c echo.Context) error { 21 domain := c.Param("domain") 22 inst, err := lifecycle.GetInstance(domain) 23 if err != nil { 24 return err 25 } 26 27 if inst.PasswordDefined != nil { 28 return c.NoContent(http.StatusNoContent) 29 } 30 31 defined := false 32 if inst.OnboardingFinished { 33 defined = true 34 if inst.HasForcedOIDC() || inst.MagicLink { 35 bitwarden, err := settings.Get(inst) 36 if err == nil && !bitwarden.ExtensionInstalled { 37 defined = false 38 } 39 } 40 } 41 inst.PasswordDefined = &defined 42 if err := instance.Update(inst); err != nil { 43 return c.JSON(http.StatusInternalServerError, echo.Map{ 44 "error": err, 45 }) 46 } 47 48 return c.NoContent(http.StatusNoContent) 49 } 50 51 func orphanAccountFixer(c echo.Context) error { 52 domain := c.Param("domain") 53 inst, err := lifecycle.GetInstance(domain) 54 if err != nil { 55 return err 56 } 57 58 var accounts []*account.Account 59 err = couchdb.GetAllDocs(inst, consts.Accounts, nil, &accounts) 60 if err != nil || len(accounts) == 0 { 61 return err 62 } 63 64 var konnectors []*couchdb.JSONDoc 65 err = couchdb.GetAllDocs(inst, consts.Konnectors, nil, &konnectors) 66 if err != nil { 67 return err 68 } 69 70 var slugsToDelete []string 71 for _, acc := range accounts { 72 if acc.AccountType == "" { 73 continue // Skip the design docs 74 } 75 found := false 76 for _, konn := range konnectors { 77 if konn.M["slug"] == acc.AccountType { 78 found = true 79 break 80 } 81 } 82 if !found { 83 for _, slug := range slugsToDelete { 84 if slug == acc.AccountType { 85 found = true 86 break 87 } 88 } 89 if !found { 90 slugsToDelete = append(slugsToDelete, acc.AccountType) 91 } 92 } 93 } 94 if len(slugsToDelete) == 0 { 95 return nil 96 } 97 98 if _, _, err = stack.Start(); err != nil { 99 return err 100 } 101 jobsSystem := job.System() 102 log := inst.Logger().WithNamespace("fixer") 103 copier := app.Copier(consts.KonnectorType, inst) 104 105 for _, slug := range slugsToDelete { 106 opts := &app.InstallerOptions{ 107 Operation: app.Install, 108 Type: consts.KonnectorType, 109 SourceURL: "registry://" + slug + "/stable", 110 Slug: slug, 111 Registries: inst.Registries(), 112 } 113 ins, err := app.NewInstaller(inst, copier, opts) 114 if err != nil { 115 return err 116 } 117 if _, err = ins.RunSync(); err != nil { 118 return err 119 } 120 121 for _, acc := range accounts { 122 if acc.AccountType != slug { 123 continue 124 } 125 acc.ManualCleaning = true 126 oldRev := acc.Rev() // The deletion job needs the rev just before the deletion 127 if err := couchdb.DeleteDoc(inst, acc); err != nil { 128 log.Errorf("Cannot delete account: %v", err) 129 } 130 j, err := account.PushAccountDeletedJob(jobsSystem, inst, acc.ID(), oldRev, slug) 131 if err != nil { 132 log.Errorf("Cannot push a job for account deletion: %v", err) 133 } 134 if err = j.WaitUntilDone(inst); err != nil { 135 log.Error(err.Error()) 136 } 137 } 138 opts.Operation = app.Delete 139 ins, err = app.NewInstaller(inst, copier, opts) 140 if err != nil { 141 return err 142 } 143 if _, err = ins.RunSync(); err != nil { 144 return err 145 } 146 } 147 148 return c.NoContent(http.StatusNoContent) 149 } 150 151 type serviceMessage struct { 152 Slug string `json:"slug"` 153 Name string `json:"name"` 154 // and some other fields not needed here 155 } 156 157 func serviceTriggersFixer(c echo.Context) error { 158 domain := c.Param("domain") 159 inst, err := lifecycle.GetInstance(domain) 160 if err != nil { 161 return err 162 } 163 164 jobsSystem := job.System() 165 triggers, err := jobsSystem.GetAllTriggers(inst) 166 if err != nil { 167 return err 168 } 169 byApps := make(map[string][]job.Trigger) 170 for _, trigger := range triggers { 171 trigger := trigger 172 infos := trigger.Infos() 173 if infos.WorkerType != "service" { 174 continue 175 } 176 if infos.Type == "@at" { 177 continue 178 } 179 var msg serviceMessage 180 if err := json.Unmarshal(infos.Message, &msg); err != nil { 181 continue 182 } 183 list := byApps[msg.Slug] 184 list = append(list, trigger) 185 byApps[msg.Slug] = list 186 } 187 188 var toDelete []job.Trigger 189 recreated := 0 190 updated := 0 191 192 for slug, triggers := range byApps { 193 manifest, err := app.GetWebappBySlug(inst, slug) 194 if errors.Is(err, app.ErrNotFound) { 195 // The app has been uninstalled, but some duplicate triggers has 196 // been left 197 toDelete = append(toDelete, triggers...) 198 continue 199 } else if err != nil { 200 return err 201 } 202 203 // Fill the trigger ids for the services when they are missing. 204 updateApp := false 205 for name, service := range manifest.Services() { 206 if service.TriggerOptions == "" { 207 continue 208 } 209 var recreate bool 210 if service.TriggerID == "" { 211 for _, trigger := range triggers { 212 infos := trigger.Infos() 213 if infos.Debounce != service.Debounce { 214 continue 215 } 216 opts := infos.Type + " " + infos.Arguments 217 if opts != service.TriggerOptions { 218 continue 219 } 220 var msg serviceMessage 221 if err := json.Unmarshal(infos.Message, &msg); err != nil { 222 continue 223 } 224 if msg.Name != name { 225 continue 226 } 227 service.TriggerID = infos.TID 228 updateApp = true 229 break 230 } 231 recreate = service.TriggerID == "" 232 } else { 233 trigger, err := jobsSystem.GetTrigger(inst, service.TriggerID) 234 recreate = errors.Is(err, job.ErrNotFoundTrigger) 235 if err == nil { 236 var msg serviceMessage 237 if err := json.Unmarshal(trigger.Infos().Message, &msg); err != nil { 238 return err 239 } 240 if msg.Name == "" { 241 fixTriggerName(inst, trigger, msg, name) 242 updated++ 243 } 244 } 245 } 246 247 if recreate { 248 triggerID, err := app.CreateServiceTrigger(inst, slug, name, service) 249 if err != nil { 250 return err 251 } 252 service.TriggerID = triggerID 253 updateApp = true 254 recreated++ 255 } 256 } 257 258 if updateApp { 259 if err := couchdb.UpdateDoc(inst, manifest); err != nil { 260 return err 261 } 262 } 263 264 // Add to the list of triggers that should be deleted all the triggers 265 // for this application that are not tied to a service. 266 for _, trigger := range triggers { 267 trigger := trigger 268 tid := trigger.Infos().TID 269 found := false 270 for _, service := range manifest.Services() { 271 if service.TriggerID == tid { 272 found = true 273 } 274 } 275 if !found { 276 toDelete = append(toDelete, trigger) 277 } 278 } 279 } 280 281 for _, trigger := range toDelete { 282 if err := jobsSystem.DeleteTrigger(inst, trigger.ID()); err != nil { 283 return err 284 } 285 } 286 287 return c.JSON(http.StatusOK, echo.Map{ 288 "Domain": domain, 289 "RecreatedTriggersCount": recreated, 290 "UpdatedTriggersCount": updated, 291 "DeletedTriggersCount": len(toDelete), 292 }) 293 } 294 295 func fixTriggerName(inst *instance.Instance, trigger job.Trigger, msg serviceMessage, name string) error { 296 msg.Name = name 297 raw, err := json.Marshal(msg) 298 if err != nil { 299 return err 300 } 301 infos := trigger.Infos().Clone().(*job.TriggerInfos) 302 infos.Message = job.Message(raw) 303 return couchdb.UpdateDoc(inst, infos) 304 } 305 306 func indexesFixer(c echo.Context) error { 307 domain := c.Param("domain") 308 inst, err := lifecycle.GetInstance(domain) 309 if err != nil { 310 return err 311 } 312 313 if err := lifecycle.DefineViewsAndIndex(inst); err != nil { 314 return err 315 } 316 317 return c.NoContent(http.StatusNoContent) 318 }