package core import ( "database/sql" "errors" "fmt" "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/router" ) const CollectionNameSuperusers = "_superusers" // DefaultInstallerEmail is the default superuser email address // for the initial autogenerated superuser account. const DefaultInstallerEmail = "__pbinstaller@example.com" func (app *BaseApp) registerSuperuserHooks() { app.OnRecordDelete(CollectionNameSuperusers).Bind(&hook.Handler[*RecordEvent]{ Id: "pbSuperusersRecordDelete", 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: "pbSuperusersRecordSaveExec", 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) // prevent sending password reset emails to the installer address app.OnMailerRecordPasswordResetSend(CollectionNameSuperusers).Bind(&hook.Handler[*MailerRecordEvent]{ Id: "pbSuperusersInstallerPasswordReset", Func: func(e *MailerRecordEvent) error { if e.Record.Email() == DefaultInstallerEmail { return errors.New("cannot reset the password for the installer superuser") } return e.Next() }, }) collectionSaveHandler := &hook.Handler[*CollectionEvent]{ Id: "pbSuperusersCollectionSaveExec", Func: func(e *CollectionEvent) error { // don't allow name change even if executed with SaveNoValidate e.Collection.Name = CollectionNameSuperusers // for now don't allow superusers OAuth2 since we don't want // to accidentally create a new superuser by just OAuth2 signin e.Collection.OAuth2.Enabled = false e.Collection.OAuth2.Providers = nil // force password auth e.Collection.PasswordAuth.Enabled = true // for superusers we don't allow for now standalone OTP auth and always require to be combined with MFA if e.Collection.OTP.Enabled { e.Collection.MFA.Enabled = true } 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 }