From 889ebe60bfd8458c6be9207938bdff1b5f11471e Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Sat, 2 Mar 2024 22:11:02 +0100 Subject: [PATCH 01/22] Made request logger less verbose by not logging requests to static files This was the existing behaviour until we switched out the static middleware a while ago --- IHP/ControllerSupport.hs | 14 ++++++----- IHP/ErrorController.hs | 13 ++++------ IHP/IDE/ToolServer.hs | 5 ++-- IHP/RouterSupport.hs | 51 ++++++++++++++++++++-------------------- IHP/Server.hs | 23 +++++------------- 5 files changed, 47 insertions(+), 59 deletions(-) diff --git a/IHP/ControllerSupport.hs b/IHP/ControllerSupport.hs index a5468b0ef..b32988fde 100644 --- a/IHP/ControllerSupport.hs +++ b/IHP/ControllerSupport.hs @@ -66,6 +66,9 @@ class InitControllerContext application where initContext = pure () {-# INLINABLE initContext #-} +instance InitControllerContext () where + initContext = pure () + {-# INLINE runAction #-} runAction :: forall controller. (Controller controller, ?context :: ControllerContext, ?modelContext :: ModelContext, ?applicationContext :: ApplicationContext, ?requestContext :: RequestContext) => controller -> IO ResponseReceived runAction controller = do @@ -149,12 +152,11 @@ prepareRLSIfNeeded modelContext = do Nothing -> pure modelContext {-# INLINE startWebSocketApp #-} -startWebSocketApp :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => IO ResponseReceived -> IO ResponseReceived -startWebSocketApp onHTTP = do +startWebSocketApp :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => IO ResponseReceived -> Network.Wai.Application +startWebSocketApp onHTTP request respond = do let ?modelContext = ?applicationContext.modelContext - let ?requestContext = ?context - let respond = ?context.respond - let request = ?context.request + requestContext <- createRequestContext ?applicationContext request respond + let ?requestContext = requestContext let handleConnection pendingConnection = do connection <- WebSockets.acceptRequest pendingConnection @@ -177,7 +179,7 @@ startWebSocketApp onHTTP = do Just response -> respond response Nothing -> onHTTP {-# INLINE startWebSocketAppAndFailOnHTTP #-} -startWebSocketAppAndFailOnHTTP :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => IO ResponseReceived +startWebSocketAppAndFailOnHTTP :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => Network.Wai.Application startWebSocketAppAndFailOnHTTP = startWebSocketApp @webSocketApp @application (respond $ responseLBS HTTP.status400 [(hContentType, "text/plain")] "This endpoint is only available via a WebSocket") where respond = ?context.respond diff --git a/IHP/ErrorController.hs b/IHP/ErrorController.hs index 29b84d9b8..a468d0bcc 100644 --- a/IHP/ErrorController.hs +++ b/IHP/ErrorController.hs @@ -316,9 +316,10 @@ recordNotFoundExceptionHandlerProd exception controller additionalInfo = in Just (handleNotFound ?context.request ?context.respond) Nothing -> Nothing -handleRouterException :: (?context :: RequestContext) => SomeException -> IO ResponseReceived -handleRouterException exception = - case fromException exception of +handleRouterException :: (?applicationContext :: ApplicationContext) => SomeException -> Application +handleRouterException exception request respond = + let ?context = ?applicationContext + in case fromException exception of Just Router.NoConstructorMatched { expectedType, value, field } -> do let errorMessage = [hsx|

Routing failed with: {tshow exception}

@@ -329,14 +330,12 @@ handleRouterException exception = let title = case value of Just value -> [hsx|Expected {expectedType} for field {field} but got {value}|] Nothing -> [hsx|The action was called without the required {field} parameter|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) Just Router.BadType { expectedType, value = Just value, field } -> do let errorMessage = [hsx|

Routing failed with: {tshow exception}

|] let title = [hsx|Query parameter {field} needs to be a {expectedType} but got {value}|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) _ -> case fromException exception of Just Router.UnexpectedMethodException { allowedMethods = [Router.DELETE], method = Router.GET } -> do @@ -371,7 +370,6 @@ handleRouterException exception =

|] let title = [hsx|Action was called from a GET request, but needs to be called as a DELETE request|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) Just Router.UnexpectedMethodException { allowedMethods = [Router.POST], method = Router.GET } -> do let errorMessage = [hsx| @@ -386,7 +384,6 @@ handleRouterException exception =

|] let title = [hsx|Action was called from a GET request, but needs to be called as a POST request|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) Just Router.UnexpectedMethodException { allowedMethods, method } -> do let errorMessage = [hsx| @@ -397,7 +394,6 @@ handleRouterException exception =

|] let title = [hsx|Action was called with a {method} request, but needs to be called with one of these request methods: {allowedMethods}|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) _ -> do let errorMessage = [hsx| @@ -407,7 +403,6 @@ handleRouterException exception =

Are you trying to do a DELETE action, but your link is missing class="js-delete"?

|] let title = H.text "Routing failed" - let RequestContext { respond } = ?context respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) diff --git a/IHP/IDE/ToolServer.hs b/IHP/IDE/ToolServer.hs index 722d58478..dae1e7dfa 100644 --- a/IHP/IDE/ToolServer.hs +++ b/IHP/IDE/ToolServer.hs @@ -37,6 +37,7 @@ import qualified System.Process as Process import System.Info import qualified IHP.EnvVar as EnvVar import qualified IHP.AutoRefresh.Types as AutoRefresh +import qualified IHP.AutoRefresh as AutoRefresh import IHP.Controller.Context import qualified IHP.IDE.ToolServer.Layout as Layout import IHP.Controller.Layout @@ -85,7 +86,7 @@ startToolServer' port isDebugMode = do let ?applicationContext = applicationContext requestContext <- ControllerSupport.createRequestContext applicationContext request respond let ?context = requestContext - frontControllerToWAIApp toolServerApplication [] (staticApp request respond) + frontControllerToWAIApp @ToolServerApplication @AutoRefresh.AutoRefreshWSApp (\app -> app) toolServerApplication staticApp request respond let openAppUrl = openUrl ("http://localhost:" <> tshow port <> "/") let warpSettings = Warp.defaultSettings @@ -157,4 +158,4 @@ readDatabaseNeedsMigration :: (?context :: ControllerContext) => IO Bool readDatabaseNeedsMigration = do context <- theDevServerContext state <- readIORef (context.appStateRef) - readIORef (state.databaseNeedsMigration) + readIORef (state.databaseNeedsMigration) \ No newline at end of file diff --git a/IHP/RouterSupport.hs b/IHP/RouterSupport.hs index 2e88ea0be..addf6f42d 100644 --- a/IHP/RouterSupport.hs +++ b/IHP/RouterSupport.hs @@ -84,37 +84,38 @@ runAction' :: forall application controller . ( Controller controller , ?applicationContext :: ApplicationContext - , ?context :: RequestContext , InitControllerContext application , ?application :: application , Typeable application , Typeable controller ) - => controller -> (TMap.TMap -> TMap.TMap) -> IO ResponseReceived -runAction' controller contextSetter = do + => controller -> (TMap.TMap -> TMap.TMap) -> Application +runAction' controller contextSetter request respond = do let ?modelContext = ApplicationContext.modelContext ?applicationContext - let ?requestContext = ?context + requestContext <- createRequestContext ?applicationContext request respond + let ?context = requestContext + let ?requestContext = requestContext contextOrErrorResponse <- newContextForAction contextSetter controller case contextOrErrorResponse of Left res -> res Right context -> let ?context = context in runAction controller {-# INLINABLE runAction' #-} -type RouteParseResult = IO (TMap.TMap -> TMap.TMap, (TMap.TMap -> TMap.TMap) -> IO ResponseReceived) +type RouteParseResult = IO (TMap.TMap -> TMap.TMap, (TMap.TMap -> TMap.TMap) -> Application) type RouteParser = Parser (RouteParseResult) -toRouteParser :: Parser (IO ResponseReceived) -> RouteParser +toRouteParser :: Parser Application -> RouteParser toRouteParser parser = do controller <- parser pure $ pure (\t -> t, \_ -> controller) -toRouteParser' :: Parser ((TMap.TMap -> TMap.TMap) -> IO ResponseReceived) -> RouteParser +toRouteParser' :: Parser ((TMap.TMap -> TMap.TMap) -> Application) -> RouteParser toRouteParser' parser = do controller <- parser pure $ pure (\t -> t, controller) -toRouteParseResult :: IO ResponseReceived -> RouteParseResult -toRouteParseResult ioResponseReceived = pure (\t -> t, \_ -> ioResponseReceived) +toRouteParseResult :: Application -> RouteParseResult +toRouteParseResult application = pure (\t -> t, \_ -> application) class FrontController application where controllers @@ -132,8 +133,8 @@ defaultRouter => [RouteParser] -> RouteParser defaultRouter additionalControllers = do let allControllers = controllers <> additionalControllers - ioResponseReceived <- choice $ map (\r -> r <* endOfInput) allControllers - pure ioResponseReceived + applications <- choice $ map (\r -> r <* endOfInput) allControllers + pure applications {-# INLINABLE defaultRouter #-} class HasPath controller where @@ -835,13 +836,20 @@ startPage action = get (ByteString.pack (actionPrefix @action)) action withPrefix prefix routes = string prefix >> choice (map (\r -> r <* endOfInput) routes) {-# INLINABLE withPrefix #-} -runApp :: (?applicationContext :: ApplicationContext, ?context :: RequestContext) => RouteParser -> IO ResponseReceived -> IO ResponseReceived -runApp routes notFoundAction = do - let path = ?context.request.rawPathInfo - handleException :: SomeException -> IO (Either String (IO ResponseReceived)) +frontControllerToWAIApp :: forall app (autoRefreshApp :: Type). (?applicationContext :: ApplicationContext, FrontController app, WSApp autoRefreshApp, Typeable autoRefreshApp, InitControllerContext ()) => Middleware -> app -> Application -> Application +frontControllerToWAIApp middleware application notFoundAction request respond = do + let requestContext = RequestContext { request, respond, requestBody = FormBody { params = [], files = [] }, vault = ?applicationContext.session, frameworkConfig = ?applicationContext.frameworkConfig } + + let ?context = requestContext + + let + path = request.rawPathInfo + handleException :: SomeException -> IO (Either String Application) handleException exception = pure $ Right $ ErrorController.handleRouterException exception - routedAction :: Either String (IO ResponseReceived) <- + routes = let ?application = application in router [let ?application = () in webSocketApp @autoRefreshApp] + + routedAction :: Either String Application <- (do res <- evaluate $ parseOnly (routes <* endOfInput) path case res of @@ -850,17 +858,10 @@ runApp routes notFoundAction = do (tmapSetter, controllerFn) <- io pure $ Right $ controllerFn $ tmapSetter ) - -- pure (undefined::IO ResponseReceived))) `Exception.catch` handleException case routedAction of - Left message -> notFoundAction - Right action -> action -{-# INLINABLE runApp #-} - -frontControllerToWAIApp :: forall app. (?applicationContext :: ApplicationContext, ?context :: RequestContext, FrontController app) => app -> [RouteParser] -> IO ResponseReceived -> IO ResponseReceived -frontControllerToWAIApp application additionalControllers notFoundAction = runApp defaultRouter notFoundAction - where - defaultRouter :: RouteParser = (let ?application = application in router additionalControllers) + Left message -> notFoundAction request respond + Right action -> (middleware action) request respond {-# INLINABLE frontControllerToWAIApp #-} mountFrontController :: forall frontController. (?applicationContext :: ApplicationContext, ?context :: RequestContext, FrontController frontController) => frontController -> RouteParser diff --git a/IHP/Server.hs b/IHP/Server.hs index ed93305a6..ce4e8e316 100644 --- a/IHP/Server.hs +++ b/IHP/Server.hs @@ -65,10 +65,9 @@ run configBuilder = do . runServer frameworkConfig . customMiddleware . corsMiddleware - . sessionMiddleware - . requestLoggerMiddleware . methodOverridePost - $ application staticApp + . sessionMiddleware + $ application staticApp requestLoggerMiddleware {-# INLINABLE run #-} @@ -99,7 +98,7 @@ initStaticApp frameworkConfig = do frameworkStaticDir = libDir <> "/static/" frameworkSettings = (Static.defaultWebAppSettings frameworkStaticDir) - { Static.ss404Handler = Just handleNotFound + { Static.ss404Handler = Just (frameworkConfig.requestLoggerMiddleware handleNotFound) , Static.ssMaxAge = maxAge } appSettings = (Static.defaultWebAppSettings "static/") @@ -127,16 +126,9 @@ initCorsMiddleware FrameworkConfig { corsResourcePolicy } = case corsResourcePol Just corsResourcePolicy -> Cors.cors (const (Just corsResourcePolicy)) Nothing -> id -application :: (FrontController RootApplication, ?applicationContext :: ApplicationContext) => Application -> Application -application staticApp request respond = do - requestContext <- ControllerSupport.createRequestContext ?applicationContext request respond - let ?context = requestContext - let builtinControllers = let ?application = () in - [ webSocketApp @AutoRefresh.AutoRefreshWSApp - , webSocketAppWithCustomPath @AutoRefresh.AutoRefreshWSApp "" -- For b.c. with older versions of ihp-auto-refresh.js - ] - - frontControllerToWAIApp RootApplication builtinControllers (staticApp request respond) +application :: (FrontController RootApplication, ?applicationContext :: ApplicationContext) => Application -> Middleware -> Application +application staticApp middleware request respond = do + frontControllerToWAIApp @RootApplication @AutoRefresh.AutoRefreshWSApp middleware RootApplication staticApp request respond {-# INLINABLE application #-} runServer :: (?applicationContext :: ApplicationContext) => FrameworkConfig -> Application -> IO () @@ -152,9 +144,6 @@ runServer FrameworkConfig { environment = Env.Production, appPort, exceptionTrac |> Warp.setPort appPort |> Warp.setOnException exceptionTracker.onException -instance ControllerSupport.InitControllerContext () where - initContext = pure () - withInitalizers :: FrameworkConfig -> ModelContext -> IO () -> IO () withInitalizers frameworkConfig modelContext continue = do let ?context = frameworkConfig From b1cf07d5130cfc6e806e5a50cfaff0a397f714dc Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Sat, 2 Mar 2024 22:11:14 +0100 Subject: [PATCH 02/22] Fixed demo app in Main.hs not working --- Main.hs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Main.hs b/Main.hs index 0442bc236..0775466b4 100644 --- a/Main.hs +++ b/Main.hs @@ -13,12 +13,19 @@ data DemoController = DemoAction deriving (Eq, Show, Data) instance AutoRoute DemoController instance InitControllerContext RootApplication -instance FrontController RootApplication where +data WebApplication = WebApplication deriving (Eq, Show) + +instance InitControllerContext WebApplication where + initContext = pure () + +instance FrontController WebApplication where controllers = - [ parseRoute @DemoController - , startPage DemoAction + [ startPage DemoAction ] +instance FrontController RootApplication where + controllers = [ mountFrontController WebApplication ] + instance Controller DemoController where action DemoAction = renderPlain "Hello World!" From be721e54f5b278bfc7bd81b1adfe6f0a70543d41 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Sat, 2 Mar 2024 22:11:33 +0100 Subject: [PATCH 03/22] Define IHP_LIB for IHP development --- .envrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.envrc b/.envrc index cffc922b0..a7d0f9482 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ use flake . --impure +export IHP_LIB=lib/IHP/ \ No newline at end of file From c70c578682a6a3c800d637ad24c184d53d3568c2 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Sat, 2 Mar 2024 22:19:12 +0100 Subject: [PATCH 04/22] Fixed tests --- Test/Controller/AccessDeniedSpec.hs | 2 +- Test/Controller/NotFoundSpec.hs | 2 +- Test/RouterSupportSpec.hs | 2 +- Test/SEO/Sitemap.hs | 2 +- Test/ViewSupportSpec.hs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Test/Controller/AccessDeniedSpec.hs b/Test/Controller/AccessDeniedSpec.hs index 8a847540c..34ca74011 100644 --- a/Test/Controller/AccessDeniedSpec.hs +++ b/Test/Controller/AccessDeniedSpec.hs @@ -75,7 +75,7 @@ makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session - pure (sessionMiddleware (Server.application handleNotFound)) + pure (sessionMiddleware $ (Server.application handleNotFound) (\app -> app)) assertAccessDenied :: SResponse -> IO () assertAccessDenied response = do diff --git a/Test/Controller/NotFoundSpec.hs b/Test/Controller/NotFoundSpec.hs index 2c526db25..3448c5cec 100644 --- a/Test/Controller/NotFoundSpec.hs +++ b/Test/Controller/NotFoundSpec.hs @@ -75,7 +75,7 @@ makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session - pure (sessionMiddleware (Server.application handleNotFound)) + pure (sessionMiddleware $ (Server.application handleNotFound) (\app -> app)) assertNotFound :: SResponse -> IO () assertNotFound response = do diff --git a/Test/RouterSupportSpec.hs b/Test/RouterSupportSpec.hs index 09a4c0158..bac750f42 100644 --- a/Test/RouterSupportSpec.hs +++ b/Test/RouterSupportSpec.hs @@ -141,7 +141,7 @@ config = do option (AppPort 8000) application :: (?applicationContext :: ApplicationContext) => Application -application = Server.application handleNotFound +application = Server.application handleNotFound (\app -> app) tests :: Spec tests = beforeAll (mockContextNoDatabase WebApplication config) do diff --git a/Test/SEO/Sitemap.hs b/Test/SEO/Sitemap.hs index 82155b618..ed85c1d1b 100644 --- a/Test/SEO/Sitemap.hs +++ b/Test/SEO/Sitemap.hs @@ -79,5 +79,5 @@ tests = beforeAll (mockContextNoDatabase WebApplication config) do describe "SEO" do describe "Sitemap" do it "should render a XML Sitemap" $ withContext do - runSession (testGet "/sitemap.xml") (Server.application handleNotFound) + runSession (testGet "/sitemap.xml") (Server.application handleNotFound (\app -> app)) >>= assertSuccess "http://localhost:8000/test/ShowPost?postId=00000000-0000-0000-0000-0000000000002105-04-16hourly" diff --git a/Test/ViewSupportSpec.hs b/Test/ViewSupportSpec.hs index 8911230cd..f5fe528d7 100644 --- a/Test/ViewSupportSpec.hs +++ b/Test/ViewSupportSpec.hs @@ -102,7 +102,7 @@ makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session - pure (sessionMiddleware (Server.application handleNotFound)) + pure (sessionMiddleware $ (Server.application handleNotFound (\app -> app))) tests :: Spec tests = beforeAll (mockContextNoDatabase WebApplication config) do From 0a562b229b6eefe318f575d3bb8e632e84ecccba Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Sat, 2 Mar 2024 22:28:09 +0100 Subject: [PATCH 05/22] More AWS documentation (#1913) * more aws bits * Update Guide/deployment.markdown * Update Guide/deployment.markdown Co-authored-by: Amitai Burstein * Update Guide/deployment.markdown Co-authored-by: Amitai Burstein * Update Guide/deployment.markdown * specify least privilege-based better iam policy * s3 doc * logging part * proper initial db guide * gitignore * Update Guide/deployment.markdown * added flake.nix part * db url fix --------- Co-authored-by: Amitai Burstein --- .gitignore | 2 + Guide/deployment.markdown | 142 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ba0123cec..e8d960f41 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ devenv.local.nix # nix build output links result* + +.idea diff --git a/Guide/deployment.markdown b/Guide/deployment.markdown index 643e875b1..aa4e54b94 100644 --- a/Guide/deployment.markdown +++ b/Guide/deployment.markdown @@ -10,19 +10,151 @@ IHP comes with a standard command called `deploy-to-nixos`. This tool is a littl AWS EC2 is a good choice for deploying IHP in a professional setup. -### Creating a new EC2 Instance +### AWS infrastructure preparation + +#### Creating a new EC2 Instance Start a new EC2 instance and use the official NixOS AMI `NixOS-23.05.426.afc48694f2a-x86_64-linux`. You can find the latest NixOS AMI at https://nixos.org/download#nixos-amazon Example steps: - Visit [EC2 creation page](https://eu-west-1.console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstances:) in your desired region. - - Select AMI by name, it will appear under "Community AMIs" after searching by name. - - Select at least a `t3a.small` instance size to have enough RAM for the compilation - - Specify a generous root disk volume. By nature NixOS can consume lots of disk space as you trial-and-error your application deployment. As a minimum, we advise 60 GiB + - Select AMI by name, it will appear under "Community AMIs" after searching by name (there can be a slight delay before the result appears as it searches in all community AMIs). + - Select at least a `t3a.small` instance size to have enough RAM for the compilation. For a real-world application, chances are that you need `t3a.medium` to successfully compile it. + - Specify a generous root disk volume. By nature NixOS can consume lots of disk space as you trial-and-error your application deployment. As a minimum, we advise 60 GiB. - Under `Network settings`, allow SSH traffic from your IP address only, allow HTTPS and HTTP traffic from the internet. Due to the certificate validation for Let's Encrypt, even if your application does not need to have it, allow HTTP too. - Make sure to attach SSH keys to the instance at creation time, that is available locally, so you can SSH to the EC2 instance without password later. + - Either before creating the EC2 instance, you import your existing keypair to [EC2 Key Pairs](https://us-east-1.console.aws.amazon.com/ec2/home?region=eu-west-1#ImportKeyPair:), then you should select it at the EC2 creation page. + - Or let AWS create one on-the-fly: ![image](https://github.com/digitallyinduced/ihp/assets/114076/317b022a-ad6e-43ae-931d-8710db0b711c) . Afterwards, you will be able to download the private key file, later on it is referred as `ihp-app.pem` in this documentation. + +#### (Optional) Creating an RDS Instance + +For production systems, it is advised to use a fully managed PostgreSQL instance, it can be multi-region, fault tolerant, but most of all, +daily backups happen automatically with configurable retention. + +To switch from the local PostgreSQL instance to a managed one (you can do it after or before the initial deployment), you can execute the following steps: + - Visit [RDS creation page](https://us-east-1.console.aws.amazon.com/rds/home?region=eu-west-1#launch-dbinstance:) in your desired region. + - Select PostgreSQL as the Engine Type. + - Select a compatible Engine Version, there are good chances that the very last version will fit. + - At Templates, Choose `Free Tier` for any non-live environments, `Production` for the live environment. + - (Optional) Choose `Auto generate password` for having a secure master password. + - Choose `Connect to an EC2 compute resource` and select your already existing EC2 instance. + - Then you can `Create database`. This process is slow, check back in 10 minutes or so afterward. Note down the auto-generated password. + - Edit your `flake.nix`, under `flake.nixosConfigurations."ihp-app".services.ihp`, you can specify the database URL like: `databaseUrl = lib.mkForce "postgresql://postgres:YOUR-PASSWORD@YOUR-HOSTNAME.amatonaws.com/postgres";`. You can find the proper hostname after the initialization is complete, on the RDS instance detail page. + - `pg_dump --no-owner --no-acl` your existing local database on the EC2 instance directly, and then, you can load it to the newly created instance via `pgsql`. `deploy-to-nixos` won't populate the initial schema at an existing remote database, that's why dumping, `scp d`ing and loading it via `psql` is necessary. + +#### (Optional) Creating an S3 bucket + +If your application needs to store files, on AWS, those should use an S3 bucket for that. + +Infrastructure-side preparation: + - Visit the [S3 creation page](https://s3.console.aws.amazon.com/s3/bucket/create?region=eu-west-1) and create a bucket in the same region..If objects should or should not be public, it's up to the application's business requirements. The S3 [ARN](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) from the S3 details page should be noted down. + - Create an new IAM user for the S3 access. Create an [AWS access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) for that IAM user. + - For that user, attach a policy that allows access to the bucket, for example: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor1", + "Effect": "Allow", + "Action": "s3:*", + "Resource": [ + "YOUR-BUCKET-ARN", + "YOUR-BUCKET-ARN/*" + ] + } + ] +} +``` + - See the [Storage guide](https://ihp.digitallyinduced.com/Guide/file-storage.html#s3) on how to use the access key. + + If your application requires so, make the S3 bucket publicly available. + - Go to https://s3.console.aws.amazon.com/s3/buckets/YOUR-BUCKET?region=eu-west-1&bucketType=general&tab=permissions (permissions tab of the S3 bucket) + - Set `Block all public access˙ to entirely off. + - Set a bucket policy like this: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "YOUR-BUCKET-ARN/*" + } + ] +} +``` + - Test the access by locating a file in a bucket under Objects and "Copy S3 URI" for it. + +#### (Optional) Connecting CloudWatch + +For a production system, logging is essential, so you are informed about anomalies before customer complaints, or you are able to provide an evidence for an incident and so on. + +Mind the region of your EC2 instance for these steps. + +- [Create a CloudWatch log group](https://eu-west-1.console.aws.amazon.com/cloudwatch/home?region=eu-west-1#logsV2:log-groups/create-log-group), note down the ARN. +- Create a log stream inside the previously created log group, for instance `in`. +- Create an IAM user with an access key and secret with the following policy: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Resource": [ + "[YOUR-GROUP-ARN]", + "[YOUR-GROUP-ARN]:*" + ] + } + ] +} +``` +- Configure the `services.vector` part in your `flake.nix` to activate logging: +``` +services.vector = { + enable = true; + journaldAccess = true; + settings = { + sources.journald = { + type = "journald"; + include_units = ["app.service" "nginx.service" "worker.service"]; + }; + transforms.remap_remove_specific_keys = { + type = "remap"; + inputs = ["journald"]; + source = '' + del(._STREAM_ID) + del(._SYSTEMD_UNIT) + del(._BOOT_ID) + del(.source_type) + ''; + }; + sinks.out = { + auth = { + access_key_id = "YOUR-IAM-ACCESS-KEY"; + secret_access_key = "YOUR-IAM-ACCESS-KEY"; + }; + inputs = ["remap_remove_specific_keys"]; + type = "aws_cloudwatch_logs"; + compression = "gzip"; + encoding.codec = "json"; + region = "us-east-1"; + group_name = "tpp-qa"; + stream_name = "in"; + }; + }; +}; +``` +- Review the incoming log entries, adjust remapping accordingly. You might want to remove or transform more entries to make the logs useful for alerts or accountability. -### Connecting to the EC2 Instance +### Connecting to the EC2 / Virtual Machine Instance After you've created the instance, configure your local SSH settings to point to the instance. From cd882069b9ebbd20575545f44a0d006cc2e4bccd Mon Sep 17 00:00:00 2001 From: Montmorency Date: Sat, 2 Mar 2024 21:29:08 +0000 Subject: [PATCH 06/22] Emacs direnv (#1872) * emacs direnv and lsp config. * added link to emacs config for direnv/nix haskell. --- Guide/editors.markdown | 64 ++++++++++++++++++++++++++++++++++ Guide/search/package-lock.json | 58 ++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/Guide/editors.markdown b/Guide/editors.markdown index 26f78b3c4..76de50f3e 100644 --- a/Guide/editors.markdown +++ b/Guide/editors.markdown @@ -149,6 +149,70 @@ and `chmod +x ~/bin/emacs-line`, then export this env var in your shell (e.g. in export IHP_EDITOR="$HOME/bin/emacs-line" ``` +Another useful package set that integrates with lsp/lsp-ui and loads the default nix environment from direnv as well as removing [common flycheck issue](https://github.com/joncol/dotfiles/blob/master/homedir/.emacs.d/init.el). +This config also adds a jump to definition for functions bound to "C-c p": + +```emacs +(use-package direnv + :defer + :custom + (direnv-always-show-summary nil) + :config + (direnv-mode)) +(use-package lsp-mode + :custom + (lsp-lens-enable nil) + (lsp-enable-symbol-highlighting nil) + + :hook + (lsp-mode . lsp-enable-which-key-integration) + + :config + ;; This is to make `lsp-mode' work with `direnv' and pick up the correct + ;; version of GHC. + (advice-add 'lsp :before #'direnv-update-environment) + (setq lsp-modeline-code-actions-enable nil)) + +(use-package lsp-ui + :hook (prog-mode . lsp-ui-mode) + :bind (("C-c p" . lsp-ui-peek-find-definitions)) + :config + (setq lsp-ui-doc-position 'bottom)) + +;; (add-hook 'haskell-mode-hook #'lsp) +(use-package flycheck-haskell + ;; Disabling this package, since it only gives error: + ;; "Reading Haskell configuration failed with exit code Segmentation fault and + ;; output:", when trying to run it in Nix/direnv setup. + :disabled + :hook (haskell-mode . flycheck-haskell-setup)) + +(add-hook 'haskell-mode-hook + (lambda () + (rainbow-mode -1) +;; we aren't evil: +;; (evil-leader/set-key "x h" 'haskell-hoogle) +;; (setq evil-shift-width 2) + (define-key haskell-mode-map (kbd "C-c C-c C-s") + 'haskell-mode-stylish-buffer) + (bind-key (kbd "C-c C-c C-a") 'haskell-sort-imports) + (setq haskell-auto-insert-module-format-string + "module %s\n () where\n\n") + (haskell-auto-insert-module-template) + (smartparens-mode) + (sp-local-pair 'haskell-mode "{" "}") + (setq haskell-hoogle-command nil) + (ligature-mode))) + +(use-package lsp-haskell + :hook ((haskell-mode . lsp-deferred) + (haskell-literate-mode . lsp-deferred)) + :custom + (lsp-haskell-server-path "haskell-language-server")) + +(use-package haskell-mode + :defer) +``` ## Using IHP with Vim / NeoVim diff --git a/Guide/search/package-lock.json b/Guide/search/package-lock.json index c41bd2464..4e84da9b5 100644 --- a/Guide/search/package-lock.json +++ b/Guide/search/package-lock.json @@ -177,6 +177,29 @@ "react-dom": ">= 16.8.0 < 18.0.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "peer": true + }, + "node_modules/@types/react": { + "version": "17.0.71", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.71.tgz", + "integrity": "sha512-lfqOu9mp16nmaGRrS8deS2Taqhd5Ih0o92Te5Ws6I1py4ytHBcXLqh0YIqVsViqwVI5f+haiFM6hju814BzcmA==", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "peer": true + }, "node_modules/algoliasearch": { "version": "4.10.5", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.10.5.tgz", @@ -198,6 +221,12 @@ "@algolia/transporter": "4.10.5" } }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "peer": true + }, "node_modules/esbuild": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", @@ -629,6 +658,29 @@ "algoliasearch": "^4.0.0" } }, + "@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "peer": true + }, + "@types/react": { + "version": "17.0.71", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.71.tgz", + "integrity": "sha512-lfqOu9mp16nmaGRrS8deS2Taqhd5Ih0o92Te5Ws6I1py4ytHBcXLqh0YIqVsViqwVI5f+haiFM6hju814BzcmA==", + "peer": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "peer": true + }, "algoliasearch": { "version": "4.10.5", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.10.5.tgz", @@ -650,6 +702,12 @@ "@algolia/transporter": "4.10.5" } }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "peer": true + }, "esbuild": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", From 13b55672120e07de109449069378590329ba6eda Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Sat, 2 Mar 2024 22:43:06 +0100 Subject: [PATCH 07/22] fixed markdown --- Guide/deployment.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guide/deployment.markdown b/Guide/deployment.markdown index aa4e54b94..6bc69feb2 100644 --- a/Guide/deployment.markdown +++ b/Guide/deployment.markdown @@ -70,7 +70,7 @@ Infrastructure-side preparation: If your application requires so, make the S3 bucket publicly available. - Go to https://s3.console.aws.amazon.com/s3/buckets/YOUR-BUCKET?region=eu-west-1&bucketType=general&tab=permissions (permissions tab of the S3 bucket) - - Set `Block all public access˙ to entirely off. + - Set `Block all public access` to entirely off. - Set a bucket policy like this: ```json { From c526aebc02c29530d7d286de178cd1dc1ba8c3cb Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Thu, 7 Mar 2024 19:30:17 +0100 Subject: [PATCH 08/22] make it easier to spot schema + fixture errors (#1928) --- lib/IHP/Makefile.dist | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/IHP/Makefile.dist b/lib/IHP/Makefile.dist index 6a3ca2890..ec3bd6f14 100644 --- a/lib/IHP/Makefile.dist +++ b/lib/IHP/Makefile.dist @@ -114,10 +114,10 @@ psql: ## Connects to the running postgresql server @psql -h $$PWD/build/db -d app db: Application/Schema.sql Application/Fixtures.sql ## Creates a new database with the current Schema and imports Fixtures.sql - echo "drop schema public cascade; create schema public;" | psql -h $$PWD/build/db -d app - psql -h $$PWD/build/db -d app < "${IHP_LIB}/IHPSchema.sql" - psql -h $$PWD/build/db -d app < Application/Schema.sql - psql -h $$PWD/build/db -d app < Application/Fixtures.sql + (echo "drop schema public cascade; create schema public;" | psql -h $${PWD}/build/db -d app) && \ + psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < "$${IHP_LIB}/IHPSchema.sql" && \ + psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < Application/Schema.sql && \ + psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < Application/Fixtures.sql dumpdb: dump_db ## Saves the current database state into the Fixtures.sql dump_db: ## Saves the current database state into the Fixtures.sql From 779144da3cb3503044a0d2cb4d11437abfe2721c Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Tue, 12 Mar 2024 10:58:18 +0100 Subject: [PATCH 09/22] Only track exception (e.g. with sentry) in production This was the expected behaviour, but turns out I missed one place --- IHP/ErrorController.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/IHP/ErrorController.hs b/IHP/ErrorController.hs index a468d0bcc..3ff0f5a80 100644 --- a/IHP/ErrorController.hs +++ b/IHP/ErrorController.hs @@ -78,11 +78,12 @@ displayException exception action additionalInfo = do -- to the error tracking service (e.g. sentry). Usually this service also writes -- the error message to the stderr output -- - let exceptionTracker = ?applicationContext.frameworkConfig.exceptionTracker.onException - let request = ?requestContext.request + when (?context.frameworkConfig.environment == Environment.Production) do + let exceptionTracker = ?applicationContext.frameworkConfig.exceptionTracker.onException + let request = ?requestContext.request - exceptionTracker (Just request) exception + exceptionTracker (Just request) exception supportingHandlers |> head From da05bbe62df348c86cc0e1d1ab8b31b977c88b78 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Wed, 13 Mar 2024 10:50:25 +0100 Subject: [PATCH 10/22] ihp-openai: Don't use deprecated prompt anymore, as we mostly use the chat API --- ihp-openai/IHP/OpenAI.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ihp-openai/IHP/OpenAI.hs b/ihp-openai/IHP/OpenAI.hs index f5ba2daf1..5652a44db 100644 --- a/ihp-openai/IHP/OpenAI.hs +++ b/ihp-openai/IHP/OpenAI.hs @@ -19,7 +19,7 @@ import Control.Applicative ((<|>)) data CompletionRequest = CompletionRequest { messages :: ![Message] - , prompt :: !Text + , prompt :: !Text -- ^ Deprecated, use 'messages' instead , maxTokens :: !Int , temperature :: !Double , presencePenalty :: !Double @@ -39,7 +39,7 @@ instance ToJSON CompletionRequest where toJSON CompletionRequest { model, prompt, messages, maxTokens, temperature, presencePenalty, frequencePenalty, stream } = object [ "model" .= model - , "messages" .= (messages <> [userMessage prompt]) + , "messages" .= (messages <> (if not (Text.null prompt) then [userMessage prompt] else [])) , "max_tokens" .= maxTokens , "stream" .= stream , "temperature" .= temperature From 042b09113c1163ca7a98e96105111d890ffbb4be Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Wed, 13 Mar 2024 10:51:57 +0100 Subject: [PATCH 11/22] ihp-openai: Added missing assistantMessage --- ihp-openai/IHP/OpenAI.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ihp-openai/IHP/OpenAI.hs b/ihp-openai/IHP/OpenAI.hs index 5652a44db..69f23f94d 100644 --- a/ihp-openai/IHP/OpenAI.hs +++ b/ihp-openai/IHP/OpenAI.hs @@ -62,6 +62,9 @@ userMessage content = Message { role = UserRole, content } systemMessage :: Text -> Message systemMessage content = Message { role = SystemRole, content } +assistantMessage :: Text -> Message +assistantMessage content = Message { role = AssistantRole, content } + newCompletionRequest :: CompletionRequest newCompletionRequest = CompletionRequest { prompt = "" From e73425adfe3dc05c8ffc3dce2a1418286971b929 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 10:00:10 +0100 Subject: [PATCH 12/22] Fixed actions in the IDE not working because request body is consumed twice --- IHP/IDE/ToolServer.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/IHP/IDE/ToolServer.hs b/IHP/IDE/ToolServer.hs index dae1e7dfa..efddb8d21 100644 --- a/IHP/IDE/ToolServer.hs +++ b/IHP/IDE/ToolServer.hs @@ -84,8 +84,6 @@ startToolServer' port isDebugMode = do let toolServerApplication = ToolServerApplication { devServerContext = ?context } let application :: Wai.Application = \request respond -> do let ?applicationContext = applicationContext - requestContext <- ControllerSupport.createRequestContext applicationContext request respond - let ?context = requestContext frontControllerToWAIApp @ToolServerApplication @AutoRefresh.AutoRefreshWSApp (\app -> app) toolServerApplication staticApp request respond let openAppUrl = openUrl ("http://localhost:" <> tshow port <> "/") From ffbb5c6a4fc6fc7b9517b22477efa7f3b4c0257d Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 10:43:27 +0100 Subject: [PATCH 13/22] Simplify session vault key handling Moved the session vault key from the ApplicationContext and RequestContext data structure to a global variable. This is the suggested way by the WAI developers. See https://www.yesodweb.com/blog/2015/10/using-wais-vault --- IHP/ApplicationContext.hs | 2 -- IHP/Controller/RequestContext.hs | 1 - IHP/Controller/Session.hs | 11 +++++++++-- IHP/ControllerSupport.hs | 4 ++-- IHP/RouterSupport.hs | 2 +- IHP/Server.hs | 13 ++++++------- IHP/Test/Mocking.hs | 17 +++++++---------- Test/Controller/AccessDeniedSpec.hs | 2 +- Test/Controller/CookieSpec.hs | 2 +- Test/Controller/NotFoundSpec.hs | 2 +- Test/Controller/ParamSpec.hs | 4 ++-- Test/View/CSSFrameworkSpec.hs | 2 +- Test/View/FormSpec.hs | 2 +- Test/ViewSupportSpec.hs | 2 +- 14 files changed, 33 insertions(+), 33 deletions(-) diff --git a/IHP/ApplicationContext.hs b/IHP/ApplicationContext.hs index a21fcd8dc..9174e71e3 100644 --- a/IHP/ApplicationContext.hs +++ b/IHP/ApplicationContext.hs @@ -2,14 +2,12 @@ module IHP.ApplicationContext where import IHP.Prelude import Network.Wai.Session (Session) -import qualified Data.Vault.Lazy as Vault import IHP.AutoRefresh.Types (AutoRefreshServer) import IHP.FrameworkConfig (FrameworkConfig) import IHP.PGListener (PGListener) data ApplicationContext = ApplicationContext { modelContext :: !ModelContext - , session :: !(Vault.Key (Session IO ByteString ByteString)) , autoRefreshServer :: !(IORef AutoRefreshServer) , frameworkConfig :: !FrameworkConfig , pgListener :: PGListener diff --git a/IHP/Controller/RequestContext.hs b/IHP/Controller/RequestContext.hs index bf6a46ddf..fffc93b66 100644 --- a/IHP/Controller/RequestContext.hs +++ b/IHP/Controller/RequestContext.hs @@ -24,6 +24,5 @@ data RequestContext = RequestContext { request :: Request , respond :: Respond , requestBody :: RequestBody - , vault :: (Vault.Key (Session IO ByteString ByteString)) , frameworkConfig :: FrameworkConfig } diff --git a/IHP/Controller/Session.hs b/IHP/Controller/Session.hs index d0e633c1c..3d5c207e6 100644 --- a/IHP/Controller/Session.hs +++ b/IHP/Controller/Session.hs @@ -24,6 +24,7 @@ module IHP.Controller.Session , getSessionEither , deleteSession , getSessionAndClear + , sessionVaultKey ) where import IHP.Prelude @@ -36,6 +37,8 @@ import qualified Network.Wai as Wai import qualified Data.Serialize as Serialize import Data.Serialize (Serialize) import Data.Serialize.Text () +import qualified Network.Wai.Session +import System.IO.Unsafe (unsafePerformIO) -- | Types of possible errors as a result of -- requesting a value from the session storage @@ -161,5 +164,9 @@ sessionVault = case vaultLookup of Just session -> session Nothing -> error "sessionInsert: The session vault is missing in the request" where - RequestContext { request, vault } = ?context.requestContext - vaultLookup = Vault.lookup vault (Wai.vault request) + RequestContext { request } = ?context.requestContext + vaultLookup = Vault.lookup sessionVaultKey request.vault + +sessionVaultKey :: Vault.Key (Network.Wai.Session.Session IO ByteString ByteString) +sessionVaultKey = unsafePerformIO Vault.newKey +{-# NOINLINE sessionVaultKey #-} \ No newline at end of file diff --git a/IHP/ControllerSupport.hs b/IHP/ControllerSupport.hs index b32988fde..197958203 100644 --- a/IHP/ControllerSupport.hs +++ b/IHP/ControllerSupport.hs @@ -259,7 +259,7 @@ requestBodyJSON = {-# INLINE createRequestContext #-} createRequestContext :: ApplicationContext -> Request -> Respond -> IO RequestContext -createRequestContext ApplicationContext { session, frameworkConfig } request respond = do +createRequestContext ApplicationContext { frameworkConfig } request respond = do let contentType = lookup hContentType (requestHeaders request) requestBody <- case contentType of "application/json" -> do @@ -270,7 +270,7 @@ createRequestContext ApplicationContext { session, frameworkConfig } request res (params, files) <- WaiParse.parseRequestBodyEx frameworkConfig.parseRequestBodyOptions WaiParse.lbsBackEnd request pure RequestContext.FormBody { .. } - pure RequestContext.RequestContext { request, respond, requestBody, vault = session, frameworkConfig } + pure RequestContext.RequestContext { request, respond, requestBody, frameworkConfig } -- | Returns a custom config parameter diff --git a/IHP/RouterSupport.hs b/IHP/RouterSupport.hs index addf6f42d..96da10814 100644 --- a/IHP/RouterSupport.hs +++ b/IHP/RouterSupport.hs @@ -838,7 +838,7 @@ withPrefix prefix routes = string prefix >> choice (map (\r -> r <* endOfInput) frontControllerToWAIApp :: forall app (autoRefreshApp :: Type). (?applicationContext :: ApplicationContext, FrontController app, WSApp autoRefreshApp, Typeable autoRefreshApp, InitControllerContext ()) => Middleware -> app -> Application -> Application frontControllerToWAIApp middleware application notFoundAction request respond = do - let requestContext = RequestContext { request, respond, requestBody = FormBody { params = [], files = [] }, vault = ?applicationContext.session, frameworkConfig = ?applicationContext.frameworkConfig } + let requestContext = RequestContext { request, respond, requestBody = FormBody { params = [], files = [] }, frameworkConfig = ?applicationContext.frameworkConfig } let ?context = requestContext diff --git a/IHP/Server.hs b/IHP/Server.hs index ce4e8e316..0157da068 100644 --- a/IHP/Server.hs +++ b/IHP/Server.hs @@ -8,6 +8,7 @@ import Network.Wai.Middleware.MethodOverridePost (methodOverridePost) import Network.Wai.Session (withSession, Session) import Network.Wai.Session.ClientSession (clientsessionStore) import qualified Web.ClientSession as ClientSession +import IHP.Controller.Session (sessionVaultKey) import qualified Data.Vault.Lazy as Vault import IHP.ApplicationContext import qualified IHP.ControllerSupport as ControllerSupport @@ -48,14 +49,12 @@ run configBuilder = do withInitalizers frameworkConfig modelContext do withPGListener \pgListener -> do - sessionVault <- Vault.newKey - autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) let ?modelContext = modelContext - let ?applicationContext = ApplicationContext { modelContext = ?modelContext, session = sessionVault, autoRefreshServer, frameworkConfig, pgListener } + let ?applicationContext = ApplicationContext { modelContext = ?modelContext, autoRefreshServer, frameworkConfig, pgListener } - sessionMiddleware <- initSessionMiddleware sessionVault frameworkConfig + sessionMiddleware <- initSessionMiddleware frameworkConfig staticApp <- initStaticApp frameworkConfig let corsMiddleware = initCorsMiddleware frameworkConfig let requestLoggerMiddleware = frameworkConfig.requestLoggerMiddleware @@ -108,8 +107,8 @@ initStaticApp frameworkConfig = do pure (Static.staticApp appSettings) -initSessionMiddleware :: Vault.Key (Session IO ByteString ByteString) -> FrameworkConfig -> IO Middleware -initSessionMiddleware sessionVault FrameworkConfig { sessionCookie } = do +initSessionMiddleware :: FrameworkConfig -> IO Middleware +initSessionMiddleware FrameworkConfig { sessionCookie } = do let path = "Config/client_session_key.aes" hasSessionSecretEnvVar <- EnvVar.hasEnvVar "IHP_SESSION_SECRET" @@ -118,7 +117,7 @@ initSessionMiddleware sessionVault FrameworkConfig { sessionCookie } = do if hasSessionSecretEnvVar || not doesConfigDirectoryExist then ClientSession.getKeyEnv "IHP_SESSION_SECRET" else ClientSession.getKey path - let sessionMiddleware :: Middleware = withSession store "SESSION" sessionCookie sessionVault + let sessionMiddleware :: Middleware = withSession store "SESSION" sessionCookie sessionVaultKey pure sessionMiddleware initCorsMiddleware :: FrameworkConfig -> Middleware diff --git a/IHP/Test/Mocking.hs b/IHP/Test/Mocking.hs index 0598b2abd..8582cf66a 100644 --- a/IHP/Test/Mocking.hs +++ b/IHP/Test/Mocking.hs @@ -33,6 +33,7 @@ import qualified Network.Wai.Session import qualified Data.Serialize as Serialize import qualified Control.Exception as Exception import qualified IHP.PGListener as PGListener +import IHP.Controller.Session (sessionVaultKey) type ContextParameters application = (?applicationContext :: ApplicationContext, ?context :: RequestContext, ?modelContext :: ModelContext, ?application :: application, InitControllerContext application, ?mocking :: MockContext application) @@ -58,17 +59,15 @@ withIHPApp application configBuilder hspecAction = do withTestDatabase \testDatabase -> do modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections (testDatabase.url) logger - session <- Vault.newKey pgListener <- PGListener.init modelContext autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) - let sessionVault = Vault.insert session mempty Vault.empty - let applicationContext = ApplicationContext { modelContext = modelContext, session, autoRefreshServer, frameworkConfig, pgListener } + let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty + let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener } let requestContext = RequestContext { request = defaultRequest {vault = sessionVault} , requestBody = FormBody [] [] , respond = const (pure ResponseReceived) - , vault = session , frameworkConfig = frameworkConfig } (hspecAction MockContext { .. }) @@ -81,17 +80,15 @@ mockContextNoDatabase application configBuilder = do logger <- newLogger def { level = Warn } -- don't log queries modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections databaseUrl logger - session <- Vault.newKey - let sessionVault = Vault.insert session mempty Vault.empty + let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty pgListener <- PGListener.init modelContext autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) - let applicationContext = ApplicationContext { modelContext = modelContext, session, autoRefreshServer, frameworkConfig, pgListener } + let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener } let requestContext = RequestContext { request = defaultRequest {vault = sessionVault} , requestBody = FormBody [] [] , respond = \resp -> pure ResponseReceived - , vault = session , frameworkConfig = frameworkConfig } pure MockContext{..} @@ -230,8 +227,8 @@ withUser user callback = insertSession key value = pure () - newVault = Vault.insert vaultKey newSession (Wai.vault request) - RequestContext { request, vault = vaultKey } = ?mocking.requestContext + newVault = Vault.insert sessionVaultKey newSession (Wai.vault request) + RequestContext { request } = ?mocking.requestContext sessionValue = Serialize.encode (user.id) sessionKey = cs (Session.sessionKey @user) diff --git a/Test/Controller/AccessDeniedSpec.hs b/Test/Controller/AccessDeniedSpec.hs index 34ca74011..dba55e502 100644 --- a/Test/Controller/AccessDeniedSpec.hs +++ b/Test/Controller/AccessDeniedSpec.hs @@ -74,7 +74,7 @@ config = do makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ - let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session + let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie sessionVaultKey pure (sessionMiddleware $ (Server.application handleNotFound) (\app -> app)) assertAccessDenied :: SResponse -> IO () diff --git a/Test/Controller/CookieSpec.hs b/Test/Controller/CookieSpec.hs index ca950a556..8750b175e 100644 --- a/Test/Controller/CookieSpec.hs +++ b/Test/Controller/CookieSpec.hs @@ -37,6 +37,6 @@ createControllerContext = do let requestBody = FormBody { params = [], files = [] } request = Wai.defaultRequest - requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = error "frameworkConfig" } + requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = error "frameworkConfig" } let ?requestContext = requestContext newControllerContext diff --git a/Test/Controller/NotFoundSpec.hs b/Test/Controller/NotFoundSpec.hs index 3448c5cec..b79ca3cd3 100644 --- a/Test/Controller/NotFoundSpec.hs +++ b/Test/Controller/NotFoundSpec.hs @@ -74,7 +74,7 @@ config = do makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ - let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session + let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie sessionVaultKey pure (sessionMiddleware $ (Server.application handleNotFound) (\app -> app)) assertNotFound :: SResponse -> IO () diff --git a/Test/Controller/ParamSpec.hs b/Test/Controller/ParamSpec.hs index 630ad13c0..d45ed7c00 100644 --- a/Test/Controller/ParamSpec.hs +++ b/Test/Controller/ParamSpec.hs @@ -434,14 +434,14 @@ createControllerContextWithParams params = let requestBody = FormBody { params, files = [] } request = Wai.defaultRequest - requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = error "frameworkConfig" } + requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = error "frameworkConfig" } in FrozenControllerContext { requestContext, customFields = TypeMap.empty } createControllerContextWithJson params = let requestBody = JSONBody { jsonPayload = Just (json params), rawPayload = cs params } request = Wai.defaultRequest - requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = error "frameworkConfig" } + requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = error "frameworkConfig" } in FrozenControllerContext { requestContext, customFields = TypeMap.empty } json :: Text -> Aeson.Value diff --git a/Test/View/CSSFrameworkSpec.hs b/Test/View/CSSFrameworkSpec.hs index 11f1a2c24..945defe00 100644 --- a/Test/View/CSSFrameworkSpec.hs +++ b/Test/View/CSSFrameworkSpec.hs @@ -721,5 +721,5 @@ createControllerContextWithCSSFramework cssFramework = do option cssFramework let requestBody = FormBody { params = [], files = [] } let request = Wai.defaultRequest - let requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = frameworkConfig } + let requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = frameworkConfig } pure FrozenControllerContext { requestContext, customFields = TypeMap.empty } \ No newline at end of file diff --git a/Test/View/FormSpec.hs b/Test/View/FormSpec.hs index 7cc5d27bb..75532c411 100644 --- a/Test/View/FormSpec.hs +++ b/Test/View/FormSpec.hs @@ -49,7 +49,7 @@ createControllerContext = do frameworkConfig <- FrameworkConfig.buildFrameworkConfig (pure ()) let requestBody = FormBody { params = [], files = [] } let request = Wai.defaultRequest - let requestContext = RequestContext { request, respond = undefined, requestBody, vault = undefined, frameworkConfig = frameworkConfig } + let requestContext = RequestContext { request, respond = undefined, requestBody, frameworkConfig = frameworkConfig } pure FrozenControllerContext { requestContext, customFields = mempty } data Project' = Project {id :: (Id' "projects"), title :: Text, meta :: MetaBag} deriving (Eq, Show) diff --git a/Test/ViewSupportSpec.hs b/Test/ViewSupportSpec.hs index f5fe528d7..191bda6e8 100644 --- a/Test/ViewSupportSpec.hs +++ b/Test/ViewSupportSpec.hs @@ -101,7 +101,7 @@ config = do makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ - let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session + let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie sessionVaultKey pure (sessionMiddleware $ (Server.application handleNotFound (\app -> app))) tests :: Spec From e87c0a3225350fabaa8aafe78d5447a22ec4d761 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 10:52:16 +0100 Subject: [PATCH 14/22] removed unused importsd --- IHP/ApplicationContext.hs | 1 - IHP/Controller/RequestContext.hs | 2 -- IHP/Controller/Response.hs | 5 ++--- IHP/ControllerSupport.hs | 3 +-- IHP/EnvVar.hs | 1 - IHP/IDE/FileWatcher.hs | 1 - IHP/IDE/ToolServer.hs | 8 +++----- IHP/Server.hs | 7 ++----- 8 files changed, 8 insertions(+), 20 deletions(-) diff --git a/IHP/ApplicationContext.hs b/IHP/ApplicationContext.hs index 9174e71e3..97c16dc04 100644 --- a/IHP/ApplicationContext.hs +++ b/IHP/ApplicationContext.hs @@ -1,7 +1,6 @@ module IHP.ApplicationContext where import IHP.Prelude -import Network.Wai.Session (Session) import IHP.AutoRefresh.Types (AutoRefreshServer) import IHP.FrameworkConfig (FrameworkConfig) import IHP.PGListener (PGListener) diff --git a/IHP/Controller/RequestContext.hs b/IHP/Controller/RequestContext.hs index fffc93b66..5bac8c508 100644 --- a/IHP/Controller/RequestContext.hs +++ b/IHP/Controller/RequestContext.hs @@ -8,8 +8,6 @@ import ClassyPrelude import qualified Data.ByteString.Lazy as LBS import Network.Wai (Request, Response, ResponseReceived) import Network.Wai.Parse (File, Param) -import qualified Data.Vault.Lazy as Vault -import Network.Wai.Session (Session) import IHP.FrameworkConfig import qualified Data.Aeson as Aeson diff --git a/IHP/Controller/Response.hs b/IHP/Controller/Response.hs index 5a79af936..4dddf3f70 100644 --- a/IHP/Controller/Response.hs +++ b/IHP/Controller/Response.hs @@ -9,12 +9,11 @@ where import ClassyPrelude import Network.HTTP.Types.Header import qualified IHP.Controller.Context as Context -import IHP.Controller.Context (ControllerContext(ControllerContext)) import qualified Network.Wai import Network.Wai (Response) import qualified Control.Exception as Exception -respondAndExit :: (?context::ControllerContext) => Response -> IO () +respondAndExit :: (?context :: Context.ControllerContext) => Response -> IO () respondAndExit response = do responseWithHeaders <- addResponseHeadersFromContext response Exception.throwIO (ResponseException responseWithHeaders) @@ -35,7 +34,7 @@ addResponseHeaders headers = Network.Wai.mapResponseHeaders (\hs -> headers <> h -- > addResponseHeadersFromContext response -- You probabaly want `setHeader` -- -addResponseHeadersFromContext :: (?context :: ControllerContext) => Response -> IO Response +addResponseHeadersFromContext :: (?context :: Context.ControllerContext) => Response -> IO Response addResponseHeadersFromContext response = do maybeHeaders <- Context.maybeFromContext @[Header] let headers = fromMaybe [] maybeHeaders diff --git a/IHP/ControllerSupport.hs b/IHP/ControllerSupport.hs index 197958203..900d66695 100644 --- a/IHP/ControllerSupport.hs +++ b/IHP/ControllerSupport.hs @@ -29,7 +29,7 @@ module IHP.ControllerSupport import ClassyPrelude import IHP.HaskellSupport -import Network.Wai (Response, Request, ResponseReceived, responseLBS, requestHeaders) +import Network.Wai (Request, ResponseReceived, responseLBS, requestHeaders) import qualified Network.HTTP.Types as HTTP import qualified Network.Wai import IHP.ModelSupport @@ -39,7 +39,6 @@ import qualified Data.ByteString.Lazy import qualified IHP.Controller.RequestContext as RequestContext import IHP.Controller.RequestContext (RequestContext, Respond) import qualified Data.CaseInsensitive -import qualified Control.Exception as Exception import qualified IHP.ErrorController as ErrorController import qualified Data.Typeable as Typeable import IHP.FrameworkConfig (FrameworkConfig (..), ConfigProvider(..)) diff --git a/IHP/EnvVar.hs b/IHP/EnvVar.hs index a2e164bb8..0ae52926f 100644 --- a/IHP/EnvVar.hs +++ b/IHP/EnvVar.hs @@ -11,7 +11,6 @@ import IHP.Prelude import Data.String.Interpolate.IsString (i) import qualified System.Posix.Env.ByteString as Posix import Network.Socket (PortNumber) -import Data.Word (Word16) import IHP.Mail.Types import IHP.Environment diff --git a/IHP/IDE/FileWatcher.hs b/IHP/IDE/FileWatcher.hs index 7713e50d1..d380eb9d2 100644 --- a/IHP/IDE/FileWatcher.hs +++ b/IHP/IDE/FileWatcher.hs @@ -9,7 +9,6 @@ import System.Directory (listDirectory, doesDirectoryExist) import qualified Data.Map as Map import qualified System.FSNotify as FS import IHP.IDE.Types -import qualified Data.Time.Clock as Clock import qualified Data.List as List import IHP.IDE.LiveReloadNotificationServer (notifyAssetChange) import qualified Control.Debounce as Debounce diff --git a/IHP/IDE/ToolServer.hs b/IHP/IDE/ToolServer.hs index efddb8d21..2f7d09747 100644 --- a/IHP/IDE/ToolServer.hs +++ b/IHP/IDE/ToolServer.hs @@ -6,13 +6,11 @@ import qualified Network.Wai.Handler.Warp as Warp import IHP.IDE.Types import IHP.IDE.PortConfig import qualified IHP.ControllerSupport as ControllerSupport -import qualified IHP.ErrorController as ErrorController import IHP.ApplicationContext import IHP.ModelSupport import IHP.RouterSupport hiding (get) import Network.Wai.Session.ClientSession (clientsessionStore) import qualified Web.ClientSession as ClientSession -import qualified Data.Vault.Lazy as Vault import Network.Wai.Middleware.MethodOverridePost (methodOverridePost) import Network.Wai.Session (withSession) import qualified Network.WebSockets as Websocket @@ -49,6 +47,7 @@ import qualified IHP.PGListener as PGListener import qualified Network.Wai.Application.Static as Static import qualified WaiAppStatic.Types as Static import IHP.Controller.NotFound (handleNotFound) +import IHP.Controller.Session (sessionVaultKey) withToolServer :: (?context :: Context) => IO () -> IO () withToolServer inner = withAsyncBound async (\_ -> inner) @@ -72,15 +71,14 @@ startToolServer' port isDebugMode = do Just baseUrl -> Config.option $ Config.BaseUrl baseUrl Nothing -> pure () - session <- Vault.newKey store <- fmap clientsessionStore (ClientSession.getKey "Config/client_session_key.aes") - let sessionMiddleware :: Wai.Middleware = withSession store "SESSION" (frameworkConfig.sessionCookie) session + let sessionMiddleware :: Wai.Middleware = withSession store "SESSION" (frameworkConfig.sessionCookie) sessionVaultKey let modelContext = notConnectedModelContext undefined pgListener <- PGListener.init modelContext autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) staticApp <- initStaticApp - let applicationContext = ApplicationContext { modelContext, session, autoRefreshServer, frameworkConfig, pgListener } + let applicationContext = ApplicationContext { modelContext, autoRefreshServer, frameworkConfig, pgListener } let toolServerApplication = ToolServerApplication { devServerContext = ?context } let application :: Wai.Application = \request respond -> do let ?applicationContext = applicationContext diff --git a/IHP/Server.hs b/IHP/Server.hs index 0157da068..3899c4a5a 100644 --- a/IHP/Server.hs +++ b/IHP/Server.hs @@ -5,19 +5,16 @@ import IHP.Prelude import qualified Network.Wai.Handler.Warp as Warp import Network.Wai import Network.Wai.Middleware.MethodOverridePost (methodOverridePost) -import Network.Wai.Session (withSession, Session) +import Network.Wai.Session (withSession) import Network.Wai.Session.ClientSession (clientsessionStore) import qualified Web.ClientSession as ClientSession import IHP.Controller.Session (sessionVaultKey) -import qualified Data.Vault.Lazy as Vault import IHP.ApplicationContext -import qualified IHP.ControllerSupport as ControllerSupport import qualified IHP.Environment as Env import qualified IHP.PGListener as PGListener import IHP.FrameworkConfig -import IHP.RouterSupport (frontControllerToWAIApp, FrontController, webSocketApp, webSocketAppWithCustomPath) -import IHP.ErrorController +import IHP.RouterSupport (frontControllerToWAIApp, FrontController) import qualified IHP.AutoRefresh as AutoRefresh import qualified IHP.AutoRefresh.Types as AutoRefresh import IHP.LibDir From 40d98d0ec198bdd812886725b932df825036ecbe Mon Sep 17 00:00:00 2001 From: Rodrigo Mesquita Date: Fri, 15 Mar 2024 16:39:31 +0000 Subject: [PATCH 15/22] Fix conditional GHC 9.6 compilation The CPP macro __GLASGOW_HASKELL__ was using an incorrect integer numbering scheme for GHC versions, making the conditional compilation incorrect (960 means GHC 9.60, whereas 906 means GHC 9.6). See https://downloads.haskell.org/ghc/latest/docs/users_guide/intro.html#version-numbering --- ihp-hsx/IHP/HSX/HaskellParser.hs | 2 +- ihp-hsx/IHP/HSX/HsExpToTH.hs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ihp-hsx/IHP/HSX/HaskellParser.hs b/ihp-hsx/IHP/HSX/HaskellParser.hs index 1895d8fa7..08f93e69b 100644 --- a/ihp-hsx/IHP/HSX/HaskellParser.hs +++ b/ihp-hsx/IHP/HSX/HaskellParser.hs @@ -30,7 +30,7 @@ parseHaskellExpression sourcePos extensions input = error = renderWithContext defaultSDocContext $ vcat $ map (formatBulleted defaultSDocContext) -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 $ map (diagnosticMessage NoDiagnosticOpts) #else $ map diagnosticMessage diff --git a/ihp-hsx/IHP/HSX/HsExpToTH.hs b/ihp-hsx/IHP/HSX/HsExpToTH.hs index d7a33e6d0..35ed01fff 100644 --- a/ihp-hsx/IHP/HSX/HsExpToTH.hs +++ b/ihp-hsx/IHP/HSX/HsExpToTH.hs @@ -28,7 +28,7 @@ import qualified GHC.Unit.Module as Module import GHC.Stack import qualified Data.List.NonEmpty as NonEmpty import Language.Haskell.Syntax.Type -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 import Language.Haskell.Syntax.Basic #endif @@ -92,7 +92,7 @@ toExp (Expr.HsVar _ n) = then TH.ConE (toName n') else TH.VarE (toName n') -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsUnboundVar _ n) = TH.UnboundVarE (TH.mkName . occNameString $ occName n) #else toExp (Expr.HsUnboundVar _ n) = TH.UnboundVarE (TH.mkName . occNameString $ n) @@ -110,7 +110,7 @@ toExp (Expr.HsOverLit _ OverLit {ol_val}) toExp (Expr.HsApp _ e1 e2) = TH.AppE (toExp . unLoc $ e1) (toExp . unLoc $ e2) -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsAppType _ e _ HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body) #else toExp (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body) @@ -124,7 +124,7 @@ toExp (Expr.NegApp _ e _) = TH.AppE (TH.VarE 'negate) (toExp . unLoc $ e) -- NOTE: for lambda, there is only one match -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])))) #else toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])) _)) @@ -199,7 +199,7 @@ toExp (Expr.HsProjection _ locatedFields) = extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..." in -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 TH.ProjectionE (NonEmpty.map (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc) locatedFields) #else TH.ProjectionE (NonEmpty.map (unpackFS . unLoc . extractFieldLabel . unLoc) locatedFields) @@ -210,13 +210,13 @@ toExp (Expr.HsGetField _ expr locatedField) = extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..." in -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 TH.GetFieldE (toExp (unLoc expr)) (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc $ locatedField) #else TH.GetFieldE (toExp (unLoc expr)) (unpackFS . unLoc . extractFieldLabel . unLoc $ locatedField) #endif -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsOverLabel _ _ fastString) = TH.LabelE (unpackFS fastString) #else toExp (Expr.HsOverLabel _ fastString) = TH.LabelE (unpackFS fastString) From 2947dfe65dc668011c082efb0fe2512c7cf28f6d Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 20:27:02 +0100 Subject: [PATCH 16/22] Use ghc96 everywhere --- NixSupport/default.nix | 2 +- NixSupport/make-nixpkgs-from-options.nix | 2 +- devenv-module.nix | 2 +- flake-module.nix | 2 +- ihp-hsx/shell.nix | 2 +- ihp-openai/shell.nix | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NixSupport/default.nix b/NixSupport/default.nix index 5a0fa1975..50a8bc71d 100644 --- a/NixSupport/default.nix +++ b/NixSupport/default.nix @@ -1,4 +1,4 @@ -{ compiler ? "ghc94" +{ compiler ? "ghc96" , additionalNixpkgsOptions ? {} , pkgs ? import "${toString projectPath}/Config/nix/nixpkgs-config.nix" { ihp = ihp; additionalNixpkgsOptions = additionalNixpkgsOptions; } , ghc ? pkgs.haskell.packages.${compiler} diff --git a/NixSupport/make-nixpkgs-from-options.nix b/NixSupport/make-nixpkgs-from-options.nix index ff6551f27..5c3517906 100644 --- a/NixSupport/make-nixpkgs-from-options.nix +++ b/NixSupport/make-nixpkgs-from-options.nix @@ -8,7 +8,7 @@ , dontHaddockPackages ? [] , nixPkgsRev ? "277bf961c323b6cde46932cc9308135d0687af95" , nixPkgsSha256 ? "sha256-ciSZqliNLDattmjR/1yQ0mJHZSkXWZlnIR6TcdDGFMQ" -, compiler ? "ghc94" +, compiler ? "ghc96" , manualOverrides ? haskellPackagesNew: haskellPackagesOld: { } # More exotic overrides go here , additionalNixpkgsOptions ? {} }: diff --git a/devenv-module.nix b/devenv-module.nix index 0086532e2..020e75013 100644 --- a/devenv-module.nix +++ b/devenv-module.nix @@ -8,7 +8,7 @@ that is defined in flake-module.nix perSystem = { nix-filter, pkgs, lib, ... }: let ghcCompiler = import ./NixSupport/mkGhcCompiler.nix { inherit pkgs; - ghcCompiler = pkgs.haskell.packages.ghc94; + ghcCompiler = pkgs.haskell.packages.ghc96; ihp = ./.; filter = inputs.nix-filter.lib; }; diff --git a/flake-module.nix b/flake-module.nix index 338894378..507f62bc8 100644 --- a/flake-module.nix +++ b/flake-module.nix @@ -26,7 +26,7 @@ ihpFlake: description = '' The GHC compiler to use for IHP. ''; - default = pkgs.haskell.packages.ghc94; + default = pkgs.haskell.packages.ghc96; }; packages = lib.mkOption { diff --git a/ihp-hsx/shell.nix b/ihp-hsx/shell.nix index f74a66058..cb3f12e2a 100644 --- a/ihp-hsx/shell.nix +++ b/ihp-hsx/shell.nix @@ -3,7 +3,7 @@ let ihp = ./../.; haskellPackagesDir = ./../NixSupport/haskell-packages; }; - ghc = pkgs.haskell.packages.ghc94; + ghc = pkgs.haskell.packages.ghc96; haskellDeps = ghc.ghcWithPackages (p: with p; [ classy-prelude string-conversions diff --git a/ihp-openai/shell.nix b/ihp-openai/shell.nix index 926699d11..197cf9b06 100644 --- a/ihp-openai/shell.nix +++ b/ihp-openai/shell.nix @@ -3,7 +3,7 @@ let ihp = ./../.; haskellPackagesDir = ./../NixSupport/haskell-packages; }; - ghc = pkgs.haskell.packages.ghc94; + ghc = pkgs.haskell.packages.ghc96; haskellDeps = ghc.ghcWithPackages (p: with p; [ text bytestring From 1cd5170b8ccec284cd96145c4cd9d98bde1f97e8 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 20:33:16 +0100 Subject: [PATCH 17/22] Updated nixpkgs --- flake.lock | 8 ++++---- flake.nix | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 5a1756a44..2e7092209 100644 --- a/flake.lock +++ b/flake.lock @@ -488,17 +488,17 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1696291921, - "narHash": "sha256-isKgVAoUxuxYEuO3Q4xhbfKcZrF/+UkJtOTv0eb/W5E=", + "lastModified": 1710461556, + "narHash": "sha256-uxBpjpYSVLCkNxOLHDuli2twd6mmuvuTuU77aT/eiGY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ea0284a3da391822909be5e98a60c1e62572a7dc", + "rev": "cb97fed5722648882d1b4a03df0b67681909fb68", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "ea0284a3da391822909be5e98a60c1e62572a7dc", + "rev": "cb97fed5722648882d1b4a03df0b67681909fb68", "type": "github" } }, diff --git a/flake.nix b/flake.nix index bed7dd964..88e4c7138 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { # TODO use nixpkgs-unstable and just .lock a version? - nixpkgs.url = "github:NixOS/nixpkgs?rev=ea0284a3da391822909be5e98a60c1e62572a7dc"; + nixpkgs.url = "github:NixOS/nixpkgs?rev=cb97fed5722648882d1b4a03df0b67681909fb68"; # pre-defined set of default target systems systems.url = "github:nix-systems/default"; From d59d471eda04fd1e82d169799ce4c7343dafb517 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 20:47:30 +0100 Subject: [PATCH 18/22] Added forks for broken packages --- NixSupport/haskell-packages/minio-hs.nix | 41 +++++++++++++++++++++++ NixSupport/haskell-packages/smtp-mail.nix | 22 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 NixSupport/haskell-packages/minio-hs.nix create mode 100644 NixSupport/haskell-packages/smtp-mail.nix diff --git a/NixSupport/haskell-packages/minio-hs.nix b/NixSupport/haskell-packages/minio-hs.nix new file mode 100644 index 000000000..d2322f623 --- /dev/null +++ b/NixSupport/haskell-packages/minio-hs.nix @@ -0,0 +1,41 @@ +{ mkDerivation, aeson, base, base64-bytestring, binary, bytestring +, case-insensitive, conduit, conduit-extra, crypton-connection +, cryptonite, cryptonite-conduit, digest, directory, filepath +, http-client, http-client-tls, http-conduit, http-types, ini, lib +, memory, network-uri, QuickCheck, raw-strings-qq, relude +, resourcet, retry, tasty, tasty-hunit, tasty-quickcheck +, tasty-smallcheck, text, time, time-units, transformers, unliftio +, unliftio-core, unordered-containers, xml-conduit, fetchFromGitHub +}: +mkDerivation { + pname = "minio-hs"; + version = "1.7.0"; + src = fetchFromGitHub { + owner = "mpscholten"; + repo = "minio-hs"; + rev = "786cf1881f0b62b7539e63547e76afc3c1ade36a"; + sha256 = "sha256-dK4nH6j16oFNB27tp1ExAWahhFUlKpzLpqTiHju8dl8="; + }; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson base base64-bytestring binary bytestring case-insensitive + conduit conduit-extra crypton-connection cryptonite + cryptonite-conduit digest directory filepath http-client + http-client-tls http-conduit http-types ini memory network-uri + relude resourcet retry text time time-units transformers unliftio + unliftio-core unordered-containers xml-conduit + ]; + testHaskellDepends = [ + aeson base base64-bytestring binary bytestring case-insensitive + conduit conduit-extra crypton-connection cryptonite + cryptonite-conduit digest directory filepath http-client + http-client-tls http-conduit http-types ini memory network-uri + QuickCheck raw-strings-qq relude resourcet retry tasty tasty-hunit + tasty-quickcheck tasty-smallcheck text time time-units transformers + unliftio unliftio-core unordered-containers xml-conduit + ]; + homepage = "https://github.com/minio/minio-hs#readme"; + description = "A MinIO Haskell Library for Amazon S3 compatible cloud storage"; + license = lib.licenses.asl20; +} \ No newline at end of file diff --git a/NixSupport/haskell-packages/smtp-mail.nix b/NixSupport/haskell-packages/smtp-mail.nix new file mode 100644 index 000000000..56c141158 --- /dev/null +++ b/NixSupport/haskell-packages/smtp-mail.nix @@ -0,0 +1,22 @@ +{ mkDerivation, array, base, base16-bytestring, base64-bytestring +, bytestring, crypton-connection, cryptonite, filepath, lib, memory +, mime-mail, network, network-bsd, text, fetchFromGitHub +}: +mkDerivation { + pname = "smtp-mail"; + version = "0.3.0.0"; + src = fetchFromGitHub { + owner = "MasterWordServices"; + repo = "smtp-mail"; + rev = "4c724c80814ab1da7c37256a6c10e04c88b9af95"; + sha256 = "sha256-gjg2k8UHBwumoH8OUsFZ+Xd5icotaVHRmlQt6Xc7Mf0="; + }; + libraryHaskellDepends = [ + array base base16-bytestring base64-bytestring bytestring + crypton-connection cryptonite filepath memory mime-mail network network-bsd + text + ]; + homepage = "http://github.com/jhickner/smtp-mail"; + description = "Simple email sending via SMTP"; + license = lib.licenses.bsd3; +} \ No newline at end of file From 5f289b778f873cb51d638cc6b11e47e12290fb02 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Fri, 15 Mar 2024 20:57:48 +0100 Subject: [PATCH 19/22] Allow overriding the logger in the controller context (#1923) * Allow overriding the logger in the controller context * added more comments --- IHP/Controller/Context.hs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/IHP/Controller/Context.hs b/IHP/Controller/Context.hs index 61a8cd3bc..0ddb744cd 100644 --- a/IHP/Controller/Context.hs +++ b/IHP/Controller/Context.hs @@ -10,6 +10,7 @@ import qualified Data.Typeable as Typeable import IHP.Controller.RequestContext import IHP.FrameworkConfig import IHP.Log.Types +import System.IO.Unsafe (unsafePerformIO) -- | A container storing useful data along the request lifecycle, such as the request, the current user, set current view layout, flash messages, ... -- @@ -137,5 +138,28 @@ instance HasField "frameworkConfig" ControllerContext FrameworkConfig where getField controllerContext = controllerContext.requestContext.frameworkConfig {-# INLINABLE getField #-} +-- The following hack is bad, but allows us to override the logger using 'putContext' +-- The alternative would be https://github.com/digitallyinduced/ihp/pull/1921 which is also not very nice +-- +-- This can be useful to customize the log formatter for all actions of an app: +-- +-- > import IHP.Log.Types +-- > import IHP.Controller.Context +-- > +-- > instance InitControllerContext WebApplication where +-- > initContext = do +-- > let defaultLogger :: Logger = ?context.frameworkConfig.logger +-- > let withUserIdLogger :: Logger = { Log.formatter = userIdFormatter defaultLogger.formatter } +-- > putContext withUserIdLogger +-- > +-- > userIdFormatter :: (?context :: Context) => Log.LogFormatter -> Log.LogFormatter +-- > userIdFormatter existingFormatter time level string = +-- > existingFormatter time level (prependUserId string) +-- > +-- > preprendUserId :: (?context :: Context) => Text -> Text +-- > preprendUserId string = "userId: " <> show currentUserId <> " " <> string +-- +-- This design mistake should be fixed in IHP v2 instance HasField "logger" ControllerContext Logger where - getField controllerContext = controllerContext.frameworkConfig.logger + getField context@(FrozenControllerContext { customFields }) = fromMaybe context.frameworkConfig.logger (TypeMap.lookup @Logger customFields) + getField context = (unsafePerformIO (freeze context)).logger -- Hacky, but there's no better way. The only way to retrieve the logger here, is by reading from the IORef in an unsafe way From fa08a0c2caddc0f82c6c4a0a1ecb00ce9373572c Mon Sep 17 00:00:00 2001 From: Amitai Burstein Date: Sat, 16 Mar 2024 17:23:29 +0200 Subject: [PATCH 20/22] Add docs about Unique Constraints --- Guide/database.markdown | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Guide/database.markdown b/Guide/database.markdown index 954c42328..4408b7e4b 100644 --- a/Guide/database.markdown +++ b/Guide/database.markdown @@ -765,14 +765,6 @@ Similarly as for renaming, deleting a column currently won't work automatically 1. Delete your column in the Schema Designer 2. Delete the column from the database by executing `ALTER TABLE tablename DROP COLUMN colname` -### Alternate Method - -There's always more than one way. This is another. - -1. Make changes in the Schema Designer -2. Click `Save DB to Fixtures` in the Schema Designer (Use the arrow next to the `Update DB` button to see this option) -3. Edit `Fixtures.sql` to your heart's content. -4. Click `Push to DB` in the Schema Designer (Use the arrow next to the `Update DB` button to see this option) ### Migrations In Production @@ -848,3 +840,16 @@ action CreateUserAction = do redirectTo NewSessionAction ``` + +## Unique Constraints + +It's possible to use the UI to set the unique constraint on a column. However, sometimes you might want to add a unique constraint on multiple columns. This can be done by adding a unique constraint to the `Schema.sql` file. For example, to add a unique constraint on the `email` and `username` columns of the `users` table, you would add the following to the `Schema.sql` file: + +```sql +CREATE TABLE users ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + username TEXT NOT NULL, + UNIQUE (email, username) +); +``` \ No newline at end of file From cc0ae34a6ddcbacac0dbe3e13a6c84358d387ab6 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Sun, 17 Mar 2024 12:10:49 +0100 Subject: [PATCH 21/22] Docs: Add reference to the migration guide --- Guide/database.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Guide/database.markdown b/Guide/database.markdown index 4408b7e4b..ed1c145c6 100644 --- a/Guide/database.markdown +++ b/Guide/database.markdown @@ -768,7 +768,7 @@ Similarly as for renaming, deleting a column currently won't work automatically ### Migrations In Production -IHP currently has no built-in migration system yet. We're still experimenting with a great way to solve this. Until then, the recommended approach used by digitally induced is to manually migrate your database using DDL statements as shown above. +See the [Migrations guide](https://ihp.digitallyinduced.com/Guide/database-migrations.html). ## Supported Database Types @@ -852,4 +852,4 @@ CREATE TABLE users ( username TEXT NOT NULL, UNIQUE (email, username) ); -``` \ No newline at end of file +``` From 5658c05304b69aad1884d8a63352511cc9ff81eb Mon Sep 17 00:00:00 2001 From: Amitai Burstein Date: Mon, 18 Mar 2024 09:49:15 +0200 Subject: [PATCH 22/22] Docs about Passing JSON to the View (#1934) * Docs about Passing JSON to the View fixes #1900 * indent code * Move toJSON code into the View --- Guide/view.markdown | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Guide/view.markdown b/Guide/view.markdown index 6a6f4419d..4e326c972 100644 --- a/Guide/view.markdown +++ b/Guide/view.markdown @@ -181,6 +181,58 @@ Now the `company` variable can be used to read the current user's company across Use [`theRequest`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewSupport.html#v:theRequest) to access the current [WAI request](https://hackage.haskell.org/package/wai-3.2.2.1/docs/Network-Wai.html). +### Passing JSON to the View + +You might need to pass JSON values to the view, so later you could have a JS script to read it. You should use Aeson's `toJSON` function to convert your data to JSON and then pass it to the view. Let's say you have a `Posts` controller and `Post`s records that you want to print in the console. + +```haskell +-- Web/Controller/Posts.hs +instance Controller PostsController where + action PostsAction = do + posts <- query @Post |> fetch + + render IndexView { .. } + +``` + +Then in the view, you can access the JSON data like this: + +```haskell +-- Web/View/Posts/Index.hs + +module Web.View.Posts.Index where +import Web.View.Prelude + +-- Add Aeson import. +import Data.Aeson (encode) + +data IndexView = IndexView { posts :: [Post] } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+ Open the developer's console to see the posts JSON data. +
+ {- Pass the encoded JSON to the JS script -} + + |] + where + postsToJson :: [Post] -> Value + postsToJson posts = + posts + |> fmap (\post -> object + [ "id" .= post.id + , "title" .= post.title + , "body" .= post.body + ]) + |> toJSON +``` + +No you can go to `/Posts`, create a few posts, and see their JSON in the browser's developer console. + ### Highlighting the current active link Use [`isActiveAction`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewSupport.html#v:isActiveAction) to check whether the current request URL matches a given action: