package apis import ( "log" "net/http" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/forms" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tokens" "github.com/pocketbase/pocketbase/tools/auth" "github.com/pocketbase/pocketbase/tools/rest" "github.com/pocketbase/pocketbase/tools/routine" "github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/security" "golang.org/x/oauth2" ) // BindUserApi registers the user api endpoints and the corresponding handlers. func BindUserApi(app core.App, rg *echo.Group) { api := userApi{app: app} subGroup := rg.Group("/users", ActivityLogger(app)) subGroup.GET("/auth-methods", api.authMethods) subGroup.POST("/auth-via-oauth2", api.oauth2Auth, RequireGuestOnly()) subGroup.POST("/auth-via-email", api.emailAuth, RequireGuestOnly()) subGroup.POST("/request-password-reset", api.requestPasswordReset) subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) subGroup.POST("/request-verification", api.requestVerification) subGroup.POST("/confirm-verification", api.confirmVerification) subGroup.POST("/request-email-change", api.requestEmailChange, RequireUserAuth()) subGroup.POST("/confirm-email-change", api.confirmEmailChange) subGroup.POST("/refresh", api.refresh, RequireUserAuth()) // crud subGroup.GET("", api.list, RequireAdminAuth()) subGroup.POST("", api.create) subGroup.GET("/:id", api.view, RequireAdminOrOwnerAuth("id")) subGroup.PATCH("/:id", api.update, RequireAdminAuth()) subGroup.DELETE("/:id", api.delete, RequireAdminOrOwnerAuth("id")) } type userApi struct { app core.App } func (api *userApi) authResponse(c echo.Context, user *models.User, meta any) error { token, tokenErr := tokens.NewUserAuthToken(api.app, user) if tokenErr != nil { return rest.NewBadRequestError("Failed to create auth token.", tokenErr) } event := &core.UserAuthEvent{ HttpContext: c, User: user, Token: token, Meta: meta, } return api.app.OnUserAuthRequest().Trigger(event, func(e *core.UserAuthEvent) error { result := map[string]any{ "token": e.Token, "user": e.User, } if e.Meta != nil { result["meta"] = e.Meta } return e.HttpContext.JSON(http.StatusOK, result) }) } func (api *userApi) refresh(c echo.Context) error { user, _ := c.Get(ContextUserKey).(*models.User) if user == nil { return rest.NewNotFoundError("Missing auth user context.", nil) } return api.authResponse(c, user, nil) } type providerInfo struct { Name string `json:"name"` State string `json:"state"` CodeVerifier string `json:"codeVerifier"` CodeChallenge string `json:"codeChallenge"` CodeChallengeMethod string `json:"codeChallengeMethod"` AuthUrl string `json:"authUrl"` } func (api *userApi) authMethods(c echo.Context) error { result := struct { EmailPassword bool `json:"emailPassword"` AuthProviders []providerInfo `json:"authProviders"` }{ EmailPassword: true, AuthProviders: []providerInfo{}, } settings := api.app.Settings() result.EmailPassword = settings.EmailAuth.Enabled nameConfigMap := settings.NamedAuthProviderConfigs() for name, config := range nameConfigMap { if !config.Enabled { continue } provider, err := auth.NewProviderByName(name) if err != nil { if api.app.IsDebug() { log.Println(err) } // skip provider continue } if err := config.SetupProvider(provider); err != nil { if api.app.IsDebug() { log.Println(err) } // skip provider continue } state := security.RandomString(30) codeVerifier := security.RandomString(30) codeChallenge := security.S256Challenge(codeVerifier) codeChallengeMethod := "S256" result.AuthProviders = append(result.AuthProviders, providerInfo{ Name: name, State: state, CodeVerifier: codeVerifier, CodeChallenge: codeChallenge, CodeChallengeMethod: codeChallengeMethod, AuthUrl: provider.BuildAuthUrl( state, oauth2.SetAuthURLParam("code_challenge", codeChallenge), oauth2.SetAuthURLParam("code_challenge_method", codeChallengeMethod), ) + "&redirect_uri=", // empty redirect_uri so that users can append their url }) } return c.JSON(http.StatusOK, result) } func (api *userApi) oauth2Auth(c echo.Context) error { form := forms.NewUserOauth2Login(api.app) if readErr := c.Bind(form); readErr != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) } user, authData, submitErr := form.Submit() if submitErr != nil { return rest.NewBadRequestError("Failed to authenticate.", submitErr) } return api.authResponse(c, user, authData) } func (api *userApi) emailAuth(c echo.Context) error { if !api.app.Settings().EmailAuth.Enabled { return rest.NewBadRequestError("Email/Password authentication is not enabled.", nil) } form := forms.NewUserEmailLogin(api.app) if readErr := c.Bind(form); readErr != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) } user, submitErr := form.Submit() if submitErr != nil { return rest.NewBadRequestError("Failed to authenticate.", submitErr) } return api.authResponse(c, user, nil) } func (api *userApi) requestPasswordReset(c echo.Context) error { form := forms.NewUserPasswordResetRequest(api.app) if err := c.Bind(form); err != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) } if err := form.Validate(); err != nil { return rest.NewBadRequestError("An error occurred while validating the form.", err) } // run in background because we don't need to show // the result to the user (prevents users enumeration) routine.FireAndForget(func() { if err := form.Submit(); err != nil && api.app.IsDebug() { log.Println(err) } }) return c.NoContent(http.StatusNoContent) } func (api *userApi) confirmPasswordReset(c echo.Context) error { form := forms.NewUserPasswordResetConfirm(api.app) if readErr := c.Bind(form); readErr != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) } user, submitErr := form.Submit() if submitErr != nil { return rest.NewBadRequestError("Failed to set new password.", submitErr) } return api.authResponse(c, user, nil) } func (api *userApi) requestEmailChange(c echo.Context) error { loggedUser, _ := c.Get(ContextUserKey).(*models.User) if loggedUser == nil { return rest.NewUnauthorizedError("The request requires valid authorized user.", nil) } form := forms.NewUserEmailChangeRequest(api.app, loggedUser) if err := c.Bind(form); err != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) } if err := form.Submit(); err != nil { return rest.NewBadRequestError("Failed to request email change.", err) } return c.NoContent(http.StatusNoContent) } func (api *userApi) confirmEmailChange(c echo.Context) error { form := forms.NewUserEmailChangeConfirm(api.app) if readErr := c.Bind(form); readErr != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) } user, submitErr := form.Submit() if submitErr != nil { return rest.NewBadRequestError("Failed to confirm email change.", submitErr) } return api.authResponse(c, user, nil) } func (api *userApi) requestVerification(c echo.Context) error { form := forms.NewUserVerificationRequest(api.app) if err := c.Bind(form); err != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) } if err := form.Validate(); err != nil { return rest.NewBadRequestError("An error occurred while validating the form.", err) } // run in background because we don't need to show // the result to the user (prevents users enumeration) routine.FireAndForget(func() { if err := form.Submit(); err != nil && api.app.IsDebug() { log.Println(err) } }) return c.NoContent(http.StatusNoContent) } func (api *userApi) confirmVerification(c echo.Context) error { form := forms.NewUserVerificationConfirm(api.app) if readErr := c.Bind(form); readErr != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", readErr) } user, submitErr := form.Submit() if submitErr != nil { return rest.NewBadRequestError("An error occurred while submitting the form.", submitErr) } return api.authResponse(c, user, nil) } // ------------------------------------------------------------------- // CRUD // ------------------------------------------------------------------- func (api *userApi) list(c echo.Context) error { fieldResolver := search.NewSimpleFieldResolver( "id", "created", "updated", "email", "verified", ) users := []*models.User{} result, searchErr := search.NewProvider(fieldResolver). Query(api.app.Dao().UserQuery()). ParseAndExec(c.QueryString(), &users) if searchErr != nil { return rest.NewBadRequestError("", searchErr) } // eager load user profiles (if any) if err := api.app.Dao().LoadProfiles(users); err != nil { return rest.NewBadRequestError("", err) } event := &core.UsersListEvent{ HttpContext: c, Users: users, Result: result, } return api.app.OnUsersListRequest().Trigger(event, func(e *core.UsersListEvent) error { return e.HttpContext.JSON(http.StatusOK, e.Result) }) } func (api *userApi) view(c echo.Context) error { id := c.PathParam("id") if id == "" { return rest.NewNotFoundError("", nil) } user, err := api.app.Dao().FindUserById(id) if err != nil || user == nil { return rest.NewNotFoundError("", err) } event := &core.UserViewEvent{ HttpContext: c, User: user, } return api.app.OnUserViewRequest().Trigger(event, func(e *core.UserViewEvent) error { return e.HttpContext.JSON(http.StatusOK, e.User) }) } func (api *userApi) create(c echo.Context) error { if !api.app.Settings().EmailAuth.Enabled { return rest.NewBadRequestError("Email/Password authentication is not enabled.", nil) } user := &models.User{} form := forms.NewUserUpsert(api.app, user) // load request if err := c.Bind(form); err != nil { return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.UserCreateEvent{ HttpContext: c, User: user, } // create the user submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { return func() error { return api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error { if err := next(); err != nil { return rest.NewBadRequestError("Failed to create user.", err) } return e.HttpContext.JSON(http.StatusOK, e.User) }) } }) if submitErr == nil { api.app.OnUserAfterCreateRequest().Trigger(event) } return submitErr } func (api *userApi) update(c echo.Context) error { id := c.PathParam("id") if id == "" { return rest.NewNotFoundError("", nil) } user, err := api.app.Dao().FindUserById(id) if err != nil || user == nil { return rest.NewNotFoundError("", err) } form := forms.NewUserUpsert(api.app, user) // load request if err := c.Bind(form); err != nil { return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.UserUpdateEvent{ HttpContext: c, User: user, } // update the user submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { return func() error { return api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error { if err := next(); err != nil { return rest.NewBadRequestError("Failed to update user.", err) } return e.HttpContext.JSON(http.StatusOK, e.User) }) } }) if submitErr == nil { api.app.OnUserAfterUpdateRequest().Trigger(event) } return submitErr } func (api *userApi) delete(c echo.Context) error { id := c.PathParam("id") if id == "" { return rest.NewNotFoundError("", nil) } user, err := api.app.Dao().FindUserById(id) if err != nil || user == nil { return rest.NewNotFoundError("", err) } event := &core.UserDeleteEvent{ HttpContext: c, User: user, } handlerErr := api.app.OnUserBeforeDeleteRequest().Trigger(event, func(e *core.UserDeleteEvent) error { // delete the user model if err := api.app.Dao().DeleteUser(e.User); err != nil { return rest.NewBadRequestError("Failed to delete user. Make sure that the user is not part of a required relation reference.", err) } return e.HttpContext.NoContent(http.StatusNoContent) }) if handlerErr == nil { api.app.OnUserAfterDeleteRequest().Trigger(event) } return handlerErr }