### Set Up Local Documentation Development Source: https://github.com/digitallyinduced/ihp/blob/master/CONTRIBUTING.md Navigate to the `IHP` directory and run `direnv allow`. Then, navigate to the `Guide` directory and run `direnv allow` again to install dependencies. ```bash cd IHP direnv allow cd Guide direnv allow ``` -------------------------------- ### Install NPM Modules Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/npm.markdown Use 'npm install' to add required Node modules to your project. This example adds 'tailwindcss', 'postcss', and 'autoprefixer'. ```bash npm add tailwindcss postcss autoprefixer ``` -------------------------------- ### Start the IHP application Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown After building the application with Nix, run this command to start the production server. ```bash result/bin/RunProdServer ``` -------------------------------- ### Setup Integration Test Module (Test/Integration.hs) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/testing.markdown Configure the main module for integration tests. This setup uses `withIHPApp` and requires a PostgreSQL database with the application schema loaded. ```haskell -- Test/Integration.hs module Main where import Test.Hspec import IHP.Prelude import Test.Controller.PostsSpec main :: IO () hspec do Test.Controller.PostsSpec.tests ``` -------------------------------- ### Start Application Service Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Manually start the IHP application service using systemctl. ```bash systemctl start app.service ``` -------------------------------- ### Controller Lifecycle Example Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/controller.markdown Demonstrates the controller lifecycle where `beforeAction` is executed before the `action`. This example logs 'A' before 'B' when `ShowPostAction` is called. ```haskell instance Controller PostsController where beforeAction = putStrLn "A" action ShowPotsAction { postId } = putStrLn "B" ``` -------------------------------- ### Install Nix on Server Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Installs the Nix package manager on your server using the official installation script. This is a prerequisite for building IHP applications. ```bash curl -L https://nixos.org/nix/install | sh ``` -------------------------------- ### Add Startup Initializer Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/config.markdown Define custom IO actions to run when the application server starts. These initializers run concurrently using `async` and can be used for tasks like warming caches or starting background processes. ```haskell -- Config.hs config = do addInitializer do putStrLn "App server started!" -- Warm caches, start background tasks, etc. ``` -------------------------------- ### POST /api/passkey/authenticate/begin Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Starts the passkey authentication process by generating a challenge and returning it to the client. ```APIDOC ## POST /api/passkey/authenticate/begin ### Description Generates a challenge for passkey authentication and returns the necessary options to the client. ### Method POST ### Endpoint /api/passkey/authenticate/begin ### Response #### Success Response (200) - **credentialOptions** (object) - Options for the client to initiate credential authentication. #### Response Example ```json { "credentialOptions": { "challenge": "...", "rpId": "localhost", "userVerification": "required" } } ``` #### Error Response (400, 422) - **error** (string) - An error message describing the issue. ``` -------------------------------- ### Install IHP CLI and Create New Project Source: https://github.com/digitallyinduced/ihp/blob/master/README.md These bash commands show how to install the IHP command-line interface using Nix and then create a new IHP project named 'myproject'. It also includes the command to set up the development environment. ```bash nix profile install nixpkgs#ihp-new ihp-new myproject cd myproject && devenv up ``` -------------------------------- ### Install React and IHP JS Helpers Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/realtime-spas.markdown Install React, ReactDOM, esbuild for bundling, and IHP's JavaScript data synchronization helpers using `npm add`. Ensure you are in the `Frontend` directory when running these commands. ```bash cd Frontend # npm needs to be run from the Frontend directory # Install react and react-dom npm add react react-dom # We also need esbuild for bundling npm add esbuild # Install IHP JS helpers npm add "https://gitpkg.now.sh/digitallyinduced/ihp/lib/IHP/DataSync?0babfec7a90675ca37d56c38dac69e784e64fc83" ``` -------------------------------- ### Install IHP using Nix Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/installation.markdown Use this command to install the IHP framework via the Nix package manager. Ensure Nix is installed and configured. ```bash nix profile install nixpkgs#ihp-new ``` -------------------------------- ### Basic IHP Configuration Example Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/config.markdown Demonstrates setting basic configuration options like Production environment and AppHostname in Config.hs. ```haskell module Config where import IHP.Prelude import IHP.Environment import IHP.FrameworkConfig config :: ConfigBuilder config = do option Production option (AppHostname "myapp.com") ``` -------------------------------- ### Install ihp-new with Nix Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md Install the updated `ihp-new` tool using Nix. Choose the appropriate command based on whether you use `nix-env` or `nix profile`. ```bash # Run this: nix-env -f https://downloads.digitallyinduced.com/ihp-new.tar.gz -i ihp-new # Or this if you use nix profile: nix profile install -f https://downloads.digitallyinduced.com/ihp-new.tar.gz ``` -------------------------------- ### Check Nix Installation Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/installation.markdown Verify that Nix is installed and accessible on your system's PATH. This is a prerequisite for IHP development. ```bash nix --version ``` -------------------------------- ### Example Script: Hello World to All Users Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/scripts.markdown Fetches all users and prints a personalized greeting to the console. Requires the 'User' type to be defined. ```haskell #!/usr/bin/env run-script module Application.Script.HelloWorldToAllUsers where import Application.Script.Prelude run :: Script run = do users <- query @User |> fetch forEach users \user -> do putStrLn $ "Hello World, " <> user.firstname <> "!" ``` -------------------------------- ### Replace Welcome Controller with Custom Start Page Source: https://github.com/digitallyinduced/ihp/blob/master/ihp-welcome/README.md To remove the default welcome controller for production, replace `startPage WelcomeAction` with your own custom start page action, such as `ProjectsAction`. Ensure the `ihp-welcome` dependency and import are also removed. ```haskell instance FrontController WebApplication where controllers = [ startPage ProjectsAction -- Your own start page -- ... other controllers ] ``` -------------------------------- ### Start GHCi in IHP Project Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/debugging.markdown Start the GHCi interactive interpreter from your project directory. Ensure `devenv up` is running in another terminal. IHP's `.ghci` configuration automatically loads necessary extensions and modules. ```bash ghci ``` -------------------------------- ### Controller Authentication Example Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/controller.markdown Illustrates using `beforeAction` for authentication. `ensureIsUser` is called before the `ShowPostAction` to protect access. ```haskell instance Controller PostsController where beforeAction = ensureIsUser action ShowPostAction { postId } = ... ``` -------------------------------- ### Custom Configuration Example Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/scripts.markdown Defines a custom configuration builder named 'appConfig' in 'Config.hs', including development mode and SES settings. ```haskell -- Config.hs import IHP.Log.Types appConfig :: ConfigBuilder appConfig = do option Development option $ SES { accessKey = "myAccessKey" , secretKey = "mySecretAccessKey" , region = "eu-west-1" } ``` -------------------------------- ### robots.txt Content Example Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/seo.markdown A sample `robots.txt` file to be placed in the `static/` directory. It specifies crawler allowances and the sitemap location. ```text User-agent: * Allow: / Sitemap: https://yourdomain.com/sitemap.xml ``` -------------------------------- ### Get unique results with distinctOn Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/realtime-spas.markdown Use distinctOn() to retrieve unique results based on a specified column. The example fetches distinct todos based on userId. ```javascript const userTodos = await query('todos') .distinctOn('userId') .fetch() ``` -------------------------------- ### Set up PostgreSQL Database with Docker Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Initializes a PostgreSQL database container and imports the application schema and fixtures. ```bash $ docker run --name app-db -e POSTGRES_PASSWORD=mysecretpassword -d postgres # Import the Schema.sql $ docker exec -i app-db psql -U postgres -d postgres < Application/Schema.sql CREATE EXTENSION CREATE TABLE ... # Import the Fixtures.sql $ docker exec -i app-db psql -U postgres -d postgres < Application/Fixtures.sql ALTER TABLE INSERT 0 1 INSERT 0 1 INSERT 0 1 ... ``` -------------------------------- ### Stream OpenAI Completion in IHP Controller Source: https://github.com/digitallyinduced/ihp/blob/master/ihp-openai/README.md This example demonstrates how to use the `streamCompletion` function from the `IHP.OpenAI` module within an IHP controller to get AI-generated answers. Ensure your OpenAI secret key is securely managed, preferably not hardcoded. ```haskell module Web.Controller.Questions where import Web.Controller.Prelude import Web.View.Questions.Index import Web.View.Questions.New import Web.View.Questions.Edit import Web.View.Questions.Show import qualified IHP.OpenAI as GPT instance Controller QuestionsController where action QuestionsAction = autoRefresh do questions <- query @Question |> orderByDesc #createdAt |> fetch render IndexView { .. } action NewQuestionAction = do let question = newRecord |> set #question "What makes haskell so great?" render NewView { .. } action CreateQuestionAction = do let question = newRecord @Question question |> fill @'["question"] |> validateField #question nonEmpty |> ifValid \case Left question -> render NewView { .. } Right question -> do question <- question |> createRecord setSuccessMessage "Question created" fillAnswer question redirectTo QuestionsAction action DeleteQuestionAction { questionId } = do question <- fetch questionId deleteRecord question setSuccessMessage "Question deleted" redirectTo QuestionsAction fillAnswer :: (?modelContext :: ModelContext) => Question -> IO (Async ()) fillAnswer question = do -- Put your OpenAI secret key below: let secretKey = "sk-XXXXXXXX" -- This should be done with an IHP job worker instead of async async do GPT.streamCompletion secretKey (buildCompletionRequest question) (clearAnswer question) (appendToken question) pure () buildCompletionRequest :: Question -> GPT.CompletionRequest buildCompletionRequest Question { question } = -- Here you can adjust the parameters of the request GPT.newCompletionRequest { GPT.maxTokens = 512 , GPT.prompt = [trimming| Question: ${question} Answer: |] } -- | Sets the answer field back to an empty string clearAnswer :: (?modelContext :: ModelContext) => Question -> IO () clearAnswer question = do sqlExec "UPDATE questions SET answer = '' WHERE id = ?" (Only question.id) pure () -- | Stores a couple of newly received characters to the database appendToken :: (?modelContext :: ModelContext) => Question -> Text -> IO () appendToken question token = do sqlExec "UPDATE questions SET answer = answer || ? WHERE id = ?" (token, question.id) pure () ``` -------------------------------- ### Preventing Data Modification with Forms in Haskell Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/security.markdown Use forms with POST, DELETE, or PUT methods for actions that modify data. GET requests should not have side effects. This example shows the correct way to use a form for a DELETE action. ```haskell -- Correct: Use a form with POST for state-changing actions [hsx|
|] ``` ```haskell -- Wrong: Using a plain link for a destructive action -- (GET requests are not protected by SameSite=Lax) [hsx|Delete|] ``` -------------------------------- ### Navigate to Project Directory Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown Change your current directory to the newly created 'blog' project folder. ```bash cd blog ``` -------------------------------- ### Initialize Terraform Configuration Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Run this command in the IaC/aws folder to initialize your Terraform configuration. Ensure Terraform is installed and AWS credentials are set up. ```bash terraform init ``` -------------------------------- ### Initialize NPM in Frontend Directory Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/realtime-spas.markdown Navigate to the `Frontend` directory and run `npm init` to generate a `package.json` file. This step is necessary before installing any Node.js dependencies. ```bash mkdir Frontend cd Frontend # npm needs to be run from the Frontend directory npm init ``` -------------------------------- ### Install NPM Packages Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/assets.markdown Use `npm add` to install JavaScript libraries from the NPM registry. The installed files will be located in the `node_modules/` directory. ```bash npm add chart.js ``` -------------------------------- ### Migrate Environment Variables from start script to .envrc Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md This diff shows how to move environment variable exports from a `start` script into the `.envrc` file. This is necessary as the `start` script is no longer used with devenv.sh. ```diff if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8=" fi use flake . --impure if [ -f .env ] then set -o allexport source .env set +o allexport fi ## Add the exports from your start script here: +export SES_ACCESS_KEY="XXXX" +export SES_SECRET_KEY="XXXX" +export SES_REGION="us-east-1" ``` -------------------------------- ### Basic IHP Project Setup in flake.nix Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/package-management.markdown This snippet shows the basic structure for an IHP project's `flake.nix` file, including importing necessary modules and defining system-specific configurations for IHP. ```nix { systems = import systems; imports = [ ihp.flakeModules.default ]; perSystem = { pkgs, ... }: { ihp = { enable = true; projectPath = ./.; packages = with pkgs; [ # Native dependencies, e.g. imagemagick ]; haskellPackages = p: with p; [ # Haskell dependencies go here p.ihp base wai text ]; doJailbreakPackages = [ "google-oauth2" ]; }; }; }; } ``` -------------------------------- ### Submit Form using GET Method Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/form.markdown Override the default POST method to use GET for form submissions by setting `formMethod` to "GET" using `formForWithOptions`. This can be useful for search forms. ```haskell renderForm :: Post -> Html renderForm post = formForWithOptions post options [hsx||] options :: FormContext Post -> FormContext Post options formContext = formContext |> set #formMethod "GET" ``` -------------------------------- ### Initialize Stripe in Config Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/stripe.markdown Call 'initStripe' within your 'Config/Config.hs' to set up Stripe credentials and options. This should be done after other configuration options. ```haskell module Config where import IHP.Prelude import IHP.Environment import IHP.FrameworkConfig import IHP.Stripe.Config config :: ConfigBuilder config = do option Development option (AppHostname "localhost") initStripe ``` -------------------------------- ### Mount SessionsController and Initialize Authentication Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/authentication.markdown In `Web/FrontController.hs`, import the `Sessions` controller, mount it using `parseRoute`, and initialize authentication for your `User` type in `initContext`. ```haskell import IHP.LoginSupport.Middleware import Web.Controller.Sessions -- ... inside initContext ... initAuthentication @User -- ... inside parseRoutes ... parseRoute @SessionsController ``` -------------------------------- ### Example Rendered Flash Message HTML Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/session.markdown This is an example of the HTML structure generated by `renderFlashMessages` for success and error messages. ```html
{successMessage}
{errorMessage}
``` -------------------------------- ### Initialize Filebase Storage in Config Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/file-storage.markdown Import and call `initFilebaseStorage` in your `Config.hs` file. Replace `my-bucket-name` with your actual bucket name. Ensure `FILEBASE_KEY` and `FILEBASE_SECRET` environment variables are set. ```haskell import IHP.FileStorage.Config ``` ```haskell module Config where import IHP.Prelude import IHP.Environment import IHP.FrameworkConfig import IHP.FileStorage.Config config :: ConfigBuilder config = do option Development option (AppHostname "localhost") initFilebaseStorage "my-bucket-name" ``` -------------------------------- ### Check direnv Installation Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/installation.markdown Confirm that direnv is installed correctly. direnv is used to automatically manage your shell environment for IHP projects. ```bash direnv --version ``` -------------------------------- ### Nix Multi-Tenant Service Definitions Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Use 'let' functions to define services for multiple tenants, avoiding duplication. This example shows how to define migration and worker services for a main tenant and an additional tenant. ```nix let migrate-service = databaseUrl: minimumRevision: { serviceConfig = { Type = "oneshot"; ExecStart = self.inputs.myapp.inputs.ihp.apps."${pkgs.system}".migrate.program; }; environment = { DATABASE_URL = databaseUrl; MINIMUM_REVISION = minimumRevision; IHP_MIGRATION_DIR = "${migrationFiles}/Application/Migration/"; }; wantedBy = [ "multi-user.target" ]; after = [ "postgresql.service" ]; }; worker-service = databaseUrl: { enable = true; serviceConfig = { Type = "simple"; ExecStart = "${package}/bin/RunJobs"; Restart = "on-failure"; }; environment = ihpEnv // { GHCRTS = "-A1M -N1 -qn1"; DATABASE_URL = databaseUrl; }; wantedBy = [ "multi-user.target" ]; }; in { # Main tenant systemd.services.myapp-migrate = migrate-service "postgres://myapp:@127.0.0.1/myapp" "0"; systemd.services.myapp-worker = worker-service "postgres://myapp:@127.0.0.1/myapp"; # Additional tenants — just one line each systemd.services.myapp-migrate-acme = migrate-service "postgres://myapp:@127.0.0.1/myapp_acme" "0"; systemd.services.myapp-worker-acme = worker-service "postgres://myapp:@127.0.0.1/myapp_acme"; } ``` -------------------------------- ### Example Post ID Instance Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown Shows an example of the 'Id Post' type in Haskell, representing a specific post ID as a UUID string. ```haskell "5a8d1be2-33e3-4d3f-9812-b16576fe6a38" :: Id Post ``` -------------------------------- ### Build unoptimized binary with Nix Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Use this command to build an unoptimized binary. This is useful for faster development cycles and testing. ```bash nix build .#unoptimized-prod-server ``` -------------------------------- ### Define Custom Start Page Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/routing.markdown Customize the application's start page by providing a specific action to the startPage function in Web/FrontController.hs. Ensure to remove any default startPage definitions. ```haskell instance FrontController WebApplication where controllers = [ startPage ProjectsAction -- Generator Marker ] ``` -------------------------------- ### Get Optional Parameter with Default Value Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/controller.markdown Use `paramOrDefault` to get a parameter's value, falling back to a default if it's missing or invalid. Specify the type with `@Type`. ```haskell action UsersAction = do let maxItems = paramOrDefault @Int 50 "maxItems" ``` -------------------------------- ### Initialize Filebase File Storage Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/config.markdown Sets up file storage to use the Filebase IPFS-backed service. Requires Filebase access and secret keys, and the bucket name. ```haskell -- Config.hs import IHP.FileStorage.Config config = do initFilebaseStorage "my-bucket-name" ``` -------------------------------- ### View Running Processes Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Use the 'top' command to observe real-time process activity and resource consumption. ```bash top ``` -------------------------------- ### Curl Command to Show a Single Post Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/json-api.markdown Example `curl` command to show a single post by its ID. Replace the example UUID with the actual post ID. Assumes the API is running on localhost:8000. ```bash # Show a single post curl http://localhost:8000/ShowPost?postId=5a8a4c5e-1b5e-4b2a-9c0a-3e5e4b8e1b5e ``` -------------------------------- ### Passkey Registration - Begin Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Initiates the passkey registration process by generating a challenge and returning credential options. ```APIDOC ## POST /auth/begin-passkey-registration ### Description Generates a challenge and returns credential options for initiating passkey registration. This is the first step in the registration flow. ### Method POST ### Endpoint /auth/begin-passkey-registration ### Parameters #### Request Body - **email** (string) - Required - The email address of the user. ### Request Example ```json { "email": "user@example.com" } ``` ### Response #### Success Response (200) - **challenge** (string) - The generated challenge for registration. - **rpId** (string) - The Relying Party identifier. - **user** (object) - User information including ID, name, and display name. - **id** (string) - User's unique identifier. - **name** (string) - User's name. - **displayName** (string) - User's display name. - **excludeCredentials** (array) - A list of credentials to exclude during registration. - **id** (string) - Credential ID. - **type** (string) - Credential type. - **transports** (array) - Allowed transports for the credential. #### Response Example ```json { "challenge": "some_challenge_string", "rpId": "your_rp_id.com", "user": { "id": "user_uuid", "name": "user@example.com", "displayName": "User Name" }, "excludeCredentials": [ { "id": "existing_credential_id", "type": "public-key", "transports": ["internal", "usb", "nfc", "ble"] } ] } ``` ``` -------------------------------- ### Deploying with Nix Run Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Commands to update flake dependencies and deploy to production using the defined flake app. `--use-substitutes` enables pulling pre-built packages, and `--build-host` specifies where the build occurs. ```bash nix flake update # Pull latest app versions nix run # Deploy to production ``` -------------------------------- ### Apply Initial Schema to Database (Bash) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Applies the initial SQL schema to a specific database using psql. This command is typically run after NixOS has provisioned the database. ```bash sudo -u postgres psql secondapp < /path/to/Application/Schema.sql ``` -------------------------------- ### Controller Data Structure Examples Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown Controller data structures should end in 'Controller' and typically represent actions related to a plural resource. Examples include CRUD operations for Users, Companies, Admins, and session management. ```haskell data UsersController = UsersAction | NewUserAction | ShowUserAction { userId :: !(Id User) } | CreateUserAction | EditUserAction { userId :: !(Id User) } | UpdateUserAction { userId :: !(Id User) } | DeleteUserAction { userId :: !(Id User) } deriving (Eq, Show, Data) data CompaniesController = CompaniesAction | NewCompanyAction | ShowCompanyAction { companyId :: !(Id Company) } | CreateCompanyAction | EditCompanyAction { companyId :: !(Id Company) } | UpdateCompanyAction { companyId :: !(Id Company) } | DeleteCompanyAction { companyId :: !(Id Company) } deriving (Eq, Show, Data) data AdminsController = AdminsAction | NewAdminAction | ShowAdminAction { adminId :: !(Id Admin) } | CreateAdminAction | EditAdminAction { adminId :: !(Id Admin) } | UpdateAdminAction { adminId :: !(Id Admin) } | DeleteAdminAction { adminId :: !(Id Admin) } deriving (Eq, Show, Data) data SessionsController = NewSessionAction | CreateSessionAction | DeleteSessionAction deriving (Eq, Show, Data) ``` ```haskell data CompanyController -- Type should be plural in this case = CompaniesAction | NewCompanyAction | ShowCompanyAction { companyId :: !(Id Company) } | CreateCompanyAction | EditCompanyAction { companyId :: !(Id Company) } | UpdateCompanyAction { companyId :: !(Id Company) } | DeleteCompanyAction { companyId :: !(Id Company) } deriving (Eq, Show, Data) ``` -------------------------------- ### Passkey Authentication - Begin Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Initiates the passkey authentication process by generating a challenge and returning credential requests. ```APIDOC ## POST /auth/begin-passkey-authentication ### Description Initiates the passkey authentication process. It generates a challenge and returns the necessary options for the client to request a credential assertion. ### Method POST ### Endpoint /auth/begin-passkey-authentication ### Parameters #### Request Body - **email** (string) - Required - The email address of the user attempting to authenticate. ### Request Example ```json { "email": "user@example.com" } ``` ### Response #### Success Response (200) - **challenge** (string) - The generated challenge for authentication. - **rpId** (string) - The Relying Party identifier. - **allowCredentials** (array) - A list of allowed credentials for this user. - **id** (string) - Credential ID. - **type** (string) - Credential type. - **transports** (array) - Allowed transports for the credential. #### Response Example ```json { "challenge": "auth_challenge_string", "rpId": "your_rp_id.com", "allowCredentials": [ { "id": "user_credential_id", "type": "public-key", "transports": ["internal", "usb", "nfc", "ble"] } ] } ``` ``` -------------------------------- ### Manually Create Database User and Database (Bash) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Manually creates a PostgreSQL user and a database owned by that user. This is an alternative to declarative provisioning. ```bash ssh production sudo -u postgres createuser secondapp sudo -u postgres createdb -O secondapp secondapp ``` -------------------------------- ### Adjust macOS Open File Limit in start script Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md On macOS, append this block to the start script to increase the maximum number of open files, which is necessary for IHP to run well due to wai-static-middleware's file handle behavior. ```bash # On macOS the default max count of open files is 256. IHP needs atleast 1024 to run well. # # The wai-static-middleware sometimes doesn't close it's file handles directly (likely because of it's use of lazy bytestrings) # and then we usually hit the file limit of 256 at some point. With 1024 the limit is usually never hit as the GC kicks in earlier # and will close the remaining lazy bytestring handles. if [[ $OSTYPE == 'darwin'* ]]; then ulimit -n 4096 fi ``` -------------------------------- ### Build optimized production binary with Nix Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Use this command to build an optimized production-ready binary for your IHP application. This is recommended for deployment. ```bash nix build .#optimized-prod-server ``` -------------------------------- ### Get User Notifications API Source: https://github.com/digitallyinduced/ihp/blob/master/AGENTS.md Retrieves a list of notifications for a specific user. ```APIDOC ## GET /api/users/{userId}/notifications ### Description Fetches all notifications associated with a given user ID. ### Method GET ### Endpoint /api/users/{userId}/notifications ### Parameters #### Path Parameters - **userId** (Int32) - Required - The unique identifier of the user. ### Response #### Success Response (200) - **notifications** (Vector) - A list of notifications, where each notification is a tuple containing: - **notificationId** (Int32) - The unique ID of the notification. - **message** (Text) - The content of the notification. - **isRead** (Bool) - Indicates if the notification has been read. #### Response Example ```json { "notifications": [ [1, "Your order has been shipped", false], [2, "New message from support", false] ] } ``` ``` -------------------------------- ### Create and Schedule Background Jobs Source: https://context7.com/digitallyinduced/ihp/llms.txt Demonstrates creating background jobs from controllers. Jobs can be run immediately using `create` or scheduled for future execution using `set #runAt` with a specified `runTime`. Requires `Web.Controller.Prelude`. ```haskell -- Creating jobs from controllers action SignupAction = do user <- newRecord @User |> fill @'["email"] |> createRecord -- Run job immediately newRecord @SendEmailJob |> set #userId user.id |> set #template "welcome" |> create redirectTo DashboardAction -- Schedule job for future execution action ScheduleReminderAction = do now <- getCurrentTime let runTime = addUTCTime (nominalDay * 7) now -- 7 days from now newRecord @SendReminderJob |> set #userId currentUserId |> set #runAt runTime |> create setSuccessMessage "Reminder scheduled" ``` -------------------------------- ### Begin Passkey Registration Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Generates a challenge and returns credential options for passkey registration. Handles both new users (validating email) and existing users (using their info and excluding existing credentials). Requires `BeginPasskeyRegistrationPayload` for JSON parsing and uses session storage for challenge, email, and user ID. ```haskell -- Step 1 of registration: generate challenge, return credential options action BeginPasskeyRegistrationAction = do payloadResult <- parseJsonBody @BeginPasskeyRegistrationPayload payload <- case payloadResult of Left errorMessage -> jsonError status400 errorMessage Right payload -> pure payload -- For logged-in users adding another passkey, use their existing info. -- For new users, validate the email and generate a new user ID. (pendingUserId, email, excludeCredentials) <- case currentUserOrNothing @User of Just user -> do existingPasskeys <- query @Passkey |> filterWhere (#userId, user.id) |> fetch pure ( user.id , user.email , map passkeyCredentialDescriptor existingPasskeys ) Nothing -> do email <- requireAvailableEmail payload.email userUuid <- sqlQueryScalar "SELECT uuid_generate_v4()" () pure (Id userUuid, email, []) challenge <- liftIO generateChallenge setSession registrationChallengeSessionKey (unChallenge challenge) setSession registrationEmailSessionKey email setSession registrationUserIdSessionKey (inputValue pendingUserId) renderJson (WebAuthnJson.wjEncodeCredentialOptionsRegistration (registrationCredentialOptions challenge pendingUserId email excludeCredentials)) ``` -------------------------------- ### PostsController Definition Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/routing.markdown Example definition of a PostsController with various actions, including those with parameters. ```haskell data PostsController = PostsAction | NewPostAction | ShowPostAction { postId :: !(Id Post) } | CreatePostAction | EditPostAction { postId :: !(Id Post) } | UpdatePostAction { postId :: !(Id Post) } | DeletePostAction { postId :: !(Id Post) } ``` -------------------------------- ### Install ImageMagick Dependency Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/file-storage.markdown Add `imagemagick` to `otherDeps` in `default.nix` to use ImageMagick preprocessor. ```nix otherDeps = p: with p; [ imagemagick ]; ``` -------------------------------- ### Get User Details API Source: https://github.com/digitallyinduced/ihp/blob/master/AGENTS.md Retrieves detailed information for a specific user by their ID. ```APIDOC ## GET /api/users/{userId} ### Description Retrieves the details of a user specified by their unique ID. ### Method GET ### Endpoint /api/users/{userId} ### Parameters #### Path Parameters - **userId** (Int32) - Required - The unique identifier of the user. ### Response #### Success Response (200) - **name** (Text) - The name of the user. - **email** (Text) - The email address of the user. - **phone** (Maybe Text) - The phone number of the user, if provided. - **isActive** (Bool) - Indicates if the user account is active. #### Response Example ```json { "name": "Test User", "email": "test@example.com", "phone": "+1234567890", "isActive": true } ``` ``` -------------------------------- ### Nix Flake App for Server Deployment Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Define a deploy command as a flake app in your server's `flake.nix` for managing NixOS configurations. This example sets up a production configuration and a default app to deploy it. ```nix { outputs = { self, nixpkgs, ... }: { nixosConfigurations.production = nixpkgs.lib.nixosSystem { system = "aarch64-linux"; specialArgs = { inherit self; }; modules = [ ./hardware-configuration.nix ./configuration.nix ./postgres.nix ./myapp.nix ./secondapp.nix ]; }; apps.aarch64-darwin.default = { type = "app"; program = let pkgs = nixpkgs.legacyPackages.aarch64-darwin; in "${pkgs.writeShellApplication { name = "deploy"; text = '' set -euo pipefail exec ${pkgs.nixos-rebuild}/bin/nixos-rebuild switch \ --use-substitutes \ --fast \ --flake ${self}#production \ --target-host root@production \ --build-host root@production ''; }}/bin/deploy"; }; }; } ``` -------------------------------- ### Using isEmail Validator Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/validation.markdown Example of using the `isEmail` validator for an email text field. ```haskell |> validateField #email isEmail ``` -------------------------------- ### Generate Registration Credential Options Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Generates options for `navigator.credentials.create()` to initiate passkey registration. Includes Relying Party details, user information, supported key parameters, and exclusion lists. ```haskell registrationCredentialOptions :: (?request :: Request) => Challenge -> Id User -> Text -> [CredentialDescriptor] -> CredentialOptions 'Registration registrationCredentialOptions challenge userId displayName excludeCredentials = CredentialOptionsRegistration { corRp = CredentialRpEntity { creId = Just (RpId rpIdTextFromRequest) , creName = passkeyRelyingPartyName } , corUser = CredentialUserEntity { cueId = userHandleForUserId userId , cueDisplayName = UserAccountDisplayName displayName , cueName = UserAccountName displayName } , corChallenge = challenge , corPubKeyCredParams = [ CredentialParameters CredentialTypePublicKey (CoseSignAlgECDSA CoseHashAlgECDSASHA256) -- ES256 , CredentialParameters CredentialTypePublicKey CoseSignAlgEdDSA -- EdDSA , CredentialParameters CredentialTypePublicKey (CoseSignAlgRSA CoseHashAlgRSASHA256) -- RS256 ] , corTimeout = Just (Timeout 60000) -- 60 seconds , corExcludeCredentials = excludeCredentials , corAuthenticatorSelection = Just AuthenticatorSelectionCriteria { ascAuthenticatorAttachment = Nothing , ascResidentKey = ResidentKeyRequirementPreferred , ascUserVerification = UserVerificationRequirementPreferred } , corAttestation = AttestationConveyancePreferenceNone , corExtensions = Nothing } ``` -------------------------------- ### SQL for deleting all records Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown Example SQL generated by `deleteAll` for removing all records from a table. ```sql DELETE FROM users ``` -------------------------------- ### SQL for updating a record Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown Example SQL generated by `updateRecord` to modify a specific record. ```sql UPDATE users SET firstname = firstname, lastname = "Tester" WHERE id = "cf633b17-c409-4df5-a2fc-8c3b3d6c2ea7" ``` -------------------------------- ### Using isPhoneNumber Validator Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/validation.markdown Example of using the `isPhoneNumber` validator for a phone number text field. ```haskell |> validateField #phoneNumber isPhoneNumber ``` -------------------------------- ### Load and Run Tests in GHCI Source: https://github.com/digitallyinduced/ihp/blob/master/CONTRIBUTING.md Use this command to load the test suite into GHCI and run the main test function. Ensure you have run 'direnv allow' if this is your first time in the IHP directory. ```bash ghci :l Test/Main.hs main ``` -------------------------------- ### SQL for creating many records Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown Example SQL generated by `createMany` for inserting multiple records. ```sql INSERT INTO users (id, firstname, lastname) VALUES (DEFAULT, "", ""), (DEFAULT, "", "") , (DEFAULT, "", "") ... ; ``` -------------------------------- ### Run Migrations on Staging Database (Bash) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database-migrations.markdown This bash command demonstrates how to test migrations on a staging database copy. It first dumps the production database and then restores it to the staging URL, followed by running migrations. ```bash # Create a copy of your production database pg_dump DATABASE_URL_PRODUCTION | psql DATABASE_URL_STAGING # Run migrations against the copy DATABASE_URL=DATABASE_URL_STAGING migrate ``` -------------------------------- ### Get RP ID from Request Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Retrieves the Relying Party Identifier text from the current HTTP request. ```haskell rpIdTextFromRequest :: (?request :: Request) => Text rpIdTextFromRequest = rpIdTextFromHost requestHostText ``` -------------------------------- ### Fetching Paginated Records Source: https://context7.com/digitallyinduced/ihp/llms.txt Example of implementing pagination by limiting the number of results and skipping a certain offset. ```haskell -- Pagination paginatedPosts <- query @Post |> orderByDesc #createdAt |> limit 10 |> offset 20 |> fetch -- SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 20 ``` -------------------------------- ### Example Post Record Instance Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown Illustrates how a 'Post' record might be represented in Haskell, showing a sample UUID and text values for title and body. ```haskell Post { id = cfefdc6c-c097-414c-91c0-cbc9a79fbe4f, title = "Hello World", body = "Some body text" } :: Post ``` -------------------------------- ### Database Table Names (Bad Examples) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown Avoid non-plural, non-snake_case, or uppercase names for database tables. ```haskell post -- Not in plural form, should be `posts` UserProjects -- Not in snake case, should be `user_projects` Posts -- Should be lowercase `posts` company -- Should be `companies` ``` -------------------------------- ### Run Migrations via Nix from GitHub Actions Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database-migrations.markdown For deployments using GitHub Actions, you can execute migrations by running the `migrate` Nix target. ```bash nix run .#migrate ``` -------------------------------- ### SQL for deleting many records Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown Example SQL generated by `deleteRecords` or `deleteRecordByIds` for removing multiple records. ```sql DELETE FROM users WHERE id IN (...) ``` -------------------------------- ### SQL for deleting a single record Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown Example SQL generated by `deleteRecord` or `deleteRecordById` for removing a single record. ```sql DELETE FROM users WHERE id = "cf633b17-c409-4df5-a2fc-8c3b3d6c2ea7" ``` -------------------------------- ### Initial flake.nix Configuration Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/package-management.markdown This is the default configuration for `flake.nix` before adding any native dependencies. It outlines the project's inputs and basic structure. ```nix { { inputs = { ihp.url = "github:digitallyinduced/ihp/1.1"; nixpkgs.follows = "ihp/nixpkgs"; flake-parts.follows = "ihp/flake-parts"; devenv.follows = "ihp/devenv"; systems.follows = "ihp/systems"; }; outputs = inputs@{ ihp, flake-parts, systems, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = import systems; imports = [ ihp.flakeModules.default ]; perSystem = { pkgs, ... }: { ihp = { enable = true; projectPath = ./.; packages = with pkgs; [ # Native dependencies, e.g. imagemagick ]; haskellPackages = p: with p; [ # Haskell dependencies go here p.ihp base wai text ]; }; }; }; } } ``` -------------------------------- ### Hash Password CLI Tool Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/authentication.markdown Demonstrates how to hash a password using the `hash-password` command-line tool. This is useful for manually inserting users into the database. ```bash $ hash-password Enter your password and press enter: hunter2 sha256|17|Y32Ga1uke5CisJvVp6p2sg==|TSDuEs1+Xdaels6TYCkyCgIBHxWA/US7bvBlK0vHzvc= ``` -------------------------------- ### Set GitHub Environment Variables Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/oauth.markdown Sets the OAUTH_GITHUB_CLIENT_ID and OAUTH_GITHUB_CLIENT_SECRET environment variables in the start script for local development. ```bash # ... export OAUTH_GITHUB_CLIENT_ID="MY_GITHUB_CLIENT_ID" # <-- ADD THIS LINE BEFORE THE RunDevServer CALL export OAUTH_GITHUB_CLIENT_SECRET="MY_GITHUB_SECRET" RunDevServer ``` -------------------------------- ### Begin Passkey Authentication Challenge Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown Generates a challenge for passkey authentication, stores it in the session, and renders the necessary options for the client to initiate the authentication process. ```haskell action BeginPasskeyAuthenticationAction = do challenge <- liftIO generateChallenge setSession authenticationChallengeSessionKey (unChallenge challenge) renderJson (WebAuthnJson.wjEncodeCredentialOptionsAuthentication (authenticationCredentialOptions challenge)) ``` -------------------------------- ### Declarative PostgreSQL Database and User Provisioning Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown Automatically create databases and users for your applications using NixOS. This configuration ensures that specified databases and users exist and that users own their corresponding databases. ```nix services.postgresql = { ensureDatabases = [ "myapp" "secondapp" ]; ensureUsers = [ { name = "myapp"; ensureDBOwnership = true; } { name = "secondapp"; ensureDBOwnership = true; } ]; }; ``` -------------------------------- ### Label Results (Indexed Many-to-Many) (Before) Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md Example using the removed `labelResults` function for indexed many-to-many relationships with QueryBuilder. ```haskell -- Before labeledPosts <- query @Post |> innerJoin @Tagging (#id, #postId) |> innerJoinThirdTable @Tag @Tagging (#id, #tagId) |> labelResults @Tag #id |> fetch -- labeledPosts :: [LabeledData (Id' "tags") Post] ``` -------------------------------- ### Required Imports for BlazeHtml Example Source: https://github.com/digitallyinduced/ihp/blob/master/ihp-hsx/README.md These are the necessary imports for using BlazeHtml components and attributes within a Haskell module. ```haskell module Web.View.Posts.Edit where import Web.View.Prelude import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A import Prelude as P ``` -------------------------------- ### Connect to PostgreSQL and Query Table Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown Connect to the local PostgreSQL database using the 'make psql' command. Once connected, you can run SQL queries to inspect the database, such as selecting all rows from the 'posts' table. Type '\q' to quit. ```bash $ make psql psql (9.6.12) Type "help" for help. -- Let's do a query to check that the table is there app=# SELECT * FROM posts; id | title | body ----+-------+------ (0 rows) -- Looks alright! :) -- We can quit the PostgreSQL console by typing \q app=# \q ``` -------------------------------- ### AutoRoute with Multiple Parameters Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/routing.markdown Example of an action constructor with multiple parameters, demonstrating how AutoRoute generates query parameters for each. ```haskell data PostsController = EditPostAction { postId :: !(Id Post), userId :: !(Id User) } GET /EditPost?postId={postId}&userId={userId} => EditPostAction { postId, userId } ``` -------------------------------- ### Database Column Names (Bad Examples) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown Avoid camelCase for database column names; use snake_case instead. ```sql isConfirmed -- Not in snake case, should be `is_confirmed` firstName -- Not in snake case, should be `first_name` ``` -------------------------------- ### Database Table Names (Good Examples) Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown Use lowercase, snake_case, and plural form for database table names. ```haskell users projects companies user_projects company_admins reactions invites comments ``` -------------------------------- ### Build Production Server with Scripts Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/scripts.markdown Command to build all IHP scripts along with the application for production deployment using Nix. ```bash # Build all scripts along with the application nix build .#optimized-prod-server # Or for faster, unoptimized builds nix build .#unoptimized-prod-server ```