Files
base/core/record_model_superusers.go
zeekay e4b1f3c75c a Base is addressed at /v1/bases, and Base stops running a second auth
Two things wore the name "platform" here, and neither is the PaaS at
platform.hanzo.ai. That collision is why "where are my Bases?" had no answer a
person could act on: one prefix, two products, and the one you reached was
whichever you happened to mean.

A Base is what an org gets — its own collections, its own files, its own key —
so the resource a client addresses is /v1/bases. Not /v1/orgs: the org is IAM's
noun and Base does not get a second one. Everything scoped to an org now hangs
off the Base it belongs to (/v1/bases/{id}/config, /creds, /customers), and
compliance moves to /v1/compliance, a sibling of /v1/iam rather than a child of
Base's own resource. The package is `org`, which seven of its files already
were. Nothing addressed the old prefix — no client, no UI, no manifest — so this
renames a surface rather than breaking one.

The auth proxy is DELETED, not renamed. Its five routes reshaped bodies and
forwarded to /v1/iam/login, /signup, /userinfo and /send-verification-code —
which the opaque /v1/iam/{path...} passthrough beside it already reaches. So
Base carried a hand-written login, signup and one-time-code flow whose only job
was to be a second way to reach IAM. Auth is IAM's, and one way is the whole
point of that rule.

The prefix test that already banned /api/ bans this too, so the name cannot come
back: it reads route LITERALS through go/ast, so a comment explaining the ban is
prose and a route is a route. Verified to fail on the exact regression.
2026-08-10 19:18:08 -07:00

101 lines
3.0 KiB
Go

package core
import (
"database/sql"
"errors"
"fmt"
"github.com/hanzoai/base/tools/hook"
"github.com/hanzoai/base/tools/router"
)
const CollectionNameSuperusers = "_superusers"
// DefaultInstallerEmail is the default superuser email address
// for the initial autogenerated superuser account.
const DefaultInstallerEmail = "[email protected]"
func (app *BaseApp) registerSuperuserHooks() {
app.OnRecordDelete(CollectionNameSuperusers).Bind(&hook.Handler[*RecordEvent]{
Id: "baseSuperusersRecordDelete",
Func: func(e *RecordEvent) error {
originalApp := e.App
txErr := e.App.RunInTransaction(func(txApp App) error {
e.App = txApp
total, err := e.App.CountRecords(CollectionNameSuperusers)
if err != nil {
return fmt.Errorf("failed to fetch total superusers count: %w", err)
}
if total == 1 {
return router.NewBadRequestError("You can't delete the only existing superuser", nil)
}
return e.Next()
})
e.App = originalApp
return txErr
},
Priority: -99,
})
recordSaveHandler := &hook.Handler[*RecordEvent]{
Id: "baseSuperusersRecordSaveExec",
Func: func(e *RecordEvent) error {
e.Record.SetVerified(true) // always mark superusers as verified
if err := e.Next(); err != nil {
return err
}
// ensure that the installer superuser is deleted
if e.Type == ModelEventTypeCreate && e.Record.Email() != DefaultInstallerEmail {
record, err := app.FindAuthRecordByEmail(CollectionNameSuperusers, DefaultInstallerEmail)
if errors.Is(err, sql.ErrNoRows) {
// already deleted
} else if err != nil {
e.App.Logger().Warn("Failed to fetch installer superuser", "error", err)
} else {
err = e.App.Delete(record)
if err != nil {
e.App.Logger().Warn("Failed to delete installer superuser", "error", err)
}
}
}
return nil
},
Priority: -99,
}
app.OnRecordCreateExecute(CollectionNameSuperusers).Bind(recordSaveHandler)
app.OnRecordUpdateExecute(CollectionNameSuperusers).Bind(recordSaveHandler)
collectionSaveHandler := &hook.Handler[*CollectionEvent]{
Id: "baseSuperusersCollectionSaveExec",
Func: func(e *CollectionEvent) error {
// don't allow name change even if executed with SaveNoValidate
e.Collection.Name = CollectionNameSuperusers
// the _superusers collection is mirrored from IAM — no local
// password / OAuth2 / MFA / OTP path lives here. The PKCE
// flow against IAM is the only way in (see plugins/org
// proxy + apis.recordAuthMethods).
e.Collection.OAuth2.Enabled = false
e.Collection.OAuth2.Providers = nil
return e.Next()
},
Priority: 99,
}
app.OnCollectionCreateExecute(CollectionNameSuperusers).Bind(collectionSaveHandler)
app.OnCollectionUpdateExecute(CollectionNameSuperusers).Bind(collectionSaveHandler)
}
// IsSuperuser returns whether the current record is a superuser, aka.
// whether the record is from the _superusers collection.
func (m *Record) IsSuperuser() bool {
return m.Collection().Name == CollectionNameSuperusers
}