diff --git a/Guide/architecture.markdown b/Guide/architecture.markdown
index 5038ba144..ade680502 100644
--- a/Guide/architecture.markdown
+++ b/Guide/architecture.markdown
@@ -307,15 +307,16 @@ The router maps URLs like `/Posts` to `PostsAction`, `/ShowPost?postId=...` to `
### Step 7: initContext Runs
-Before your action code runs, IHP calls `initContext` from your application's `InitControllerContext` instance. This is where you set up shared controller state, such as loading the currently logged-in user:
+Before your action code runs, IHP calls `initContext` from your application's `InitControllerContext` instance. This is where you set up shared controller state, such as the default layout:
```haskell
instance InitControllerContext WebApplication where
initContext = do
setLayout defaultLayout
- initAuthentication @User
```
+Authentication runs earlier as a WAI middleware (`AuthMiddleware (authMiddleware @User)` in `Config.hs`) so the current user is already in the request vault by the time `initContext` runs.
+
If `initContext` throws an exception (for example, if authentication redirects to a login page), the action is never called.
### Step 8: beforeAction Runs
diff --git a/Guide/authentication.markdown b/Guide/authentication.markdown
index 0efc91b1d..68bf13432 100644
--- a/Guide/authentication.markdown
+++ b/Guide/authentication.markdown
@@ -887,7 +887,7 @@ config = do
### How Login and Logout Work
-When a user logs in, IHP stores the user's ID in the session under the key `login.User` (or `login.Admin` for admin authentication). The key is constructed from the model name, not the table name. The `initAuthentication @User` call in your `FrontController.hs` reads this session value on each request and fetches the corresponding user record from the database.
+When a user logs in, IHP stores the user's ID in the session under the key `login.User` (or `login.Admin` for admin authentication). The key is constructed from the model name, not the table name. The `authMiddleware @User` middleware (configured in `Config.hs`) reads this session value on each request and fetches the corresponding user record from the database.
When a user logs out, IHP sets the session value for `login.User` to an empty string. The session cookie itself remains, but the user ID is cleared.
@@ -980,17 +980,19 @@ Here is a summary of every change needed to add authentication. Use this as a re
- Implement the login form view
6. In `Web/FrontController.hs`:
- - Add `import IHP.LoginSupport.Middleware`
- Add `import Web.Controller.Sessions`
- Mount the controller: `parseRoute @SessionsController`
- - Add `initAuthentication @User` to `initContext`
-7. Add `ensureIsUser` to `beforeAction` in any controller that requires login.
+7. In `Config/Config.hs`:
+ - Add `import IHP.LoginSupport.Middleware`
+ - Add `option $ AuthMiddleware (authMiddleware @User)`
+
+8. Add `ensureIsUser` to `beforeAction` in any controller that requires login.
-8. Add a logout link in your layout: `Logout`
+9. Add a logout link in your layout: `Logout`
-9. (Optional) Create a registration controller and view for user sign-up.
+10. (Optional) Create a registration controller and view for user sign-up.
-10. (Optional) Set up password reset flow with token generation and email.
+11. (Optional) Set up password reset flow with token generation and email.
[Next: Authorization](https://ihp.digitallyinduced.com/Guide/authorization.html)
diff --git a/Guide/authorization.markdown b/Guide/authorization.markdown
index 75e2cb06b..c454a7f39 100644
--- a/Guide/authorization.markdown
+++ b/Guide/authorization.markdown
@@ -520,13 +520,12 @@ This approach gives you full control over what the user sees and where they are
For an additional layer of protection, IHP supports PostgreSQL Row-Level Security (RLS). With RLS, the database itself enforces that users can only access rows they are authorized to see, regardless of what your application code does.
-See the [IHP DataSync documentation](https://ihp.digitallyinduced.com/Guide/realtime-spas.html) for details on how to set up RLS policies. In your `FrontController.hs`, call `enableRowLevelSecurityIfLoggedIn` after `initAuthentication`:
+See the [IHP DataSync documentation](https://ihp.digitallyinduced.com/Guide/realtime-spas.html) for details on how to set up RLS policies. With `AuthMiddleware (authMiddleware @User)` enabled in `Config.hs`, call `enableRowLevelSecurityIfLoggedIn` from your `FrontController.hs`:
```haskell
instance InitControllerContext WebApplication where
initContext = do
setLayout defaultLayout
- initAuthentication @User
enableRowLevelSecurityIfLoggedIn
```
diff --git a/Guide/passkeys.markdown b/Guide/passkeys.markdown
index 4dc6e0ab9..e7df3b1d2 100644
--- a/Guide/passkeys.markdown
+++ b/Guide/passkeys.markdown
@@ -340,10 +340,11 @@ instance FrontController WebApplication where
]
instance InitControllerContext WebApplication where
- initContext = do
- initAuthentication @User
+ initContext = pure ()
```
+In `Config/Config.hs` add `option $ AuthMiddleware (authMiddleware @User)` so the current user is loaded into the request vault on every request.
+
### Sessions Controller
Create `Web/Controller/Sessions.hs` for logout:
diff --git a/Guide/recipes.markdown b/Guide/recipes.markdown
index bfd6b4fd4..0fc352f6a 100644
--- a/Guide/recipes.markdown
+++ b/Guide/recipes.markdown
@@ -233,9 +233,7 @@ The `DeleteSessionAction` expects a `HTTP DELETE` request, which is set by JavaS
## Making a dynamic Login/Logout button
-Depending on the `Maybe User` type in the [ControllerContext](https://ihp.digitallyinduced.com/api-docs/IHP-Controller-Context.html), by using [`fromFrozenContext`](https://ihp.digitallyinduced.com/api-docs/IHP-Controller-Context.html#v:fromFrozenContext) we can tell if no user is logged in when the `Maybe User` is `Nothing`, and confirm someone is logged in if the `Maybe User` is a `Just user`. Here is an example of a navbar, which has a dynamic Login/Logout button. You can define this in your View/Layout to reuse this in your Views.
-
-> The `@` syntax from [`fromFrozenContext @(Maybe User)`](https://ihp.digitallyinduced.com/api-docs/IHP-Controller-Context.html#v:fromFrozenContext) is just syntax sugar for `let maybeUser :: Maybe User = fromFrozenContext`
+Use [`currentUserOrNothing`](https://ihp.digitallyinduced.com/api-docs/IHP-LoginSupport-Helper-View.html#v:currentUserOrNothing) to check whether someone is logged in. It returns `Just user` when a user is authenticated and `Nothing` otherwise. Here is an example of a navbar with a dynamic Login/Logout button that you can place in your View/Layout to reuse across your views.
```haskell
navbar :: Html
@@ -259,7 +257,7 @@ navbar = [hsx|
where
loginLogoutButton :: Html
loginLogoutButton =
- case fromFrozenContext @(Maybe User) of
+ case currentUserOrNothing of
Just user -> [hsx|Logout|]
Nothing -> [hsx|Login|]
```
diff --git a/Guide/seo.markdown b/Guide/seo.markdown
index 491b291ba..50998a6ac 100644
--- a/Guide/seo.markdown
+++ b/Guide/seo.markdown
@@ -206,7 +206,7 @@ defaultLayout inner = [hsx|
|]
```
-If you want per-page Twitter tags, you can use the same `putContext`/`fromFrozenContext` pattern described in the [Views documentation](view.html) for layout variables.
+If you want per-page Twitter tags, you can use the same vault-key pattern described in the [Views documentation](view.html) for layout variables.
## Canonical URLs
@@ -214,15 +214,36 @@ A canonical URL tells search engines which version of a page is the "official" o
### Adding a Canonical Tag to the Layout
-Since IHP does not have a built-in `setCanonical` helper, you can use the `putContext`/`fromFrozenContext` pattern to pass a canonical URL from your view to the layout.
+Since IHP does not have a built-in `setCanonical` helper, you can store the canonical URL in the WAI request vault and read it back in the layout.
-First, create a newtype to store the canonical URL. You can add this to `Web/View/Layout.hs` or a shared module:
+First, declare a vault key and a tiny middleware to set it:
```haskell
+-- Web/View/Layout.hs (or a shared module)
+import qualified Data.Vault.Lazy as Vault
+import Network.Wai (Request, vault)
+import System.IO.Unsafe (unsafePerformIO)
+
newtype CanonicalUrl = CanonicalUrl Text
+
+canonicalUrlVaultKey :: Vault.Key (IORef (Maybe CanonicalUrl))
+canonicalUrlVaultKey = unsafePerformIO Vault.newKey
+{-# NOINLINE canonicalUrlVaultKey #-}
+
+setCanonical :: (?request :: Request) => Text -> IO ()
+setCanonical url = case Vault.lookup canonicalUrlVaultKey (vault ?request) of
+ Just ref -> writeIORef ref (Just (CanonicalUrl url))
+ Nothing -> pure ()
+
+currentCanonical :: (?request :: Request) => Maybe CanonicalUrl
+currentCanonical = case Vault.lookup canonicalUrlVaultKey (vault ?request) of
+ Just ref -> unsafePerformIO (readIORef ref)
+ Nothing -> Nothing
```
-In your layout, read the canonical URL from the context and render it if present:
+Add `insertNewIORefVaultMiddleware canonicalUrlVaultKey Nothing` to your `Config/Config.hs` middleware stack so the IORef is created on every request.
+
+In your layout, read the canonical URL from the vault and render it if present:
```haskell
defaultLayout :: Html -> Html
@@ -240,21 +261,21 @@ defaultLayout inner = [hsx|