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 := ¬ification.Properties{} 630 if err := json.Unmarshal(m["properties"], &p); err != nil { 631 return err 632 } 633 634 n := ¬ification.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 }