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  }