2022-10-30 10:28:14 +02:00
package apis
import (
2023-04-10 21:27:00 +02:00
"encoding/json"
2022-10-30 10:28:14 +02:00
"errors"
"fmt"
2023-11-26 13:33:17 +02:00
"log/slog"
2022-10-30 10:28:14 +02:00
"net/http"
2023-11-27 20:05:06 +02:00
"sort"
2022-10-30 10:28:14 +02:00
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/resolvers"
"github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/routine"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/security"
2023-04-10 21:27:00 +02:00
"github.com/pocketbase/pocketbase/tools/subscriptions"
2022-10-30 10:28:14 +02:00
"golang.org/x/oauth2"
)
// bindRecordAuthApi registers the auth record api endpoints and
// the corresponding handlers.
func bindRecordAuthApi ( app core . App , rg * echo . Group ) {
api := recordAuthApi { app : app }
2023-04-10 21:27:00 +02:00
// global oauth2 subscription redirect handler
rg . GET ( "/oauth2-redirect" , api . oauth2SubscriptionRedirect )
// common collection record related routes
2022-10-30 10:28:14 +02:00
subGroup := rg . Group (
"/collections/:collection" ,
ActivityLogger ( app ) ,
LoadCollectionContext ( app , models . CollectionTypeAuth ) ,
)
subGroup . GET ( "/auth-methods" , api . authMethods )
subGroup . POST ( "/auth-refresh" , api . authRefresh , RequireSameContextRecordAuth ( ) )
2023-01-07 22:25:56 +02:00
subGroup . POST ( "/auth-with-oauth2" , api . authWithOAuth2 )
subGroup . POST ( "/auth-with-password" , api . authWithPassword )
2022-10-30 10:28:14 +02:00
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 , RequireSameContextRecordAuth ( ) )
subGroup . POST ( "/confirm-email-change" , api . confirmEmailChange )
subGroup . GET ( "/records/:id/external-auths" , api . listExternalAuths , RequireAdminOrOwnerAuth ( "id" ) )
subGroup . DELETE ( "/records/:id/external-auths/:provider" , api . unlinkExternalAuth , RequireAdminOrOwnerAuth ( "id" ) )
}
type recordAuthApi struct {
app core . App
}
func ( api * recordAuthApi ) authRefresh ( c echo . Context ) error {
record , _ := c . Get ( ContextAuthRecordKey ) . ( * models . Record )
if record == nil {
return NewNotFoundError ( "Missing auth record context." , nil )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordAuthRefreshEvent )
event . HttpContext = c
event . Collection = record . Collection ( )
event . Record = record
2023-01-15 17:00:28 +02:00
2023-05-29 20:50:07 +02:00
return api . app . OnRecordBeforeAuthRefreshRequest ( ) . Trigger ( event , func ( e * core . RecordAuthRefreshEvent ) error {
2023-07-20 09:40:03 +02:00
return api . app . OnRecordAfterAuthRefreshRequest ( ) . Trigger ( event , func ( e * core . RecordAuthRefreshEvent ) error {
return RecordAuthResponse ( api . app , e . HttpContext , e . Record , nil )
2023-05-29 20:50:07 +02:00
} )
2023-01-15 17:00:28 +02:00
} )
2022-10-30 10:28:14 +02:00
}
type providerInfo struct {
2023-11-29 20:19:54 +02:00
Name string ` json:"name" `
DisplayName string ` json:"displayName" `
State string ` json:"state" `
AuthUrl string ` json:"authUrl" `
// technically could be omitted if the provider doesn't support PKCE,
// but to avoid breaking existing typed clients we'll return them as empty string
2022-10-30 10:28:14 +02:00
CodeVerifier string ` json:"codeVerifier" `
CodeChallenge string ` json:"codeChallenge" `
CodeChallengeMethod string ` json:"codeChallengeMethod" `
}
func ( api * recordAuthApi ) authMethods ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
authOptions := collection . AuthOptions ( )
result := struct {
2023-11-29 20:19:54 +02:00
AuthProviders [ ] providerInfo ` json:"authProviders" `
2022-10-30 10:28:14 +02:00
UsernamePassword bool ` json:"usernamePassword" `
EmailPassword bool ` json:"emailPassword" `
2023-12-06 13:30:47 +02:00
OnlyVerified bool ` json:"onlyVerified" `
2022-10-30 10:28:14 +02:00
} {
UsernamePassword : authOptions . AllowUsernameAuth ,
EmailPassword : authOptions . AllowEmailAuth ,
2023-12-06 13:30:47 +02:00
OnlyVerified : authOptions . OnlyVerified ,
2022-10-30 10:28:14 +02:00
AuthProviders : [ ] providerInfo { } ,
}
if ! authOptions . AllowOAuth2Auth {
return c . JSON ( http . StatusOK , result )
}
nameConfigMap := api . app . Settings ( ) . NamedAuthProviderConfigs ( )
for name , config := range nameConfigMap {
if ! config . Enabled {
continue
}
provider , err := auth . NewProviderByName ( name )
if err != nil {
2023-11-26 13:33:17 +02:00
api . app . Logger ( ) . Debug ( "Missing or invalid provier name" , slog . String ( "name" , name ) )
2022-10-30 10:28:14 +02:00
continue // skip provider
}
if err := config . SetupProvider ( provider ) ; err != nil {
2023-11-26 13:33:17 +02:00
api . app . Logger ( ) . Debug (
"Failed to setup provider" ,
slog . String ( "name" , name ) ,
slog . String ( "error" , err . Error ( ) ) ,
)
2022-10-30 10:28:14 +02:00
continue // skip provider
}
2023-11-29 20:19:54 +02:00
info := providerInfo {
Name : name ,
DisplayName : provider . DisplayName ( ) ,
State : security . RandomString ( 30 ) ,
2023-03-01 23:29:45 +02:00
}
2023-11-29 20:19:54 +02:00
if info . DisplayName == "" {
info . DisplayName = name
}
urlOpts := [ ] oauth2 . AuthCodeOption { }
2023-05-24 14:34:25 +02:00
// custom providers url options
switch name {
case auth . NameApple :
2023-03-01 23:29:45 +02:00
// see https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
urlOpts = append ( urlOpts , oauth2 . SetAuthURLParam ( "response_mode" , "query" ) )
}
2023-11-29 20:19:54 +02:00
if provider . PKCE ( ) {
info . CodeVerifier = security . RandomString ( 43 )
info . CodeChallenge = security . S256Challenge ( info . CodeVerifier )
info . CodeChallengeMethod = "S256"
urlOpts = append ( urlOpts ,
oauth2 . SetAuthURLParam ( "code_challenge" , info . CodeChallenge ) ,
oauth2 . SetAuthURLParam ( "code_challenge_method" , info . CodeChallengeMethod ) ,
)
}
info . AuthUrl = provider . BuildAuthUrl (
info . State ,
urlOpts ... ,
) + "&redirect_uri=" // empty redirect_uri so that users can append their redirect url
result . AuthProviders = append ( result . AuthProviders , info )
2022-10-30 10:28:14 +02:00
}
2023-11-27 20:05:06 +02:00
// sort providers
sort . SliceStable ( result . AuthProviders , func ( i , j int ) bool {
return result . AuthProviders [ i ] . Name < result . AuthProviders [ j ] . Name
} )
2022-10-30 10:28:14 +02:00
return c . JSON ( http . StatusOK , result )
}
func ( api * recordAuthApi ) authWithOAuth2 ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
if ! collection . AuthOptions ( ) . AllowOAuth2Auth {
return NewBadRequestError ( "The collection is not configured to allow OAuth2 authentication." , nil )
}
var fallbackAuthRecord * models . Record
loggedAuthRecord , _ := c . Get ( ContextAuthRecordKey ) . ( * models . Record )
if loggedAuthRecord != nil && loggedAuthRecord . Collection ( ) . Id == collection . Id {
fallbackAuthRecord = loggedAuthRecord
}
form := forms . NewRecordOAuth2Login ( api . app , collection , fallbackAuthRecord )
if readErr := c . Bind ( form ) ; readErr != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , readErr )
}
2023-03-25 15:18:28 +02:00
event := new ( core . RecordAuthWithOAuth2Event )
event . HttpContext = c
event . Collection = collection
2023-03-26 18:32:23 +02:00
event . ProviderName = form . Provider
2023-03-25 15:18:28 +02:00
event . IsNewRecord = false
2023-01-15 17:00:28 +02:00
form . SetBeforeNewRecordCreateFunc ( func ( createForm * forms . RecordUpsert , authRecord * models . Record , authUser * auth . AuthUser ) error {
2022-10-30 10:28:14 +02:00
return createForm . DrySubmit ( func ( txDao * daos . Dao ) error {
2023-03-25 15:18:28 +02:00
event . IsNewRecord = true
2023-05-22 22:59:36 +02:00
// clone the current request data and assign the form create data as its body data
2023-07-17 22:13:39 +02:00
requestInfo := * RequestInfo ( c )
requestInfo . Data = form . CreateData
2022-10-30 10:28:14 +02:00
createRuleFunc := func ( q * dbx . SelectQuery ) error {
admin , _ := c . Get ( ContextAdminKey ) . ( * models . Admin )
if admin != nil {
return nil // either admin or the rule is empty
}
if collection . CreateRule == nil {
return errors . New ( "Only admins can create new accounts with OAuth2" )
}
if * collection . CreateRule != "" {
2023-07-17 22:13:39 +02:00
resolver := resolvers . NewRecordFieldResolver ( txDao , collection , & requestInfo , true )
2022-10-30 10:28:14 +02:00
expr , err := search . FilterData ( * collection . CreateRule ) . BuildExpr ( resolver )
if err != nil {
return err
}
resolver . UpdateQuery ( q )
q . AndWhere ( expr )
}
return nil
}
if _ , err := txDao . FindRecordById ( collection . Id , createForm . Id , createRuleFunc ) ; err != nil {
2022-11-30 17:23:00 +02:00
return fmt . Errorf ( "Failed create rule constraint: %w" , err )
2022-10-30 10:28:14 +02:00
}
return nil
} )
} )
2023-01-15 17:00:28 +02:00
_ , _ , submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * forms . RecordOAuth2LoginData ] ) forms . InterceptorNextFunc [ * forms . RecordOAuth2LoginData ] {
return func ( data * forms . RecordOAuth2LoginData ) error {
event . Record = data . Record
event . OAuth2User = data . OAuth2User
2023-03-26 18:32:23 +02:00
event . ProviderClient = data . ProviderClient
2023-01-15 17:00:28 +02:00
return api . app . OnRecordBeforeAuthWithOAuth2Request ( ) . Trigger ( event , func ( e * core . RecordAuthWithOAuth2Event ) error {
data . Record = e . Record
data . OAuth2User = e . OAuth2User
if err := next ( data ) ; err != nil {
return NewBadRequestError ( "Failed to authenticate." , err )
}
2023-01-15 17:14:41 +02:00
e . Record = data . Record
e . OAuth2User = data . OAuth2User
2023-05-17 23:19:36 +02:00
meta := struct {
* auth . AuthUser
IsNew bool ` json:"isNew" `
} {
AuthUser : e . OAuth2User ,
IsNew : event . IsNewRecord ,
}
2023-07-20 09:40:03 +02:00
return api . app . OnRecordAfterAuthWithOAuth2Request ( ) . Trigger ( event , func ( e * core . RecordAuthWithOAuth2Event ) error {
return RecordAuthResponse ( api . app , e . HttpContext , e . Record , meta )
2023-05-29 20:50:07 +02:00
} )
2023-01-15 17:00:28 +02:00
} )
}
} )
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) authWithPassword ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
form := forms . NewRecordPasswordLogin ( api . app , collection )
if readErr := c . Bind ( form ) ; readErr != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , readErr )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordAuthWithPasswordEvent )
event . HttpContext = c
event . Collection = collection
event . Password = form . Password
event . Identity = form . Identity
2023-01-15 17:00:28 +02:00
_ , submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
return func ( record * models . Record ) error {
event . Record = record
return api . app . OnRecordBeforeAuthWithPasswordRequest ( ) . Trigger ( event , func ( e * core . RecordAuthWithPasswordEvent ) error {
if err := next ( e . Record ) ; err != nil {
return NewBadRequestError ( "Failed to authenticate." , err )
}
2023-07-20 09:40:03 +02:00
return api . app . OnRecordAfterAuthWithPasswordRequest ( ) . Trigger ( event , func ( e * core . RecordAuthWithPasswordEvent ) error {
return RecordAuthResponse ( api . app , e . HttpContext , e . Record , nil )
2023-05-29 20:50:07 +02:00
} )
2023-01-15 17:00:28 +02:00
} )
}
} )
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) requestPasswordReset ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
authOptions := collection . AuthOptions ( )
if ! authOptions . AllowUsernameAuth && ! authOptions . AllowEmailAuth {
return NewBadRequestError ( "The collection is not configured to allow password authentication." , nil )
}
form := forms . NewRecordPasswordResetRequest ( api . app , collection )
if err := c . Bind ( form ) ; err != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , err )
}
if err := form . Validate ( ) ; err != nil {
return NewBadRequestError ( "An error occurred while validating the form." , err )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordRequestPasswordResetEvent )
event . HttpContext = c
event . Collection = collection
2022-12-03 14:50:02 +02:00
2023-01-15 17:00:28 +02:00
submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
2022-12-03 14:50:02 +02:00
return func ( record * models . Record ) error {
event . Record = record
return api . app . OnRecordBeforeRequestPasswordResetRequest ( ) . Trigger ( event , func ( e * core . RecordRequestPasswordResetEvent ) error {
// run in background because we don't need to show the result to the client
routine . FireAndForget ( func ( ) {
2023-11-26 13:33:17 +02:00
if err := next ( e . Record ) ; err != nil {
api . app . Logger ( ) . Debug (
"Failed to send password reset email" ,
slog . String ( "error" , err . Error ( ) ) ,
)
2022-12-03 14:50:02 +02:00
}
} )
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterRequestPasswordResetRequest ( ) . Trigger ( event , func ( e * core . RecordRequestPasswordResetEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-12-03 14:50:02 +02:00
} )
2022-10-30 10:28:14 +02:00
}
} )
2023-05-29 20:50:07 +02:00
// eagerly write 204 response and skip submit errors
// as a measure against emails enumeration
2022-12-03 14:50:02 +02:00
if ! c . Response ( ) . Committed {
c . NoContent ( http . StatusNoContent )
}
2023-05-29 20:50:07 +02:00
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) confirmPasswordReset ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
form := forms . NewRecordPasswordResetConfirm ( api . app , collection )
if readErr := c . Bind ( form ) ; readErr != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , readErr )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordConfirmPasswordResetEvent )
event . HttpContext = c
event . Collection = collection
2022-10-30 10:28:14 +02:00
2023-01-15 17:00:28 +02:00
_ , submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
2022-12-03 14:50:02 +02:00
return func ( record * models . Record ) error {
event . Record = record
return api . app . OnRecordBeforeConfirmPasswordResetRequest ( ) . Trigger ( event , func ( e * core . RecordConfirmPasswordResetEvent ) error {
if err := next ( e . Record ) ; err != nil {
return NewBadRequestError ( "Failed to set new password." , err )
}
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterConfirmPasswordResetRequest ( ) . Trigger ( event , func ( e * core . RecordConfirmPasswordResetEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-12-03 14:50:02 +02:00
} )
}
} )
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) requestVerification ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
form := forms . NewRecordVerificationRequest ( api . app , collection )
if err := c . Bind ( form ) ; err != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , err )
}
if err := form . Validate ( ) ; err != nil {
return NewBadRequestError ( "An error occurred while validating the form." , err )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordRequestVerificationEvent )
event . HttpContext = c
event . Collection = collection
2022-12-03 14:50:02 +02:00
2023-01-15 17:00:28 +02:00
submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
2022-12-03 14:50:02 +02:00
return func ( record * models . Record ) error {
event . Record = record
return api . app . OnRecordBeforeRequestVerificationRequest ( ) . Trigger ( event , func ( e * core . RecordRequestVerificationEvent ) error {
// run in background because we don't need to show the result to the client
routine . FireAndForget ( func ( ) {
2023-11-26 13:33:17 +02:00
if err := next ( e . Record ) ; err != nil {
api . app . Logger ( ) . Debug (
"Failed to send verification email" ,
slog . String ( "error" , err . Error ( ) ) ,
)
2022-12-03 14:50:02 +02:00
}
} )
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterRequestVerificationRequest ( ) . Trigger ( event , func ( e * core . RecordRequestVerificationEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-12-03 14:50:02 +02:00
} )
2022-10-30 10:28:14 +02:00
}
} )
2023-05-29 20:50:07 +02:00
// eagerly write 204 response and skip submit errors
// as a measure against users enumeration
2022-12-03 14:50:02 +02:00
if ! c . Response ( ) . Committed {
c . NoContent ( http . StatusNoContent )
}
2023-05-29 20:50:07 +02:00
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) confirmVerification ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
form := forms . NewRecordVerificationConfirm ( api . app , collection )
if readErr := c . Bind ( form ) ; readErr != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , readErr )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordConfirmVerificationEvent )
event . HttpContext = c
event . Collection = collection
2022-10-30 10:28:14 +02:00
2023-01-15 17:00:28 +02:00
_ , submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
2022-12-03 14:50:02 +02:00
return func ( record * models . Record ) error {
event . Record = record
return api . app . OnRecordBeforeConfirmVerificationRequest ( ) . Trigger ( event , func ( e * core . RecordConfirmVerificationEvent ) error {
if err := next ( e . Record ) ; err != nil {
return NewBadRequestError ( "An error occurred while submitting the form." , err )
}
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterConfirmVerificationRequest ( ) . Trigger ( event , func ( e * core . RecordConfirmVerificationEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-12-03 14:50:02 +02:00
} )
}
} )
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) requestEmailChange ( c echo . Context ) error {
2023-01-27 22:19:08 +02:00
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
2022-10-30 10:28:14 +02:00
record , _ := c . Get ( ContextAuthRecordKey ) . ( * models . Record )
if record == nil {
return NewUnauthorizedError ( "The request requires valid auth record." , nil )
}
form := forms . NewRecordEmailChangeRequest ( api . app , record )
if err := c . Bind ( form ) ; err != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , err )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordRequestEmailChangeEvent )
event . HttpContext = c
event . Collection = collection
event . Record = record
2022-10-30 10:28:14 +02:00
2023-05-29 20:50:07 +02:00
return form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
2022-12-03 14:50:02 +02:00
return func ( record * models . Record ) error {
return api . app . OnRecordBeforeRequestEmailChangeRequest ( ) . Trigger ( event , func ( e * core . RecordRequestEmailChangeEvent ) error {
if err := next ( e . Record ) ; err != nil {
return NewBadRequestError ( "Failed to request email change." , err )
}
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterRequestEmailChangeRequest ( ) . Trigger ( event , func ( e * core . RecordRequestEmailChangeEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-12-03 14:50:02 +02:00
} )
}
} )
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) confirmEmailChange ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
form := forms . NewRecordEmailChangeConfirm ( api . app , collection )
if readErr := c . Bind ( form ) ; readErr != nil {
return NewBadRequestError ( "An error occurred while loading the submitted data." , readErr )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordConfirmEmailChangeEvent )
event . HttpContext = c
event . Collection = collection
2022-12-03 14:50:02 +02:00
2023-01-15 17:00:28 +02:00
_ , submitErr := form . Submit ( func ( next forms . InterceptorNextFunc [ * models . Record ] ) forms . InterceptorNextFunc [ * models . Record ] {
2022-12-03 14:50:02 +02:00
return func ( record * models . Record ) error {
event . Record = record
return api . app . OnRecordBeforeConfirmEmailChangeRequest ( ) . Trigger ( event , func ( e * core . RecordConfirmEmailChangeEvent ) error {
if err := next ( e . Record ) ; err != nil {
return NewBadRequestError ( "Failed to confirm email change." , err )
}
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterConfirmEmailChangeRequest ( ) . Trigger ( event , func ( e * core . RecordConfirmEmailChangeEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-12-03 14:50:02 +02:00
} )
}
} )
return submitErr
2022-10-30 10:28:14 +02:00
}
func ( api * recordAuthApi ) listExternalAuths ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
id := c . PathParam ( "id" )
if id == "" {
return NewNotFoundError ( "" , nil )
}
record , err := api . app . Dao ( ) . FindRecordById ( collection . Id , id )
if err != nil || record == nil {
return NewNotFoundError ( "" , err )
}
externalAuths , err := api . app . Dao ( ) . FindAllExternalAuthsByRecord ( record )
if err != nil {
return NewBadRequestError ( "Failed to fetch the external auths for the specified auth record." , err )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordListExternalAuthsEvent )
event . HttpContext = c
event . Collection = collection
event . Record = record
event . ExternalAuths = externalAuths
2022-10-30 10:28:14 +02:00
2022-11-17 14:17:10 +02:00
return api . app . OnRecordListExternalAuthsRequest ( ) . Trigger ( event , func ( e * core . RecordListExternalAuthsEvent ) error {
2022-10-30 10:28:14 +02:00
return e . HttpContext . JSON ( http . StatusOK , e . ExternalAuths )
} )
}
func ( api * recordAuthApi ) unlinkExternalAuth ( c echo . Context ) error {
collection , _ := c . Get ( ContextCollectionKey ) . ( * models . Collection )
if collection == nil {
return NewNotFoundError ( "Missing collection context." , nil )
}
id := c . PathParam ( "id" )
provider := c . PathParam ( "provider" )
if id == "" || provider == "" {
return NewNotFoundError ( "" , nil )
}
record , err := api . app . Dao ( ) . FindRecordById ( collection . Id , id )
if err != nil || record == nil {
return NewNotFoundError ( "" , err )
}
externalAuth , err := api . app . Dao ( ) . FindExternalAuthByRecordAndProvider ( record , provider )
if err != nil {
return NewNotFoundError ( "Missing external auth provider relation." , err )
}
2023-01-27 22:19:08 +02:00
event := new ( core . RecordUnlinkExternalAuthEvent )
event . HttpContext = c
event . Collection = collection
event . Record = record
event . ExternalAuth = externalAuth
2022-10-30 10:28:14 +02:00
2023-05-29 20:50:07 +02:00
return api . app . OnRecordBeforeUnlinkExternalAuthRequest ( ) . Trigger ( event , func ( e * core . RecordUnlinkExternalAuthEvent ) error {
2022-10-30 10:28:14 +02:00
if err := api . app . Dao ( ) . DeleteExternalAuth ( externalAuth ) ; err != nil {
return NewBadRequestError ( "Cannot unlink the external auth provider." , err )
}
2023-07-18 14:31:36 +02:00
return api . app . OnRecordAfterUnlinkExternalAuthRequest ( ) . Trigger ( event , func ( e * core . RecordUnlinkExternalAuthEvent ) error {
2023-07-20 09:40:03 +02:00
if e . HttpContext . Response ( ) . Committed {
return nil
}
2023-07-18 14:31:36 +02:00
return e . HttpContext . NoContent ( http . StatusNoContent )
} )
2022-10-30 10:28:14 +02:00
} )
}
2023-04-10 21:27:00 +02:00
// -------------------------------------------------------------------
2023-04-10 21:51:59 +02:00
const oauth2SubscriptionTopic = "@oauth2"
2023-04-10 21:27:00 +02:00
func ( api * recordAuthApi ) oauth2SubscriptionRedirect ( c echo . Context ) error {
state := c . QueryParam ( "state" )
code := c . QueryParam ( "code" )
2023-04-25 10:52:56 +02:00
if code == "" || state == "" {
return NewBadRequestError ( "Invalid OAuth2 redirect parameters." , nil )
}
2023-04-10 21:27:00 +02:00
client , err := api . app . SubscriptionsBroker ( ) . ClientById ( state )
2023-04-10 21:51:59 +02:00
if err != nil || client . IsDiscarded ( ) || ! client . HasSubscription ( oauth2SubscriptionTopic ) {
2023-04-25 10:52:56 +02:00
return NewNotFoundError ( "Missing or invalid OAuth2 subscription client." , err )
2023-04-10 21:27:00 +02:00
}
data := map [ string ] string {
"state" : state ,
"code" : code ,
}
encodedData , err := json . Marshal ( data )
if err != nil {
2023-04-25 10:52:56 +02:00
return NewBadRequestError ( "Failed to marshalize OAuth2 redirect data." , err )
2023-04-10 21:27:00 +02:00
}
msg := subscriptions . Message {
2023-04-10 21:51:59 +02:00
Name : oauth2SubscriptionTopic ,
2023-07-20 15:32:21 +02:00
Data : encodedData ,
2023-04-10 21:27:00 +02:00
}
2023-07-20 15:32:21 +02:00
client . Send ( msg )
2023-04-10 21:27:00 +02:00
2023-04-25 17:29:36 +02:00
return c . Redirect ( http . StatusTemporaryRedirect , "../_/#/auth/oauth2-redirect" )
2023-04-10 21:27:00 +02:00
}