github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/instances/instances.go (about)

     1  // Package instances is used for the admin endpoint to manage instances. It
     2  // covers a lot of things, from creating an instance to checking the FS
     3  // integrity.
     4  package instances
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/cozy/cozy-stack/model/app"
    16  	"github.com/cozy/cozy-stack/model/instance"
    17  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    18  	"github.com/cozy/cozy-stack/model/notification"
    19  	"github.com/cozy/cozy-stack/model/notification/center"
    20  	"github.com/cozy/cozy-stack/model/oauth"
    21  	"github.com/cozy/cozy-stack/model/session"
    22  	"github.com/cozy/cozy-stack/model/sharing"
    23  	"github.com/cozy/cozy-stack/pkg/consts"
    24  	"github.com/cozy/cozy-stack/pkg/couchdb"
    25  	"github.com/cozy/cozy-stack/pkg/crypto"
    26  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    27  	"github.com/cozy/cozy-stack/pkg/prefixer"
    28  	"github.com/cozy/cozy-stack/pkg/utils"
    29  	"github.com/labstack/echo/v4"
    30  )
    31  
    32  type apiInstance struct {
    33  	*instance.Instance
    34  }
    35  
    36  func (i *apiInstance) MarshalJSON() ([]byte, error) {
    37  	return json.Marshal(i.Instance)
    38  }
    39  
    40  // Links is used to generate a JSON-API link for the instance
    41  func (i *apiInstance) Links() *jsonapi.LinksList {
    42  	return &jsonapi.LinksList{Self: "/instances/" + i.Instance.DocID}
    43  }
    44  
    45  // Relationships is used to generate the content relationship in JSON-API format
    46  func (i *apiInstance) Relationships() jsonapi.RelationshipMap {
    47  	return jsonapi.RelationshipMap{}
    48  }
    49  
    50  // Included is part of the jsonapi.Object interface
    51  func (i *apiInstance) Included() []jsonapi.Object {
    52  	return nil
    53  }
    54  
    55  func createHandler(c echo.Context) error {
    56  	var err error
    57  	opts := &lifecycle.Options{
    58  		Domain:          c.QueryParam("Domain"),
    59  		Locale:          c.QueryParam("Locale"),
    60  		UUID:            c.QueryParam("UUID"),
    61  		OIDCID:          c.QueryParam("OIDCID"),
    62  		FranceConnectID: c.QueryParam("FranceConnectID"),
    63  		TOSSigned:       c.QueryParam("TOSSigned"),
    64  		TOSLatest:       c.QueryParam("TOSLatest"),
    65  		Timezone:        c.QueryParam("Timezone"),
    66  		ContextName:     c.QueryParam("ContextName"),
    67  		Email:           c.QueryParam("Email"),
    68  		PublicName:      c.QueryParam("PublicName"),
    69  		Settings:        c.QueryParam("Settings"),
    70  		AuthMode:        c.QueryParam("AuthMode"),
    71  		Passphrase:      c.QueryParam("Passphrase"),
    72  		Key:             c.QueryParam("Key"),
    73  		Apps:            utils.SplitTrimString(c.QueryParam("Apps"), ","),
    74  	}
    75  	if domainAliases := c.QueryParam("DomainAliases"); domainAliases != "" {
    76  		opts.DomainAliases = strings.Split(domainAliases, ",")
    77  	}
    78  	if sponsorships := c.QueryParam("sponsorships"); sponsorships != "" {
    79  		opts.Sponsorships = strings.Split(sponsorships, ",")
    80  	}
    81  	if featureSets := c.QueryParam("feature_sets"); featureSets != "" {
    82  		opts.FeatureSets = strings.Split(featureSets, ",")
    83  	}
    84  	if autoUpdate := c.QueryParam("AutoUpdate"); autoUpdate != "" {
    85  		b, err := strconv.ParseBool(autoUpdate)
    86  		if err != nil {
    87  			return wrapError(err)
    88  		}
    89  		opts.AutoUpdate = &b
    90  	}
    91  	if magicLink := c.QueryParam("MagicLink"); magicLink != "" {
    92  		ml, err := strconv.ParseBool(magicLink)
    93  		if err != nil {
    94  			return wrapError(err)
    95  		}
    96  		opts.MagicLink = &ml
    97  	}
    98  	if layout := c.QueryParam("SwiftLayout"); layout != "" {
    99  		opts.SwiftLayout, err = strconv.Atoi(layout)
   100  		if err != nil {
   101  			return wrapError(err)
   102  		}
   103  	} else {
   104  		opts.SwiftLayout = -1
   105  	}
   106  	if cluster := c.QueryParam("CouchCluster"); cluster != "" {
   107  		opts.CouchCluster, err = strconv.Atoi(cluster)
   108  		if err != nil {
   109  			return wrapError(err)
   110  		}
   111  	} else {
   112  		opts.CouchCluster = -1
   113  	}
   114  	if diskQuota := c.QueryParam("DiskQuota"); diskQuota != "" {
   115  		opts.DiskQuota, err = strconv.ParseInt(diskQuota, 10, 64)
   116  		if err != nil {
   117  			return wrapError(err)
   118  		}
   119  	}
   120  	if iterations := c.QueryParam("KdfIterations"); iterations != "" {
   121  		iter, err := strconv.Atoi(iterations)
   122  		if err != nil {
   123  			return wrapError(err)
   124  		}
   125  		if iter < crypto.MinPBKDF2Iterations && iter != 0 {
   126  			err := errors.New("The KdfIterations number is too low")
   127  			return jsonapi.InvalidParameter("KdfIterations", err)
   128  		}
   129  		if iter > crypto.MaxPBKDF2Iterations {
   130  			err := errors.New("The KdfIterations number is too high")
   131  			return jsonapi.InvalidParameter("KdfIterations", err)
   132  		}
   133  		opts.KdfIterations = iter
   134  	}
   135  	if traced, err := strconv.ParseBool(c.QueryParam("Trace")); err == nil {
   136  		opts.Traced = &traced
   137  	}
   138  	in, err := lifecycle.Create(opts)
   139  	if err != nil {
   140  		return wrapError(err)
   141  	}
   142  	in.CLISecret = nil
   143  	in.OAuthSecret = nil
   144  	in.SessSecret = nil
   145  	in.PassphraseHash = nil
   146  	return jsonapi.Data(c, http.StatusCreated, &apiInstance{in}, nil)
   147  }
   148  
   149  func showHandler(c echo.Context) error {
   150  	domain := c.Param("domain")
   151  	in, err := lifecycle.GetInstance(domain)
   152  	if err != nil {
   153  		return wrapError(err)
   154  	}
   155  	in.CLISecret = nil
   156  	in.OAuthSecret = nil
   157  	in.SessSecret = nil
   158  	in.PassphraseHash = nil
   159  	return jsonapi.Data(c, http.StatusOK, &apiInstance{in}, nil)
   160  }
   161  
   162  func modifyHandler(c echo.Context) error {
   163  	domain := c.Param("domain")
   164  	opts := &lifecycle.Options{
   165  		Domain:          domain,
   166  		Locale:          c.QueryParam("Locale"),
   167  		UUID:            c.QueryParam("UUID"),
   168  		OIDCID:          c.QueryParam("OIDCID"),
   169  		FranceConnectID: c.QueryParam("FranceConnectID"),
   170  		TOSSigned:       c.QueryParam("TOSSigned"),
   171  		TOSLatest:       c.QueryParam("TOSLatest"),
   172  		Timezone:        c.QueryParam("Timezone"),
   173  		ContextName:     c.QueryParam("ContextName"),
   174  		Email:           c.QueryParam("Email"),
   175  		PublicName:      c.QueryParam("PublicName"),
   176  		Settings:        c.QueryParam("Settings"),
   177  		BlockingReason:  c.QueryParam("BlockingReason"),
   178  	}
   179  	if domainAliases := c.QueryParam("DomainAliases"); domainAliases != "" {
   180  		opts.DomainAliases = strings.Split(domainAliases, ",")
   181  	}
   182  	if sponsorships := c.QueryParam("Sponsorships"); sponsorships != "" {
   183  		opts.Sponsorships = strings.Split(sponsorships, ",")
   184  	}
   185  	if quota := c.QueryParam("DiskQuota"); quota != "" {
   186  		i, err := strconv.ParseInt(quota, 10, 64)
   187  		if err != nil {
   188  			return wrapError(err)
   189  		}
   190  		opts.DiskQuota = i
   191  	}
   192  	if onboardingFinished, err := strconv.ParseBool(c.QueryParam("OnboardingFinished")); err == nil {
   193  		opts.OnboardingFinished = &onboardingFinished
   194  	}
   195  	if magicLink, err := strconv.ParseBool(c.QueryParam("MagicLink")); err == nil {
   196  		opts.MagicLink = &magicLink
   197  	}
   198  	// Deprecated: the Debug parameter should no longer be used, but is kept
   199  	// for compatibility.
   200  	if debug, err := strconv.ParseBool(c.QueryParam("Debug")); err == nil {
   201  		opts.Debug = &debug
   202  	}
   203  	if blocked, err := strconv.ParseBool(c.QueryParam("Blocked")); err == nil {
   204  		opts.Blocked = &blocked
   205  	}
   206  	if from, err := strconv.ParseBool(c.QueryParam("FromCloudery")); err == nil {
   207  		opts.FromCloudery = from
   208  	}
   209  	i, err := lifecycle.GetInstance(domain)
   210  	if err != nil {
   211  		return wrapError(err)
   212  	}
   213  	// XXX we cannot use the lifecycle.Patch function to update the deleting
   214  	// flag, as we may need to update this flag for an instance that no longer
   215  	// has its settings database.
   216  	if deleting, err := strconv.ParseBool(c.QueryParam("Deleting")); err == nil {
   217  		i.Deleting = deleting
   218  		if err := instance.Update(i); err != nil {
   219  			return wrapError(err)
   220  		}
   221  		return jsonapi.Data(c, http.StatusOK, &apiInstance{i}, nil)
   222  	}
   223  	if err = lifecycle.Patch(i, opts); err != nil {
   224  		return wrapError(err)
   225  	}
   226  	return jsonapi.Data(c, http.StatusOK, &apiInstance{i}, nil)
   227  }
   228  
   229  func listHandler(c echo.Context) error {
   230  	var instances []*instance.Instance
   231  	var links *jsonapi.LinksList
   232  	var err error
   233  
   234  	var limit int
   235  	if l := c.QueryParam("page[limit]"); l != "" {
   236  		if converted, err := strconv.Atoi(l); err == nil {
   237  			limit = converted
   238  		}
   239  	}
   240  
   241  	var skip int
   242  	if s := c.QueryParam("page[skip]"); s != "" {
   243  		if converted, err := strconv.Atoi(s); err == nil {
   244  			skip = converted
   245  		}
   246  	}
   247  
   248  	if limit > 0 {
   249  		cursor := c.QueryParam("page[cursor]")
   250  		instances, cursor, err = instance.PaginatedList(limit, cursor, skip)
   251  		if cursor != "" {
   252  			links = &jsonapi.LinksList{
   253  				Next: fmt.Sprintf("/instances?page[limit]=%d&page[cursor]=%s", limit, cursor),
   254  			}
   255  		}
   256  	} else {
   257  		instances, err = instance.List()
   258  	}
   259  	if err != nil {
   260  		if couchdb.IsNoDatabaseError(err) {
   261  			return jsonapi.DataList(c, http.StatusOK, nil, nil)
   262  		}
   263  		return wrapError(err)
   264  	}
   265  
   266  	objs := make([]jsonapi.Object, len(instances))
   267  	for i, in := range instances {
   268  		in.CLISecret = nil
   269  		in.OAuthSecret = nil
   270  		in.SessSecret = nil
   271  		in.PassphraseHash = nil
   272  		objs[i] = &apiInstance{in}
   273  	}
   274  
   275  	return jsonapi.DataList(c, http.StatusOK, objs, links)
   276  }
   277  
   278  func countHandler(c echo.Context) error {
   279  	count, err := couchdb.CountNormalDocs(prefixer.GlobalPrefixer, consts.Instances)
   280  	if couchdb.IsNoDatabaseError(err) {
   281  		count = 0
   282  	} else if err != nil {
   283  		return wrapError(err)
   284  	}
   285  	return c.JSON(http.StatusOK, echo.Map{"count": count})
   286  }
   287  
   288  func deleteHandler(c echo.Context) error {
   289  	domain := c.Param("domain")
   290  	err := lifecycle.Destroy(domain)
   291  	if err != nil {
   292  		return wrapError(err)
   293  	}
   294  	return c.NoContent(http.StatusNoContent)
   295  }
   296  
   297  func setAuthMode(c echo.Context) error {
   298  	domain := c.Param("domain")
   299  	inst, err := lifecycle.GetInstance(domain)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	m := echo.Map{}
   304  	if err := c.Bind(&m); err != nil {
   305  		return err
   306  	}
   307  
   308  	authModeString, ok := m["auth_mode"]
   309  	if !ok {
   310  		return jsonapi.BadRequest(errors.New("Missing auth_mode key"))
   311  	}
   312  
   313  	authMode, err := instance.StringToAuthMode(authModeString.(string))
   314  	if err != nil {
   315  		return jsonapi.BadRequest(err)
   316  	}
   317  
   318  	if !inst.HasAuthMode(authMode) {
   319  		inst.AuthMode = authMode
   320  		if err = instance.Update(inst); err != nil {
   321  			return err
   322  		}
   323  	} else {
   324  		alreadyAuthMode := fmt.Sprintf("Instance has already %s auth mode", authModeString)
   325  		return c.JSON(http.StatusOK, alreadyAuthMode)
   326  	}
   327  	// Return success
   328  	return c.JSON(http.StatusNoContent, nil)
   329  }
   330  
   331  func createMagicLink(c echo.Context) error {
   332  	domain := c.Param("domain")
   333  	inst, err := lifecycle.GetInstance(domain)
   334  	if err != nil {
   335  		return err
   336  	}
   337  
   338  	code, err := lifecycle.CreateMagicLinkCode(inst)
   339  	if err != nil {
   340  		if err == lifecycle.ErrMagicLinkNotAvailable {
   341  			return c.JSON(http.StatusBadRequest, echo.Map{
   342  				"error": err,
   343  			})
   344  		}
   345  		return c.JSON(http.StatusInternalServerError, echo.Map{
   346  			"error": err,
   347  		})
   348  	}
   349  
   350  	req := c.Request()
   351  	var ip string
   352  	if forwardedFor := req.Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" {
   353  		ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0])
   354  	}
   355  	if ip == "" {
   356  		ip = strings.Split(req.RemoteAddr, ":")[0]
   357  	}
   358  	inst.Logger().WithField("nspace", "loginaudit").
   359  		Infof("New magic_link code created from %s at %s", ip, time.Now())
   360  
   361  	return c.JSON(http.StatusCreated, echo.Map{
   362  		"code": code,
   363  	})
   364  }
   365  
   366  func createSessionCode(c echo.Context) error {
   367  	domain := c.Param("domain")
   368  	inst, err := lifecycle.GetInstance(domain)
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	code, err := inst.CreateSessionCode()
   374  	if err != nil {
   375  		return c.JSON(http.StatusInternalServerError, echo.Map{
   376  			"error": err,
   377  		})
   378  	}
   379  
   380  	req := c.Request()
   381  	var ip string
   382  	if forwardedFor := req.Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" {
   383  		ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0])
   384  	}
   385  	if ip == "" {
   386  		ip = strings.Split(req.RemoteAddr, ":")[0]
   387  	}
   388  	inst.Logger().WithField("nspace", "loginaudit").
   389  		Infof("New session_code created from %s at %s", ip, time.Now())
   390  
   391  	return c.JSON(http.StatusCreated, echo.Map{
   392  		"session_code": code,
   393  	})
   394  }
   395  
   396  type checkSessionCodeArgs struct {
   397  	Code string `json:"session_code"`
   398  }
   399  
   400  func checkSessionCode(c echo.Context) error {
   401  	domain := c.Param("domain")
   402  	inst, err := lifecycle.GetInstance(domain)
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	var args checkSessionCodeArgs
   408  	if err := c.Bind(&args); err != nil {
   409  		return err
   410  	}
   411  
   412  	ok := inst.CheckAndClearSessionCode(args.Code)
   413  	if !ok {
   414  		return c.JSON(http.StatusForbidden, echo.Map{"valid": false})
   415  	}
   416  
   417  	return c.JSON(http.StatusOK, echo.Map{"valid": true})
   418  }
   419  
   420  func createEmailVerifiedCode(c echo.Context) error {
   421  	domain := c.Param("domain")
   422  	inst, err := lifecycle.GetInstance(domain)
   423  	if err != nil {
   424  		return err
   425  	}
   426  
   427  	if !inst.HasAuthMode(instance.TwoFactorMail) {
   428  		return jsonapi.BadRequest(errors.New("2FA by email is not enabled on this instance"))
   429  	}
   430  
   431  	code, err := inst.CreateEmailVerifiedCode()
   432  	if err != nil {
   433  		return c.JSON(http.StatusInternalServerError, echo.Map{
   434  			"error": err,
   435  		})
   436  	}
   437  
   438  	req := c.Request()
   439  	var ip string
   440  	if forwardedFor := req.Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" {
   441  		ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0])
   442  	}
   443  	if ip == "" {
   444  		ip = strings.Split(req.RemoteAddr, ":")[0]
   445  	}
   446  	inst.Logger().WithField("nspace", "loginaudit").
   447  		Infof("New email_verified_code created from %s at %s", ip, time.Now())
   448  
   449  	return c.JSON(http.StatusCreated, echo.Map{
   450  		"email_verified_code": code,
   451  	})
   452  }
   453  
   454  func cleanSessions(c echo.Context) error {
   455  	domain := c.Param("domain")
   456  	inst, err := lifecycle.GetInstance(domain)
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	if err := couchdb.DeleteDB(inst, consts.Sessions); err != nil && !couchdb.IsNoDatabaseError(err) {
   462  		return err
   463  	}
   464  	if err := couchdb.DeleteDB(inst, consts.SessionsLogins); err != nil && !couchdb.IsNoDatabaseError(err) {
   465  		return err
   466  	}
   467  	return c.NoContent(http.StatusNoContent)
   468  }
   469  
   470  func lastActivity(c echo.Context) error {
   471  	inst, err := instance.Get(c.Param("domain"))
   472  	if err != nil {
   473  		return jsonapi.NotFound(err)
   474  	}
   475  	last := time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)
   476  	if inst.LastActivityFromDeletedOAuthClients != nil {
   477  		last = *inst.LastActivityFromDeletedOAuthClients
   478  	}
   479  
   480  	err = couchdb.ForeachDocs(inst, consts.SessionsLogins, func(_ string, data json.RawMessage) error {
   481  		var entry session.LoginEntry
   482  		if err := json.Unmarshal(data, &entry); err != nil {
   483  			return err
   484  		}
   485  		if last.Before(entry.CreatedAt) {
   486  			last = entry.CreatedAt
   487  		}
   488  		return nil
   489  	})
   490  	if err != nil {
   491  		return err
   492  	}
   493  
   494  	err = couchdb.ForeachDocs(inst, consts.Sessions, func(_ string, data json.RawMessage) error {
   495  		var sess session.Session
   496  		if err := json.Unmarshal(data, &sess); err != nil {
   497  			return err
   498  		}
   499  		if last.Before(sess.LastSeen) {
   500  			last = sess.LastSeen
   501  		}
   502  		return nil
   503  	})
   504  	// If the instance has not yet been onboarded, the io.cozy.sessions
   505  	// database will not exist.
   506  	if err != nil && !couchdb.IsNoDatabaseError(err) {
   507  		return err
   508  	}
   509  
   510  	err = couchdb.ForeachDocs(inst, consts.OAuthClients, func(_ string, data json.RawMessage) error {
   511  		var client oauth.Client
   512  		if err := json.Unmarshal(data, &client); err != nil {
   513  			return err
   514  		}
   515  		// Ignore the OAuth clients used for sharings
   516  		if client.ClientKind == "sharing" {
   517  			return nil
   518  		}
   519  		if at, ok := client.LastRefreshedAt.(string); ok {
   520  			if t, err := time.Parse(time.RFC3339Nano, at); err == nil {
   521  				if last.Before(t) {
   522  					last = t
   523  				}
   524  			}
   525  		}
   526  		if at, ok := client.SynchronizedAt.(string); ok {
   527  			if t, err := time.Parse(time.RFC3339Nano, at); err == nil {
   528  				if last.Before(t) {
   529  					last = t
   530  				}
   531  			}
   532  		}
   533  		return nil
   534  	})
   535  	if err != nil {
   536  		return err
   537  	}
   538  
   539  	return c.JSON(http.StatusOK, echo.Map{
   540  		"last-activity": last.Format("2006-01-02"),
   541  	})
   542  }
   543  
   544  func unxorID(c echo.Context) error {
   545  	inst, err := instance.Get(c.Param("domain"))
   546  	if err != nil {
   547  		return jsonapi.NotFound(err)
   548  	}
   549  	s, err := sharing.FindSharing(inst, c.Param("sharing-id"))
   550  	if err != nil {
   551  		return jsonapi.NotFound(err)
   552  	}
   553  	if s.Owner {
   554  		err := errors.New("it only works on a recipient's instance")
   555  		return jsonapi.BadRequest(err)
   556  	}
   557  	if len(s.Credentials) != 1 {
   558  		err := errors.New("unexpected credentials")
   559  		return jsonapi.BadRequest(err)
   560  	}
   561  	key := s.Credentials[0].XorKey
   562  	id := sharing.XorID(c.Param("doc-id"), key)
   563  	return c.JSON(http.StatusOK, echo.Map{"id": id})
   564  }
   565  
   566  type diskUsageResult struct {
   567  	Used          int64 `json:"used,string"`
   568  	Quota         int64 `json:"quota,string,omitempty"`
   569  	Count         int   `json:"doc_count,omitempty"`
   570  	Files         int64 `json:"files,string,omitempty"`
   571  	Versions      int64 `json:"versions,string,omitempty"`
   572  	VersionsCount int   `json:"versions_count,string,omitempty"`
   573  	Trashed       int64 `json:"trashed,string,omitempty"`
   574  }
   575  
   576  func diskUsage(c echo.Context) error {
   577  	domain := c.Param("domain")
   578  	instance, err := lifecycle.GetInstance(domain)
   579  	if err != nil {
   580  		return err
   581  	}
   582  	fs := instance.VFS()
   583  
   584  	files, err := fs.FilesUsage()
   585  	if err != nil {
   586  		return err
   587  	}
   588  
   589  	versions, err := fs.VersionsUsage()
   590  	if err != nil {
   591  		return err
   592  	}
   593  
   594  	result := &diskUsageResult{}
   595  	result.Used = files + versions
   596  	result.Files = files
   597  	result.Versions = versions
   598  
   599  	if c.QueryParam("include") == "trash" {
   600  		trashed, err := fs.TrashUsage()
   601  		if err != nil {
   602  			return err
   603  		}
   604  		result.Trashed = trashed
   605  	}
   606  
   607  	result.Quota = fs.DiskQuota()
   608  	if stats, err := couchdb.DBStatus(instance, consts.Files); err == nil {
   609  		result.Count = stats.DocCount
   610  	}
   611  	if stats, err := couchdb.DBStatus(instance, consts.FilesVersions); err == nil {
   612  		result.VersionsCount = stats.DocCount
   613  	}
   614  	return c.JSON(http.StatusOK, result)
   615  }
   616  
   617  func sendNotification(c echo.Context) error {
   618  	domain := c.Param("domain")
   619  	instance, err := lifecycle.GetInstance(domain)
   620  	if err != nil {
   621  		return err
   622  	}
   623  
   624  	m := map[string]json.RawMessage{}
   625  	if err := json.NewDecoder(c.Request().Body).Decode(&m); err != nil {
   626  		return err
   627  	}
   628  
   629  	p := &notification.Properties{}
   630  	if err := json.Unmarshal(m["properties"], &p); err != nil {
   631  		return err
   632  	}
   633  
   634  	n := &notification.Notification{}
   635  	if err := json.Unmarshal(m["notification"], &n); err != nil {
   636  		return err
   637  	}
   638  
   639  	if err := center.PushCLI(instance.DomainName(), p, n); err != nil {
   640  		return err
   641  	}
   642  	return c.JSON(http.StatusCreated, n)
   643  }
   644  
   645  func showPrefix(c echo.Context) error {
   646  	domain := c.Param("domain")
   647  
   648  	instance, err := lifecycle.GetInstance(domain)
   649  	if err != nil {
   650  		return err
   651  	}
   652  
   653  	return c.JSON(http.StatusOK, instance.DBPrefix())
   654  }
   655  
   656  func getSwiftBucketName(c echo.Context) error {
   657  	domain := c.Param("domain")
   658  
   659  	instance, err := lifecycle.GetInstance(domain)
   660  	if err != nil {
   661  		return err
   662  	}
   663  
   664  	var containerNames map[string]string
   665  	type swifter interface {
   666  		ContainerNames() map[string]string
   667  	}
   668  	if obj, ok := instance.VFS().(swifter); ok {
   669  		containerNames = obj.ContainerNames()
   670  	}
   671  
   672  	return c.JSON(http.StatusOK, containerNames)
   673  }
   674  
   675  func appVersion(c echo.Context) error {
   676  	instances, err := instance.List()
   677  	if err != nil {
   678  		return nil
   679  	}
   680  	appSlug := c.Param("slug")
   681  	version := c.Param("version")
   682  
   683  	var instancesAppVersion []string
   684  
   685  	for _, instance := range instances {
   686  		app, err := app.GetBySlug(instance, appSlug, consts.WebappType)
   687  		if err == nil {
   688  			if app.Version() == version {
   689  				instancesAppVersion = append(instancesAppVersion, instance.Domain)
   690  			}
   691  		}
   692  	}
   693  
   694  	i := struct {
   695  		Instances []string `json:"instances"`
   696  	}{
   697  		instancesAppVersion,
   698  	}
   699  
   700  	return c.JSON(http.StatusOK, i)
   701  }
   702  
   703  func wrapError(err error) error {
   704  	switch err {
   705  	case instance.ErrNotFound:
   706  		return jsonapi.NotFound(err)
   707  	case instance.ErrExists:
   708  		return jsonapi.Conflict(err)
   709  	case instance.ErrIllegalDomain:
   710  		return jsonapi.InvalidParameter("domain", err)
   711  	case instance.ErrMissingToken:
   712  		return jsonapi.BadRequest(err)
   713  	case instance.ErrInvalidToken:
   714  		return jsonapi.BadRequest(err)
   715  	case instance.ErrMissingPassphrase:
   716  		return jsonapi.BadRequest(err)
   717  	case instance.ErrInvalidPassphrase:
   718  		return jsonapi.BadRequest(err)
   719  	case instance.ErrBadTOSVersion:
   720  		return jsonapi.BadRequest(err)
   721  	}
   722  	return err
   723  }
   724  
   725  // Routes sets the routing for the instances service
   726  func Routes(router *echo.Group) {
   727  	// CRUD for instances
   728  	router.GET("", listHandler)
   729  	router.POST("", createHandler)
   730  	router.GET("/count", countHandler)
   731  	router.GET("/:domain", showHandler)
   732  	router.PATCH("/:domain", modifyHandler)
   733  	router.DELETE("/:domain", deleteHandler)
   734  
   735  	// Debug mode
   736  	router.GET("/:domain/debug", getDebug)
   737  	router.POST("/:domain/debug", enableDebug)
   738  	router.DELETE("/:domain/debug", disableDebug)
   739  
   740  	// Feature flags
   741  	router.GET("/:domain/feature/flags", getFeatureFlags)
   742  	router.PATCH("/:domain/feature/flags", patchFeatureFlags)
   743  	router.GET("/:domain/feature/sets", getFeatureSets)
   744  	router.PUT("/:domain/feature/sets", putFeatureSets)
   745  	router.GET("/feature/config/:context", getFeatureConfig)
   746  	router.GET("/feature/contexts/:context", getFeatureContext)
   747  	router.PATCH("/feature/contexts/:context", patchFeatureContext)
   748  	router.GET("/feature/defaults", getFeatureDefaults)
   749  	router.PATCH("/feature/defaults", patchFeatureDefaults)
   750  
   751  	// Authentication
   752  	router.POST("/token", createToken)
   753  	router.GET("/oauth_client", findClientBySoftwareID)
   754  	router.POST("/oauth_client", registerClient)
   755  	router.POST("/:domain/auth-mode", setAuthMode)
   756  	router.POST("/:domain/magic_link", createMagicLink)
   757  	router.POST("/:domain/session_code", createSessionCode)
   758  	router.POST("/:domain/session_code/check", checkSessionCode)
   759  	router.POST("/:domain/email_verified_code", createEmailVerifiedCode)
   760  	router.DELETE("/:domain/sessions", cleanSessions)
   761  
   762  	// Advanced features for instances
   763  	router.GET("/:domain/last-activity", lastActivity)
   764  	router.POST("/:domain/export", exporter)
   765  	router.GET("/:domain/exports/:export-id/data", dataExporter)
   766  	router.POST("/:domain/import", importer)
   767  	router.GET("/:domain/disk-usage", diskUsage)
   768  	router.GET("/:domain/prefix", showPrefix)
   769  	router.GET("/:domain/swift-prefix", getSwiftBucketName)
   770  	router.GET("/:domain/sharings/:sharing-id/unxor/:doc-id", unxorID)
   771  	router.POST("/:domain/notifications", sendNotification)
   772  
   773  	// Config
   774  	router.POST("/redis", rebuildRedis)
   775  	router.GET("/assets", assetsInfos)
   776  	router.POST("/assets", addAssets)
   777  	router.DELETE("/assets/:context/*", deleteAssets)
   778  	router.GET("/contexts", lsContexts)
   779  	router.GET("/contexts/:name", showContext)
   780  	router.GET("/with-app-version/:slug/:version", appVersion)
   781  
   782  	// Checks
   783  	router.GET("/:domain/fsck", fsckHandler)
   784  	router.POST("/:domain/checks/triggers", checkTriggers)
   785  	router.POST("/:domain/checks/shared", checkShared)
   786  	router.POST("/:domain/checks/sharings", checkSharings)
   787  
   788  	// Fixers
   789  	router.POST("/:domain/fixers/password-defined", passwordDefinedFixer)
   790  	router.POST("/:domain/fixers/orphan-account", orphanAccountFixer)
   791  	router.POST("/:domain/fixers/service-triggers", serviceTriggersFixer)
   792  	router.POST("/:domain/fixers/indexes", indexesFixer)
   793  }