-
Notifications
You must be signed in to change notification settings - Fork 201
/
Copy pathControllerSupport.hs
292 lines (255 loc) · 12.2 KB
/
ControllerSupport.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, TypeFamilies, ConstrainedClassMethods, ScopedTypeVariables, FunctionalDependencies, AllowAmbiguousTypes #-}
module IHP.ControllerSupport
( Action'
, (|>)
, getRequestBody
, getRequestPath
, getRequestPathAndQuery
, getHeader
, RequestContext (RequestContext)
, request
, requestHeaders
, getFiles
, Controller (..)
, runAction
, createRequestContext
, ControllerContext
, InitControllerContext (..)
, runActionWithNewContext
, newContextForAction
, respondAndExit
, jumpToAction
, requestBodyJSON
, startWebSocketApp
, startWebSocketAppAndFailOnHTTP
, setHeader
, getAppConfig
) where
import ClassyPrelude
import IHP.HaskellSupport
import Network.Wai (Request, ResponseReceived, responseLBS, requestHeaders)
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai
import IHP.ModelSupport
import IHP.ApplicationContext (ApplicationContext (..))
import Network.Wai.Parse as WaiParse
import qualified Data.ByteString.Lazy
import qualified IHP.Controller.RequestContext as RequestContext
import IHP.Controller.RequestContext (RequestContext, Respond)
import qualified Data.CaseInsensitive
import qualified IHP.ErrorController as ErrorController
import qualified Data.Typeable as Typeable
import IHP.FrameworkConfig (FrameworkConfig (..), ConfigProvider(..))
import qualified IHP.Controller.Context as Context
import IHP.Controller.Context (ControllerContext(ControllerContext), customFieldsRef)
import IHP.Controller.Response
import Network.HTTP.Types.Header
import qualified Data.Aeson as Aeson
import qualified Network.Wai.Handler.WebSockets as WebSockets
import qualified Network.WebSockets as WebSockets
import qualified IHP.WebSocket as WebSockets
import qualified Data.TMap as TypeMap
type Action' = IO ResponseReceived
class (Show controller, Eq controller) => Controller controller where
beforeAction :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?theAction :: controller) => IO ()
beforeAction = pure ()
{-# INLINABLE beforeAction #-}
action :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?theAction :: controller) => controller -> IO ()
class InitControllerContext application where
initContext :: (?modelContext :: ModelContext, ?requestContext :: RequestContext, ?applicationContext :: ApplicationContext, ?context :: ControllerContext) => IO ()
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
let ?theAction = controller
let respond = ?context.requestContext.respond
let doRunAction = do
authenticatedModelContext <- prepareRLSIfNeeded ?modelContext
let ?modelContext = authenticatedModelContext
beforeAction
(action controller)
ErrorController.handleNoResponseReturned controller
let handleResponseException (ResponseException response) = respond response
doRunAction `catches` [ Handler handleResponseException, Handler (\exception -> ErrorController.displayException exception controller "")]
{-# INLINE newContextForAction #-}
newContextForAction
:: forall application controller
. ( Controller controller
, ?applicationContext :: ApplicationContext
, ?context :: RequestContext
, InitControllerContext application
, ?application :: application
, Typeable application
, Typeable controller
)
=> controller -> IO (Either (IO ResponseReceived) ControllerContext)
newContextForAction controller = do
let ?modelContext = ?applicationContext.modelContext
let ?requestContext = ?context
controllerContext <- Context.newControllerContext
let ?context = controllerContext
Context.putContext ?application
Context.putContext (Context.ActionType (Typeable.typeOf controller))
try (initContext @application) >>= \case
Left (exception :: SomeException) -> do
pure $ Left $ case fromException exception of
Just (ResponseException response) ->
let respond = ?context.requestContext.respond
in respond response
Nothing -> ErrorController.displayException exception controller " while calling initContext"
Right _ -> pure $ Right ?context
{-# INLINE runActionWithNewContext #-}
runActionWithNewContext :: forall application controller. (Controller controller, ?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, Typeable controller) => controller -> IO ResponseReceived
runActionWithNewContext controller = do
contextOrResponse <- newContextForAction controller
case contextOrResponse of
Left response -> response
Right context -> do
let ?modelContext = ?applicationContext.modelContext
let ?requestContext = ?context
let ?context = context
runAction controller
-- | If 'IHP.LoginSupport.Helper.Controller.enableRowLevelSecurityIfLoggedIn' was called, this will copy the
-- the prepared RowLevelSecurityContext from the controller context into the ModelContext.
--
-- If row leve security wasn't enabled, this will just return the current model context.
prepareRLSIfNeeded :: (?context :: ControllerContext) => ModelContext -> IO ModelContext
prepareRLSIfNeeded modelContext = do
rowLevelSecurityContext <- Context.maybeFromContext
case rowLevelSecurityContext of
Just context -> pure modelContext { rowLevelSecurity = Just context }
Nothing -> pure modelContext
{-# INLINE startWebSocketApp #-}
startWebSocketApp :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => webSocketApp -> IO ResponseReceived -> Network.Wai.Application
startWebSocketApp initialState onHTTP request respond = do
let ?modelContext = ?applicationContext.modelContext
requestContext <- createRequestContext ?applicationContext request respond
let ?requestContext = requestContext
let handleConnection pendingConnection = do
connection <- WebSockets.acceptRequest pendingConnection
controllerContext <- Context.newControllerContext
let ?context = controllerContext
Context.putContext ?application
try (initContext @application) >>= \case
Left (exception :: SomeException) -> putStrLn $ "Unexpected exception in initContext, " <> tshow exception
Right context -> do
WebSockets.startWSApp initialState connection
let connectionOptions = WebSockets.connectionOptions @webSocketApp
request
|> WebSockets.websocketsApp connectionOptions handleConnection
|> \case
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) => webSocketApp -> Network.Wai.Application
startWebSocketAppAndFailOnHTTP initialState = startWebSocketApp @webSocketApp @application initialState (respond $ responseLBS HTTP.status400 [(hContentType, "text/plain")] "This endpoint is only available via a WebSocket")
where
respond = ?context.respond
jumpToAction :: forall action. (Controller action, ?context :: ControllerContext, ?modelContext :: ModelContext) => action -> IO ()
jumpToAction theAction = do
let ?theAction = theAction
beforeAction @action
action theAction
{-# INLINE getRequestBody #-}
getRequestBody :: (?context :: ControllerContext) => IO LByteString
getRequestBody =
case ?context.requestContext.requestBody of
RequestContext.JSONBody { rawPayload } -> pure rawPayload
_ -> Network.Wai.lazyRequestBody request
-- | Returns the request path, e.g. @/Users@ or @/CreateUser@
getRequestPath :: (?context :: ControllerContext) => ByteString
getRequestPath = request.rawPathInfo
{-# INLINABLE getRequestPath #-}
-- | Returns the request path and the query params, e.g. @/ShowUser?userId=9bd6b37b-2e53-40a4-bb7b-fdba67d6af42@
getRequestPathAndQuery :: (?context :: ControllerContext) => ByteString
getRequestPathAndQuery = request.rawPathInfo <> request.rawQueryString
{-# INLINABLE getRequestPathAndQuery #-}
-- | Returns a header value for a given header name. Returns Nothing if not found
--
-- The header is looked up in a case insensitive way.
--
-- >>> getHeader "Content-Type"
-- Just "text/html"
--
-- >>> getHeader "X-My-Custom-Header"
-- Nothing
--
getHeader :: (?context :: ControllerContext) => ByteString -> Maybe ByteString
getHeader name = lookup (Data.CaseInsensitive.mk name) request.requestHeaders
{-# INLINABLE getHeader #-}
-- | Set a header value for a given header name.
--
-- >>> setHeader ("Content-Language", "en")
--
setHeader :: (?context :: ControllerContext) => Header -> IO ()
setHeader header = do
maybeHeaders <- Context.maybeFromContext @[Header]
let headers = fromMaybe [] maybeHeaders
Context.putContext (header : headers)
{-# INLINABLE setHeader #-}
-- | Returns the current HTTP request.
--
-- See https://hackage.haskell.org/package/wai-3.2.2.1/docs/Network-Wai.html#t:Request
request :: (?context :: ControllerContext) => Network.Wai.Request
request = requestContext.request
{-# INLINE request #-}
{-# INLINE getFiles #-}
getFiles :: (?context :: ControllerContext) => [File Data.ByteString.Lazy.ByteString]
getFiles =
case requestContext.requestBody of
RequestContext.FormBody { files } -> files
_ -> []
requestContext :: (?context :: ControllerContext) => RequestContext
requestContext = ?context.requestContext
{-# INLINE requestContext #-}
requestBodyJSON :: (?context :: ControllerContext) => Aeson.Value
requestBodyJSON =
case ?context.requestContext.requestBody of
RequestContext.JSONBody { jsonPayload = Just value } -> value
_ -> error "Expected JSON body"
{-# INLINE createRequestContext #-}
createRequestContext :: ApplicationContext -> Request -> Respond -> IO RequestContext
createRequestContext ApplicationContext { frameworkConfig } request respond = do
let contentType = lookup hContentType (requestHeaders request)
requestBody <- case contentType of
"application/json" -> do
rawPayload <- Network.Wai.lazyRequestBody request
let jsonPayload = Aeson.decode rawPayload
pure RequestContext.JSONBody { jsonPayload, rawPayload }
_ -> do
(params, files) <- WaiParse.parseRequestBodyEx frameworkConfig.parseRequestBodyOptions WaiParse.lbsBackEnd request
pure RequestContext.FormBody { .. }
pure RequestContext.RequestContext { request, respond, requestBody, frameworkConfig }
-- | Returns a custom config parameter
--
-- >>> getAppConfig @StripePublicKey
-- StripePublicKey "pk_test_..."
--
-- Example:
--
-- First you need to define a custom config parameter in Config.hs:
--
-- > -- Config/Config.hs
-- > newtype StripePublicKey = StripePublicKey Text
-- >
-- > config :: ConfigBuilder
-- > config = do
-- > -- ...
-- > stripePublicKey <- StripePublicKey <$> env @Text "STRIPE_PUBLIC_KEY"
-- > option stripePublicKey
--
-- Then you can access it using 'getAppConfig':
--
-- > action MyAction = do
-- > let (StripePublicKey stripePublicKey) = getAppConfig @StripePublicKey
-- >
-- > putStrLn ("Stripe public key: " <> stripePublicKey)
--
getAppConfig :: forall configParameter context. (?context :: context, ConfigProvider context, Typeable configParameter) => configParameter
getAppConfig = ?context.frameworkConfig.appConfig
|> TypeMap.lookup @configParameter
|> fromMaybe (error ("Could not find " <> (show (Typeable.typeRep (Typeable.Proxy @configParameter))) <>" in config"))
{-# INLINE getAppConfig #-}