# IHP: Controllers, Views, Forms, and Authentication --- ## Controllers and Actions Controllers live in `Web/Controller/`. Each action is a constructor in `Web/Types.hs` and a case in the controller's `action` handler. ```haskell -- Web/Controller/Widgets.hs instance Controller WidgetsController where action WidgetsAction = do widgets <- query @Widget |> fetch render IndexView { .. } action ShowWidgetAction { widgetId } = do widget <- fetch widgetId events <- query @InteractionEvent |> filterWhere (#widgetId, widgetId) |> orderByDesc #occurredAt |> limit 20 |> fetch render ShowView { .. } action CreateWidgetAction = do let widget = newRecord @Widget widget |> fill @'["name", "widgetType", "hubId"] |> validateField #name nonEmpty |> ifValid \case Left widget -> render NewView { widget } Right widget -> do widget <- createRecord widget setSuccessMessage "Widget created" redirectTo WidgetsAction ``` ### Key Patterns | Pattern | Description | |---------|-------------| | `fill @'["fieldA", "fieldB"]` | Type-safe HTTP param binding; compile error if field doesn't exist | | `validateField #name nonEmpty` | Chains a validator onto the record | | `ifValid \case Left r -> ... Right r -> ...` | Branches on validation success/failure | | `createRecord r` | INSERT; returns the persisted record | | `updateRecord r` | UPDATE | | `deleteRecord r` | DELETE | | `redirectTo SomeAction { ... }` | Type-safe redirect | | `render SomeView { .. }` | Passes in-scope bindings to the view (record wildcard) | | `respondJson value` | JSON response (no view) | | `respondHtml someHtml` | Partial HTML response (useful with htmx) | ### Before Filters ```haskell instance Controller WidgetsController where beforeAction = do ensureIsUser -- redirects to login if not authenticated -- custom per-controller guards here action ... ``` ### ControllerContext An implicit key-value map threaded through all actions and views. Stores current user, request, flash messages, layout. Access via `fromContext @SomeType` or framework helpers like `currentUser`. --- ## Views and HSX ### The View Typeclass Views are data types with a `View` instance: ```haskell data ShowView = ShowView { widget :: !Widget , events :: ![InteractionEvent] } instance View ShowView where html ShowView { .. } = [hsx|

{widget.name}

{forEach events renderEvent}
|] ``` Views can also implement a `json` method for content-negotiated API responses. ### HSX Syntax HSX is a quasi-quoter (`[hsx| ... |]`) that compiles to BlazeHtml. Checked at compile time. ```haskell [hsx| -- Interpolation (auto-escaped, XSS-safe) {widget.name} -- Raw HTML (opt-out of escaping)
{preEscapedToHtml trustedHtml}
-- Conditionals {if widget.status == "active" then [hsx|Active|] else [hsx|Inactive|]} -- Loops -- Boolean attributes: True → present, False → omitted -- Maybe attributes: Just → present, Nothing → omitted Link -- Multiple root elements are OK (unlike JSX)
Name
{widget.name}
|] ``` **`
{inner}
|] ``` Set per-controller in `beforeAction` or globally in `FrontController.initContext`: ```haskell setLayout defaultLayout ``` Data shared between layout and views (e.g., current user for the nav) is passed via `putContext`/`fromFrozenContext`. --- ## Forms and Validation ### Building Forms `formFor` binds a record to a form, auto-generating method and action: ```haskell renderForm :: Widget -> Html renderForm widget = formFor widget [hsx| {textField #name} {selectField #widgetType widgetTypeOptions} {selectField #hubId hubOptions} {textareaField #description} {submitButton} |] ``` IHP detects whether the record is new (POST to `/CreateWidget`) or persisted (POST to `/UpdateWidget`) automatically. **Available field helpers:** `textField`, `emailField`, `passwordField`, `urlField`, `numberField`, `colorField`, `dateField`, `dateTimeField`, `textareaField`, `selectField`, `radioField`, `checkboxField`, `fileField`, `hiddenField`. **Field customisation options:** `helpText`, `fieldLabel`, `placeholder`, `required`, `disabled`, `autofocus`, `fieldClass`, `labelClass`, `additionalAttributes`, `disableLabel`, `disableGroup`, `disableValidationResult`. **Custom forms:** `formForWithOptions` — override `formAction`, `formClass`, form ID, HTTP method, disable AJAX submission. **AJAX submission:** forms submit via AJAX + TurboLinks by default (no full page reload). Opt out with `disableJavascriptSubmission = True`. ### Validation ```haskell annotation |> validateField #body nonEmpty |> validateField #category (`elem` validCategories) |> ifValid \case Left invalid -> render NewView { annotation = invalid } Right valid -> do createRecord valid redirectTo WidgetAnnotationsAction { widgetId = valid.widgetId } ``` **Built-in validators:** `nonEmpty`, `isEmail`, `isPhoneNumber`, `isInRange (min, max)`. **Custom validators:** return `Success` or `Failure "message"`. For DB-aware validators, use `validateFieldIO`. **How errors attach:** `validateField` stores failures in `record.meta.annotations` (a `[(fieldName, errorMessage)]` list). When `formFor` renders a field with a failure, it auto-adds `is-invalid` CSS class and renders the error message. **Manual attachment:** `|> attachFailure #field "Custom message"`. --- ## Authentication IHP provides built-in session-based authentication using salted password hashing (`pwstore-fast`). Accounts are locked for 1 hour after 10 failed attempts. ### Database Requirements The `users` table must include: `id`, `email`, `password_hash`, `locked_at`, `failed_login_attempts`. ### Setup ```haskell -- Web/Types.hs import IHP.LoginSupport.Types type CurrentUserRecord = User instance HasNewSessionUrl User where newSessionUrl _ = "/NewSession" -- Web/FrontController.hs instance InitControllerContext WebApplication where initContext = do initAuthentication @User ``` Mount `parseRoute @SessionsController` in routes. The framework handles `NewSessionAction`, `CreateSessionAction`, `DeleteSessionAction`. ### Accessing the Current User ```haskell currentUser -- User; redirects to login if absent currentUserOrNothing -- Maybe User currentUserId -- convenience shortcut ``` ### Hooks `SessionsControllerConfig` instance supports `beforeLogin` callback for login history, account status checks, and external identity validation (relevant for IHF's multi-tenant actor attribution). ### Password Handling ```haskell -- On registration: user <- newRecord @User |> fill @'["email", "passwordHash"] |> set #passwordHash (hashPassword plaintext) |> createRecord -- On update (preserve hash when blank): updatedUser <- user |> fill @'["email"] |> ifPasswordChanged (set #passwordHash . hashPassword) |> updateRecord ``` ### Actor Attribution in IHF For IHF's interaction capture, the `actor_id` and `actor_type` on `InteractionEvent` and `Annotation` should be populated from `currentUserOrNothing` in controllers. For anonymous/low-trust actors, `actor_type = "anonymous"` with a session token as `actor_id`.