2 Module : Gargantext.API
3 Description : REST API declaration
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 Main (RESTful) API of the instance Gargantext.
12 The Garg-API is typed to derive the documentation, the mock and tests.
14 This API is indeed typed in order to be able to derive both the server
17 The Garg-API-Monad enables:
20 - Database connection (long term)
21 - In Memory stack management (short term)
24 Thanks to Yann Esposito for our discussions at the start and to Nicolas
25 Pouillard (who mainly made it).
29 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
31 {-# LANGUAGE ConstraintKinds #-}
32 {-# LANGUAGE TemplateHaskell #-}
33 {-# LANGUAGE TypeOperators #-}
34 {-# LANGUAGE KindSignatures #-}
35 {-# LANGUAGE ScopedTypeVariables #-}
36 {-# LANGUAGE TypeFamilies #-}
37 {-# LANGUAGE UndecidableInstances #-}
39 ---------------------------------------------------------------------
42 ---------------------------------------------------------------------
43 import Control.Exception (finally)
45 import Control.Monad.Except (withExceptT)
46 import Control.Monad.Reader (runReaderT)
47 import Data.List (lookup)
48 import Data.Text (Text)
49 import Data.Text.Encoding (encodeUtf8)
51 import Data.Version (showVersion)
52 import GHC.Base (Applicative)
53 import GHC.Generics (D1, Meta (..), Rep, Generic)
54 import GHC.TypeLits (AppendSymbol, Symbol)
55 import Network.HTTP.Types hiding (Query)
57 import Network.Wai.Handler.Warp hiding (defaultSettings)
58 import Network.Wai.Middleware.Cors
59 import Network.Wai.Middleware.RequestLogger
61 import Servant.Auth.Server (AuthResult(..))
62 import Servant.Swagger.UI (swaggerSchemaUIServer)
63 import System.IO (FilePath)
64 import qualified Data.ByteString.Lazy.Char8 as BL8
65 import qualified Data.Text.IO as T
66 import qualified Paths_gargantext as PG -- cabal magic build module
68 import qualified Gargantext.API.Public as Public
70 import Gargantext.Prelude.Config (gc_url_backend_api)
71 import Gargantext.API.Admin.Auth (AuthContext, auth)
72 import Gargantext.API.Admin.FrontEnd (frontEndServer)
73 import Gargantext.API.Admin.Settings (newEnv)
74 import Gargantext.API.Admin.Types (FireWall(..), PortNumber, cookieSettings, env_gargConfig, jwtSettings, settings)
75 import Gargantext.API.Ngrams (saveRepo)
76 import Gargantext.API.Ngrams.Types (HasRepoSaver(..))
77 import Gargantext.API.Prelude
78 import Gargantext.API.Routes
79 import Gargantext.API.Swagger (swaggerDoc)
80 import Gargantext.Prelude
83 data Mode = Dev | Mock | Prod
84 deriving (Show, Read, Generic)
86 -- | startGargantext takes as parameters port number and Ini file.
87 startGargantext :: Mode -> PortNumber -> FilePath -> IO ()
88 startGargantext mode port file = do
89 env <- newEnv port file
92 let baseUrl = env ^. env_gargConfig . gc_url_backend_api
93 app <- makeApp env baseUrl
95 mid <- makeDevMiddleware mode
96 run port (mid app) `finally` stopGargantext env
98 portRouteInfo :: PortNumber -> IO ()
99 portRouteInfo port = do
100 T.putStrLn " ----Main Routes----- "
101 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
102 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
104 -- TODO clean this Monad condition (more generic) ?
105 stopGargantext :: HasRepoSaver env => env -> IO ()
106 stopGargantext env = do
107 T.putStrLn "----- Stopping gargantext -----"
108 runReaderT saveRepo env
111 startGargantextMock :: PortNumber -> IO ()
112 startGargantextMock port = do
114 application <- makeMockApp . MockEnv $ FireWall False
118 ----------------------------------------------------------------------
120 fireWall :: Applicative f => Request -> FireWall -> f Bool
122 let origin = lookup "Origin" (requestHeaders req)
123 let host = lookup "Host" (requestHeaders req)
125 if origin == Just (encodeUtf8 "http://localhost:8008")
126 && host == Just (encodeUtf8 "localhost:3000")
127 || (not $ unFireWall fw)
133 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
134 makeMockApp :: MockEnv -> IO Application
136 let serverApp = appMock
138 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
139 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
140 let checkOriginAndHost app req resp = do
141 blocking <- fireWall req (env ^. menv_firewall)
144 False -> resp ( responseLBS status401 []
145 "Invalid Origin or Host header")
147 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
148 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
149 { corsOrigins = Nothing -- == /*
150 , corsMethods = [ methodGet , methodPost , methodPut
151 , methodDelete, methodOptions, methodHead]
152 , corsRequestHeaders = ["authorization", "content-type"]
153 , corsExposedHeaders = Nothing
154 , corsMaxAge = Just ( 60*60*24 ) -- one day
155 , corsVaryOrigin = False
156 , corsRequireOrigin = False
157 , corsIgnoreFailures = False
160 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
161 -- $ Warp.defaultSettings
163 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
164 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
168 makeDevMiddleware :: Mode -> IO Middleware
169 makeDevMiddleware mode = do
170 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
171 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
172 -- let checkOriginAndHost app req resp = do
173 -- blocking <- fireWall req (env ^. menv_firewall)
175 -- True -> app req resp
176 -- False -> resp ( responseLBS status401 []
177 -- "Invalid Origin or Host header")
179 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
180 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
181 { corsOrigins = Nothing -- == /*
182 , corsMethods = [ methodGet , methodPost , methodPut
183 , methodDelete, methodOptions, methodHead]
184 , corsRequestHeaders = ["authorization", "content-type"]
185 , corsExposedHeaders = Nothing
186 , corsMaxAge = Just ( 60*60*24 ) -- one day
187 , corsVaryOrigin = False
188 , corsRequireOrigin = False
189 , corsIgnoreFailures = False
192 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
193 -- $ Warp.defaultSettings
195 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
197 Prod -> pure $ logStdout . corsMiddleware
198 _ -> pure $ logStdoutDev . corsMiddleware
200 ---------------------------------------------------------------------
202 ---------------------------------------------------------------------
203 -- | Server declarations
204 server :: forall env. EnvC env => env -> Text -> IO (Server API)
205 server env baseUrl = do
206 -- orchestrator <- scrapyOrchestrator env
207 pure $ swaggerSchemaUIServer swaggerDoc
208 :<|> hoistServerWithContext
209 (Proxy :: Proxy GargAPI)
210 (Proxy :: Proxy AuthContext)
212 (serverGargAPI baseUrl)
215 transform :: forall a. GargServerM env GargError a -> Handler a
216 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
220 showAsServantErr :: GargError -> ServerError
221 showAsServantErr (GargServerError err) = err
222 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
224 ---------------------------
226 serverGargAPI :: Text -> GargServerT env err (GargServerM env err) GargAPI
227 serverGargAPI baseUrl -- orchestrator
230 :<|> serverPrivateGargAPI
231 :<|> (Public.api baseUrl)
235 gargVersion :: GargServer GargVersion
236 gargVersion = pure (cs $ showVersion PG.version)
238 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
239 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
240 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
241 -- Here throwAll' requires a concrete type for the monad.
244 -- TODO-SECURITY admin only: withAdmin
245 -- Question: How do we mark admins?
247 serverGargAdminAPI :: GargServer GargAdminAPI
248 serverGargAdminAPI = roots
252 ---------------------------------------------------------------------
253 --gargMock :: Server GargAPI
254 --gargMock = mock apiGarg Proxy
255 ---------------------------------------------------------------------
256 makeApp :: EnvC env => env -> Text -> IO Application
257 makeApp env baseUrl = serveWithContext api cfg <$> server env baseUrl
259 cfg :: Servant.Context AuthContext
260 cfg = env ^. settings . jwtSettings
261 :. env ^. settings . cookieSettings
265 --appMock :: Application
266 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
267 ---------------------------------------------------------------------
271 apiGarg :: Proxy GargAPI
273 ---------------------------------------------------------------------
275 ---------------------------------------------------------------------
276 -- Type Family for the Documentation
277 type family TypeName (x :: *) :: Symbol where
279 TypeName Text = "Text"
280 TypeName x = GenericTypeName x (Rep x ())
282 type family GenericTypeName t (r :: *) :: Symbol where
283 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
285 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))