| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | package core | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2024-11-06 20:22:57 +08:00
										 |  |  | 	"database/sql" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	"fmt" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/hook" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/router" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const CollectionNameSuperusers = "_superusers" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-06 20:22:57 +08:00
										 |  |  | // DefaultInstallerEmail is the default superuser email address
 | 
					
						
							|  |  |  | // for the initial autogenerated superuser account.
 | 
					
						
							|  |  |  | const DefaultInstallerEmail = "__pbinstaller@example.com" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 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
 | 
					
						
							| 
									
										
										
										
											2024-11-06 20:22:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			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 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		Priority: -99, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	app.OnRecordCreateExecute(CollectionNameSuperusers).Bind(recordSaveHandler) | 
					
						
							|  |  |  | 	app.OnRecordUpdateExecute(CollectionNameSuperusers).Bind(recordSaveHandler) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-06 20:22:57 +08:00
										 |  |  | 	// 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() | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	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 | 
					
						
							|  |  |  | } |