content: {-# LANGUAGE AllowAmbiguousTypes #-}
  
  module IHP.LoginSupport.Helper.Controller
  ( currentUser
  , currentUserOrNothing
  , currentUserId
  , ensureIsUser
  , HasNewSessionUrl
  , currentAdmin
  , currentAdminOrNothing
  , currentAdminId
  , ensureIsAdmin
  , login
  , sessionKey
  , logout
  , CurrentUserRecord
  , CurrentAdminRecord
  , module IHP.AuthSupport.Authentication
  , enableRowLevelSecurityIfLoggedIn
  , currentRoleOrNothing
  , currentRole
  , currentRoleId
  , ensureIsRole
  ) where
  
  import IHP.Prelude
  import IHP.Controller.Redirect
  import IHP.Controller.Session
  import IHP.LoginSupport.Types
  import qualified IHP.Controller.Session as Session
  import IHP.FlashMessages.ControllerFunctions
  import qualified IHP.ModelSupport as ModelSupport
  import IHP.ControllerSupport
  import System.IO.Unsafe (unsafePerformIO)
  import IHP.AuthSupport.Authentication
  import IHP.Controller.Context
  import qualified IHP.FrameworkConfig as FrameworkConfig
  import qualified Database.PostgreSQL.Simple.ToField as PG
  import Data.Typeable
  
  currentRoleOrNothing :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => Maybe user
  currentRoleOrNothing = case unsafePerformIO (maybeFromContext @(Maybe user)) of
      Just user -> user
      Nothing -> error ("initAuthentication @" <> show (typeRep (Proxy @user)) <> " has not been called in initContext inside FrontController of this application")
  {-# INLINE currentRoleOrNothing #-}
  
  currentRole :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => user
  currentRole = fromMaybe (redirectToLogin (newSessionUrl (Proxy @user))) (currentRoleOrNothing @user)
  {-# INLINE currentRole #-}
  
  currentRoleId :: forall user userId. (?context :: ControllerContext, HasNewSessionUrl user, HasField "id" user userId, Typeable user) => userId
  currentRoleId = (currentRole @user).id
  {-# INLINE currentRoleId #-}
  
  ensureIsRole :: forall (user :: Type). (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => IO ()
  ensureIsRole =
      case currentRoleOrNothing @user of
          Just _ -> pure ()
          Nothing -> redirectToLoginWithMessage (newSessionUrl (Proxy :: Proxy user))
  {-# INLINABLE ensureIsRole #-}
  
  currentUser :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => user
  currentUser = currentRole @user
  {-# INLINABLE currentUser #-}
  
  currentUserOrNothing :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => (Maybe user)
  currentUserOrNothing = currentRoleOrNothing @user
  {-# INLINABLE currentUserOrNothing #-}
  
  currentUserId :: forall user userId. (?context :: ControllerContext, HasNewSessionUrl user, HasField "id" user userId, Typeable user, user ~ CurrentUserRecord) => userId
  currentUserId = currentRoleId @user
  {-# INLINABLE currentUserId #-}
  
  ensureIsUser :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => IO ()
  ensureIsUser = ensureIsRole @user
  {-# INLINABLE ensureIsUser #-}
  
  currentAdmin :: forall admin. (?context :: ControllerContext, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => admin
  currentAdmin = currentRole @admin
  {-# INLINABLE currentAdmin #-}
  
  currentAdminOrNothing :: forall admin. (?context :: ControllerContext, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => (Maybe admin)
  currentAdminOrNothing = currentRoleOrNothing @admin
  {-# INLINABLE currentAdminOrNothing #-}
  
  currentAdminId :: forall admin adminId. (?context :: ControllerContext, HasNewSessionUrl admin, HasField "id" admin adminId, Typeable admin, admin ~ CurrentAdminRecord) => adminId
  currentAdminId = currentRoleId @admin
  {-# INLINABLE currentAdminId #-}
  
  ensureIsAdmin :: forall (admin :: Type). (?context :: ControllerContext, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => IO ()
  ensureIsAdmin = ensureIsRole @admin
  {-# INLINABLE ensureIsAdmin #-}
  
  -- | Log's in a user
  --
  -- Examples:
  -- 
  -- > action ExampleAction = do
  -- >     user <- query @User |> fetchOne
  -- >     login user
  -- >     
  -- >     redirectToPath "/"
  --
  login :: forall user id. (?context :: ControllerContext, KnownSymbol (ModelSupport.GetModelName user), HasField "id" user id, Show id) => user -> IO ()
  login user = Session.setSession (sessionKey @user) (tshow (user.id))
  {-# INLINABLE login #-}
  
  -- | Log's out a user
  --
  -- Example:
  --
  -- > action LogoutAction = do
  -- >     let user = currentUser
  -- >     logout user
  -- >     
  -- >     redirectToPath "/"
  --
  logout :: forall user. (?context :: ControllerContext, KnownSymbol (ModelSupport.GetModelName user)) => user -> IO ()
  logout user = Session.setSession (sessionKey @user) ("" :: Text)
  {-# INLINABLE logout #-}
  
  sessionKey :: forall user. (KnownSymbol (ModelSupport.GetModelName user)) => ByteString
  sessionKey = "login." <> cs (ModelSupport.getModelName @user)
  {-# INLINABLE sessionKey #-}
  
  redirectToLoginWithMessage :: (?context :: ControllerContext) => Text -> IO ()
  redirectToLoginWithMessage newSessionPath = do
      setSuccessMessage "Please log in to access this page"
      setSession "IHP.LoginSupport.redirectAfterLogin" getRequestPathAndQuery
      redirectToPath newSessionPath
      error "Unreachable"
  
  
  redirectToLogin :: (?context :: ControllerContext) => Text -> a
  redirectToLogin newSessionPath = unsafePerformIO $ do
      redirectToPath newSessionPath
      error "Unreachable"
  
  -- | After this call the security policies defined in your Schema.sql will be applied to the controller actions called after this
  --
  -- __Example:__
  --
  -- > instance InitControllerContext WebApplication where
  -- >     initContext = do
  -- >         initAuthentication @User
  -- >         enableRowLevelSecurityIfLoggedIn
  --
  -- Let's assume we have a policy defined in our Schema.sql that only allows users to see and edit rows in the projects table that have @projects.user_id = current_user_id@:
  --
  -- > CREATE POLICY "Users can manage their projects" ON projects USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
  --
  -- Now any database queries to our @projects@ table will have this policy applied.
  --
  -- E.g. this action will now only show the users projects, even though no explicit @filterWhere (#userId, currentUserId)@ is specified on the query:
  --
  -- > action ProjectsAction = do
  -- >     projects <- query @Project |> fetch
  --
  enableRowLevelSecurityIfLoggedIn ::
      ( ?context :: ControllerContext
      , Typeable CurrentUserRecord
      , HasNewSessionUrl CurrentUserRecord
      , HasField "id" CurrentUserRecord userId
      , PG.ToField userId
      ) => IO ()
  enableRowLevelSecurityIfLoggedIn = do
      case currentUserOrNothing of
          Just user -> do
              let rlsAuthenticatedRole = ?context.frameworkConfig.rlsAuthenticatedRole
              let rlsUserId = PG.toField user.id
              let rlsContext = ModelSupport.RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId}
              putContext rlsContext
          Nothing -> pure ()