{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
module Arbiter.Servant.UI
(
AdminUI
, adminUIServer
, adminUIServerDev
, adminApplication
, devAdminApplication
, arbiterAppWithAdmin
, arbiterAppWithAdminDev
) where
import Arbiter.Servant.API (ArbiterAPI)
import Arbiter.Servant.Server (ArbiterServerConfig, BuildServer, arbiterServer)
import Control.Exception (IOException, catch)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as LBS
import Data.FileEmbed (embedDir)
import Data.List (isSuffixOf)
import Data.Text qualified as T
import Network.HTTP.Types (HeaderName, status200, status301, status404)
import Network.Wai (pathInfo, rawPathInfo, responseLBS)
import Servant
import System.FilePath ((</>))
staticFiles :: [(FilePath, ByteString)]
staticFiles :: [([Char], ByteString)]
staticFiles = $(embedDir "static")
type AdminUI = Raw
adminUIServer :: Server AdminUI
adminUIServer :: Server AdminUI
adminUIServer = Application -> Tagged Handler Application
forall {k} (s :: k) b. b -> Tagged s b
Tagged Application
adminApplication
adminApplication :: Application
adminApplication :: Application
adminApplication = ([Char] -> IO (Maybe ByteString)) -> Application
serveStaticApp (([Char] -> IO (Maybe ByteString)) -> Application)
-> ([Char] -> IO (Maybe ByteString)) -> Application
forall a b. (a -> b) -> a -> b
$ \[Char]
fp -> Maybe ByteString -> IO (Maybe ByteString)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([Char] -> [([Char], ByteString)] -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup [Char]
fp [([Char], ByteString)]
staticFiles)
devAdminApplication :: FilePath -> Application
devAdminApplication :: [Char] -> Application
devAdminApplication [Char]
dir = ([Char] -> IO (Maybe ByteString)) -> Application
serveStaticApp (([Char] -> IO (Maybe ByteString)) -> Application)
-> ([Char] -> IO (Maybe ByteString)) -> Application
forall a b. (a -> b) -> a -> b
$ \[Char]
fp ->
(ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just (ByteString -> Maybe ByteString)
-> IO ByteString -> IO (Maybe ByteString)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Char] -> IO ByteString
BS.readFile ([Char]
dir [Char] -> [Char] -> [Char]
</> [Char]
fp)) IO (Maybe ByteString)
-> (IOException -> IO (Maybe ByteString)) -> IO (Maybe ByteString)
forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` (\(IOException
_ :: IOException) -> Maybe ByteString -> IO (Maybe ByteString)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe ByteString
forall a. Maybe a
Nothing)
serveStaticApp :: (FilePath -> IO (Maybe ByteString)) -> Application
serveStaticApp :: ([Char] -> IO (Maybe ByteString)) -> Application
serveStaticApp [Char] -> IO (Maybe ByteString)
resolveFile Request
req Response -> IO ResponseReceived
sendResponse = do
let segments :: [Text]
segments = (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (Text -> Bool) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Bool
T.null) (Request -> [Text]
pathInfo Request
req)
path :: Text
path = Text -> [Text] -> Text
T.intercalate Text
"/" [Text]
segments
if Text -> Bool
T.null Text
path Bool -> Bool -> Bool
&& Bool -> Bool
not (ByteString
"/" ByteString -> ByteString -> Bool
`BS.isSuffixOf` Request -> ByteString
rawPathInfo Request
req)
then
Response -> IO ResponseReceived
sendResponse (Response -> IO ResponseReceived)
-> Response -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$
Status -> ResponseHeaders -> ByteString -> Response
responseLBS Status
status301 [(HeaderName
"Location", Request -> ByteString
rawPathInfo Request
req ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<> ByteString
"/")] ByteString
""
else do
let filePath :: [Char]
filePath =
if Text -> Bool
T.null Text
path Bool -> Bool -> Bool
|| Text
path Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
"index.html"
then [Char]
"index.html"
else Text -> [Char]
T.unpack Text
path
mContent <- [Char] -> IO (Maybe ByteString)
resolveFile [Char]
filePath
case mContent of
Just ByteString
content ->
Response -> IO ResponseReceived
sendResponse (Response -> IO ResponseReceived)
-> Response -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ Status -> ResponseHeaders -> ByteString -> Response
responseLBS Status
status200 (ResponseHeaders
securityHeaders ResponseHeaders -> ResponseHeaders -> ResponseHeaders
forall a. [a] -> [a] -> [a]
++ [[Char] -> (HeaderName, ByteString)
contentTypeHeader [Char]
filePath]) (ByteString -> ByteString
LBS.fromStrict ByteString
content)
Maybe ByteString
Nothing ->
Response -> IO ResponseReceived
sendResponse (Response -> IO ResponseReceived)
-> Response -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ Status -> ResponseHeaders -> ByteString -> Response
responseLBS Status
status404 [(HeaderName
"Content-Type", ByteString
"text/plain")] ByteString
"Not found"
securityHeaders :: [(HeaderName, ByteString)]
=
[ (HeaderName
"X-Content-Type-Options", ByteString
"nosniff")
, (HeaderName
"X-Frame-Options", ByteString
"DENY")
]
contentTypeHeader :: FilePath -> (HeaderName, ByteString)
[Char]
path
| [Char]
".html" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"text/html; charset=utf-8")
| [Char]
".css" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"text/css; charset=utf-8")
| [Char]
".js" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"application/javascript; charset=utf-8")
| [Char]
".json" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"application/json")
| [Char]
".png" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"image/png")
| [Char]
".svg" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"image/svg+xml")
| [Char]
".ico" [Char] -> [Char] -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isSuffixOf` [Char]
path = (HeaderName
"Content-Type", ByteString
"image/x-icon")
| Bool
otherwise = (HeaderName
"Content-Type", ByteString
"application/octet-stream")
adminUIServerDev :: FilePath -> Server AdminUI
adminUIServerDev :: [Char] -> Server AdminUI
adminUIServerDev [Char]
dir = Application -> Tagged Handler Application
forall {k} (s :: k) b. b -> Tagged s b
Tagged ([Char] -> Application
devAdminApplication [Char]
dir)
arbiterAppWithAdmin
:: forall registry
. ( BuildServer registry registry
, HasServer (ArbiterAPI registry) '[]
)
=> ArbiterServerConfig registry
-> Application
arbiterAppWithAdmin :: forall (registry :: [(Symbol, *)]).
(BuildServer registry registry,
HasServer (ArbiterAPI registry) '[]) =>
ArbiterServerConfig registry -> Application
arbiterAppWithAdmin ArbiterServerConfig registry
config =
Proxy (ArbiterAPI registry :<|> AdminUI)
-> Server (ArbiterAPI registry :<|> AdminUI) -> Application
forall {k} (api :: k).
HasServer api '[] =>
Proxy api -> Server api -> Application
serve
(forall t. Proxy t
forall {k} (t :: k). Proxy t
Proxy @(ArbiterAPI registry :<|> AdminUI))
(ArbiterServerConfig registry
-> ServerT (ArbiterAPI registry) Handler
forall (registry :: [(Symbol, *)]).
BuildServer registry registry =>
ArbiterServerConfig registry
-> ServerT (ArbiterAPI registry) Handler
arbiterServer ArbiterServerConfig registry
config ServerT (RegistryToAPI registry) Handler
-> Tagged Handler Application
-> ServerT (RegistryToAPI registry) Handler
:<|> Tagged Handler Application
forall a b. a -> b -> a :<|> b
:<|> Server AdminUI
Tagged Handler Application
adminUIServer)
arbiterAppWithAdminDev
:: forall registry
. ( BuildServer registry registry
, HasServer (ArbiterAPI registry) '[]
)
=> FilePath
-> ArbiterServerConfig registry
-> Application
arbiterAppWithAdminDev :: forall (registry :: [(Symbol, *)]).
(BuildServer registry registry,
HasServer (ArbiterAPI registry) '[]) =>
[Char] -> ArbiterServerConfig registry -> Application
arbiterAppWithAdminDev [Char]
dir ArbiterServerConfig registry
config =
Proxy (ArbiterAPI registry :<|> AdminUI)
-> Server (ArbiterAPI registry :<|> AdminUI) -> Application
forall {k} (api :: k).
HasServer api '[] =>
Proxy api -> Server api -> Application
serve
(forall t. Proxy t
forall {k} (t :: k). Proxy t
Proxy @(ArbiterAPI registry :<|> AdminUI))
(ArbiterServerConfig registry
-> ServerT (ArbiterAPI registry) Handler
forall (registry :: [(Symbol, *)]).
BuildServer registry registry =>
ArbiterServerConfig registry
-> ServerT (ArbiterAPI registry) Handler
arbiterServer ArbiterServerConfig registry
config ServerT (RegistryToAPI registry) Handler
-> Tagged Handler Application
-> ServerT (RegistryToAPI registry) Handler
:<|> Tagged Handler Application
forall a b. a -> b -> a :<|> b
:<|> [Char] -> Server AdminUI
adminUIServerDev [Char]
dir)