fix(server): adopt the migrated org on bootstrap; survive fresh install
Two failures found by tracing the migration path against a real upgrade. Bootstrap orphaned the entire dataset. On upgrade, 0001 creates the Default org and stamps every legacy document with it, but the instance has no users, so the operator must run /auth/bootstrap to get in — and that unconditionally created a SECOND org and put the owner in it. Every org-scoped read then filtered on the new org, so the operator would log into an empty Vantage while all their data sat under "default". Nothing errored and agents kept syncing, so it presented as total data loss. Bootstrap now adopts the sole existing org, renaming and re-slugging it, and only creates one when no org exists. More than one org with no users is refused rather than guessed. Fresh installs crash-looped. Nothing creates the settings collection before EnsureSettingsIndexes, so DropOne returned NamespaceNotFound (26), isIndexNotFound matched only IndexNotFound (27), and that check is fatal. The same early return also skipped index creation in the secrets and workflow ensures. Also: only insert the backfill org on ErrNoDocuments, so a transient read error can't race the fatal unique slug index; run 0002 before 0003 so the settings migration can't be pushed into its ambiguous branch; fail 0002's ambiguous case with a remedy instead of continuing into a fatal index build; and skip non-string ids in the owner backfill rather than aborting.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
"github.com/mrhid6/vantage/server/internal/services"
|
||||
)
|
||||
|
||||
@@ -89,7 +91,36 @@ func HandleBootstrap(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "org_name, email, and password (>=8 chars) required"})
|
||||
return
|
||||
}
|
||||
org, err := services.CreateOrg(body.OrgName)
|
||||
// An upgrade from single-tenant arrives here with no users but with the org
|
||||
// the migrations created and stamped onto every legacy document. Creating a
|
||||
// second org would put the owner somewhere else entirely, and since every
|
||||
// org-scoped read filters on org_id, the operator would land in an empty
|
||||
// Vantage with all their real data still under the migrated org — silent,
|
||||
// total-looking data loss. So adopt the existing org instead, and only
|
||||
// create when there genuinely is none. Same `switch orgCount` shape as
|
||||
// migration 0002.
|
||||
orgCount, err := services.CountOrgs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var org *models.Org
|
||||
switch orgCount {
|
||||
case 0:
|
||||
org, err = services.CreateOrg(body.OrgName)
|
||||
case 1:
|
||||
var existing *models.Org
|
||||
existing, err = services.FirstOrg()
|
||||
if err == nil {
|
||||
org, err = services.AdoptOrg(existing.OrgID, body.OrgName)
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf(
|
||||
"cannot bootstrap: %d organizations already exist but no users do; "+
|
||||
"create the owner against the intended org rather than through setup, "+
|
||||
"or remove the unintended orgs and retry", orgCount)})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user