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
| module Framework
( safetyHeaders
, redirectBack
, getA
, getAuth
, postA
, postAuth
, render
, partialRender
, prepareRequest
, limitHttpMethods
, htmxRedirectMiddleware
, isHtmx
) where
import Types
import Models
import qualified Web.Scotty.Trans as Scotty
import Web.Scotty.Trans (RoutePattern, redirect, request, html)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Network.Wai ( Middleware, mapResponseHeaders, rawPathInfo,
Request (..), responseStatus, responseLBS)
import qualified Data.Text as T
import qualified Lucid.Base as Lucid
import Control.Monad.Reader (Reader, lift, runReader, ask, runReaderT, asks)
import qualified Web
import Cookies
import qualified Repos.Users as Users
import qualified Repos.Stories as Stories
import Utils
import qualified Data.Text.Encoding as T
import Web.Scotty.Internal.Types (ActionT(..))
import Data.Maybe (fromMaybe, isJust)
import qualified Data.Text.Lazy as TL
import Colog (LogAction)
import qualified Data.ByteString as BS
import qualified Data.Aeson as JSON
import Network.HTTP.Types (parseMethod, Status (..), status400)
import Data.Time (diffUTCTime)
import qualified Data.Vault.Lazy as V
import Database.SQLite.Simple (Connection)
import Repos.Utils (CanDB)
type PreActionR a = ActionT App a
type PreAction = PreActionR ()
safetyHeaders :: Middleware
safetyHeaders app req resp = do
app req $ resp . mapResponseHeaders (\h ->
-- ("Content-Security-Policy", "default-src 'self';") :
("X-Content-Type-Options", "nosniff") :
("X-Frame-Options", "deny") :
("X-XSS-Protection", "0") : h)
redirectBack :: Monad m => ActionT m a
redirectBack = do
req <- request
let referer = TL.fromStrict . T.decodeUtf8 <$> requestHeaderReferer req
redirect $ fromMaybe "/" referer
getCSRF :: PreActionR T.Text
getCSRF = do
mCookieToken <- getCookie "csrf_token"
case mCookieToken of
Nothing -> do
-- TODO crypto secure token
setCookie "csrf_token" "abc123"
return "abc123"
Just cookieToken ->
return cookieToken
basicHandler :: Action () -> PreAction
basicHandler handler = do
csrf <- getCSRF
path <- fmap (.rawPathInfo) request
reqInfo <- getReqInfo
let reqId = reqInfo.reqId
let user = reqInfo.user
env <- lift getEnv
let ctx = Ctx reqId csrf (T.decodeUtf8 path)
let envCtx = EnvCtx env ctx user
scottyEnv <- ask
let runSc = runReaderT handler.runAM scottyEnv
liftIO $ runReaderT runSc.runHandler envCtx
basicAuthHandler :: ActionAuth () -> PreAction
basicAuthHandler handler = do
csrf <- getCSRF
path <- fmap (.rawPathInfo) request
reqInfo <- getReqInfo
let reqId = reqInfo.reqId
user <- case reqInfo.user of
Just u -> return u
Nothing -> redirect "/login"
env <- lift getEnv
let ctx = Ctx reqId csrf (T.decodeUtf8 path)
let envCtxAuth = EnvCtxAuth env ctx user
scottyEnv <- ask
let runSc = runReaderT handler.runAM scottyEnv
liftIO $ runReaderT runSc.runHandler envCtxAuth
getReqInfo :: PreActionR ReqInfo
getReqInfo = do
key <- lift $ asks (.reqInfoKey)
v <- fmap (.vault) request
let reqInfo = V.lookup key v
case reqInfo of
Just r -> return r
-- TODO error handling
Nothing -> return $ ReqInfo "" Nothing
getA :: RoutePattern -> Action () -> Server
getA route handler = do
Scotty.get route $ do
basicHandler handler
getAuth :: RoutePattern -> ActionAuth () -> Server
getAuth route handler = do
Scotty.get route $ do
basicAuthHandler handler
postA :: RoutePattern -> Action () -> Server
postA route handler = do
Scotty.post route $ do
basicHandler handler
postAuth :: RoutePattern -> ActionAuth () -> Server
postAuth route handler = do
Scotty.post route $ do
basicAuthHandler handler
render :: (HasUser m, HasCtx m, CanDB m, MonadIO m) =>
Lucid.HtmlT (Reader Web.Context) () -> ActionT m ()
render content = do
csrfToken <- lift $ fmap (.csrf) getCtx
mUser <- lift getUser
lastStories <- lift $ Stories.lastStories 3
let context = Web.Context mUser csrfToken lastStories
let page = runReader (Lucid.renderTextT (Web.root content)) context
html page
partialRender :: (HasUser m, HasCtx m, CanDB m, MonadIO m) =>
Lucid.HtmlT (Reader Web.Context) () -> ActionT m ()
partialRender content = do
csrfToken <- lift $ fmap (.csrf) getCtx
mUser <- lift getUser
lastStories <- lift $ Stories.lastStories 3
let context = Web.Context mUser csrfToken lastStories
let page = runReader (Lucid.renderTextT content) context
html page
limitHttpMethods :: Middleware
limitHttpMethods app req resp = do
let method = parseMethod req.requestMethod
case method of
Left _ -> resp $ responseLBS status400 [] ""
Right _ -> app req resp
isHtmx :: Monad m => ActionT m Bool
isHtmx = isJust <$> Scotty.header "HX-Request"
htmxRedirectMiddleware :: Middleware
htmxRedirectMiddleware app req resp = do
let htmx = isJust $ lookup "HX-Request" $ req.requestHeaders
app req $ \r -> do
if htmx then do resp
. mapResponseHeaders (map (\(k, v) -> do
if k == "Location" then ("HX-redirect", v)
else (k, v)
)) $ r
else do resp r
prepareRequest :: V.Key ReqInfo -> Connection -> LogAction IO BS.ByteString -> Middleware
prepareRequest vaultKey conn logger app req resp = do
start <- now
reqId <- shortId
let method = T.decodeUtf8 req.requestMethod
let headers = req.requestHeaders
let cookies = T.decodeUtf8 <$> lookup "Cookie" headers
let session = getCookieFromHeader cookies "session"
user <- case session of
Just s -> Users.bySession' conn s
Nothing -> return Nothing
let csrfCookie = getCookieFromHeader cookies "csrf_token"
let reqInfo = ReqInfo reqId user
let newVault = V.insert vaultKey reqInfo (vault req)
let newReq = req { vault = newVault }
let path = T.decodeUtf8 req.rawPathInfo
liftIO $ logJson logger
[ ("requestId", JSON.toJSON reqId)
, ("path", JSON.toJSON $ method <> " " <> path)
, ("time", JSON.toJSON start)
, ("user", JSON.toJSON $ (.id) <$> user)
]
app newReq $ \r -> do
let status = responseStatus r
stop <- now
let dt :: Int = round $ 1000 * diffUTCTime stop start
liftIO $ logJson logger
[ ("requestId", JSON.toJSON reqId)
, ("status", JSON.toJSON $ status.statusCode)
, ("dt", JSON.toJSON $ show dt ++ "ms")
, ("time", JSON.toJSON stop)
, ("user", JSON.toJSON $ (.id) <$> user)
]
resp r
|