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 REST API of Gargantext (both Server and Client sides)
12 TODO App type, the main monad in which the bot code is written with.
14 Provide config, state, logs and IO
15 type App m a = ( MonadState AppState m
17 , MonadLog (WithSeverity Doc) m
19 Thanks @yannEsposito for this.
22 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
25 {-# LANGUAGE NoImplicitPrelude #-}
26 {-# LANGUAGE DataKinds #-}
27 {-# LANGUAGE DeriveGeneric #-}
28 {-# LANGUAGE FlexibleInstances #-}
29 {-# LANGUAGE OverloadedStrings #-}
30 {-# LANGUAGE TemplateHaskell #-}
31 {-# LANGUAGE TypeOperators #-}
32 {-# LANGUAGE KindSignatures #-}
33 {-# LANGUAGE TypeFamilies #-}
34 {-# LANGUAGE UndecidableInstances #-}
36 ---------------------------------------------------------------------
39 ---------------------------------------------------------------------
40 import Gargantext.Prelude
42 import System.IO (FilePath)
44 import GHC.Generics (D1, Meta (..), Rep)
45 import GHC.TypeLits (AppendSymbol, Symbol)
48 import Data.Aeson.Encode.Pretty (encodePretty)
49 import qualified Data.ByteString.Lazy.Char8 as BL8
51 import Data.Text (Text)
52 import qualified Data.Text.IO as T
53 --import qualified Data.Set as Set
56 import Network.Wai.Handler.Warp hiding (defaultSettings)
59 import Servant.Mock (mock)
60 --import Servant.Job.Server (WithCallbacks)
61 import Servant.Swagger
62 import Servant.Swagger.UI
63 -- import Servant.API.Stream
65 --import Gargantext.API.Swagger
66 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
68 import Gargantext.API.Node ( Roots , roots
72 import Gargantext.API.Count ( CountAPI, count, Query)
73 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
74 --import Gargantext.API.Orchestrator
75 --import Gargantext.API.Orchestrator.Types
77 ---------------------------------------------------------------------
79 import GHC.Base (Applicative)
80 -- import Control.Lens
82 import Data.List (lookup)
83 import Data.Text.Encoding (encodeUtf8)
85 --import Network.Wai (Request, requestHeaders, responseLBS)
86 import Network.Wai (Request, requestHeaders)
87 --import qualified Network.Wai.Handler.Warp as Warp
88 import Network.Wai.Middleware.Cors
90 import Network.Wai.Middleware.RequestLogger
91 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
93 import Network.HTTP.Types hiding (Query)
96 import Gargantext.API.Settings
98 fireWall :: Applicative f => Request -> FireWall -> f Bool
100 let origin = lookup "Origin" (requestHeaders req)
101 let host = lookup "Host" (requestHeaders req)
103 let hostOk = Just (encodeUtf8 "localhost:3000")
104 let originOk = Just (encodeUtf8 "http://localhost:8008")
106 if origin == originOk
108 || (not $ unFireWall fw)
114 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
115 makeMockApp :: MockEnv -> IO Application
117 let serverApp = appMock
119 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
120 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
121 let checkOriginAndHost app req resp = do
122 blocking <- fireWall req (env ^. menv_firewall)
125 False -> resp ( responseLBS status401 []
126 "Invalid Origin or Host header")
128 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
129 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
130 { corsOrigins = Nothing -- == /*
131 , corsMethods = [ methodGet , methodPost , methodPut
132 , methodDelete, methodOptions, methodHead]
133 , corsRequestHeaders = ["authorization", "content-type"]
134 , corsExposedHeaders = Nothing
135 , corsMaxAge = Just ( 60*60*24 ) -- one day
136 , corsVaryOrigin = False
137 , corsRequireOrigin = False
138 , corsIgnoreFailures = False
141 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
142 -- $ Warp.defaultSettings
144 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
145 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
149 makeDevApp :: Env -> IO Application
151 serverApp <- makeApp env
153 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
154 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
155 -- let checkOriginAndHost app req resp = do
156 -- blocking <- fireWall req (env ^. menv_firewall)
158 -- True -> app req resp
159 -- False -> resp ( responseLBS status401 []
160 -- "Invalid Origin or Host header")
162 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
163 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
164 { corsOrigins = Nothing -- == /*
165 , corsMethods = [ methodGet , methodPost , methodPut
166 , methodDelete, methodOptions, methodHead]
167 , corsRequestHeaders = ["authorization", "content-type"]
168 , corsExposedHeaders = Nothing
169 , corsMaxAge = Just ( 60*60*24 ) -- one day
170 , corsVaryOrigin = False
171 , corsRequireOrigin = False
172 , corsIgnoreFailures = False
175 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
176 -- $ Warp.defaultSettings
178 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
179 pure $ logStdoutDev $ corsMiddleware $ serverApp
183 ---------------------------------------------------------------------
186 -- | API for serving @swagger.json@
187 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
189 -- | API for serving main operational routes of @gargantext.org@
193 "user" :> Summary "First user endpoint"
198 :<|> "node" :> Summary "Node endpoint"
199 :> Capture "id" Int :> NodeAPI
203 :<|> "corpus":> Summary "Corpus endpoint"
204 :> Capture "id" Int :> NodeAPI
207 :<|> "nodes" :> Summary "Nodes endpoint"
208 :> ReqBody '[JSON] [Int] :> NodesAPI
210 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
212 :<|> "count" :> Summary "Count endpoint"
213 :> ReqBody '[JSON] Query :> CountAPI
216 :<|> "search":> Summary "Search endpoint"
217 :> ReqBody '[JSON] SearchQuery :> SearchAPI
219 -- :<|> "scraper" :> WithCallbacks ScraperAPI
225 -- :<|> "list" :> Capture "id" Int :> NodeAPI
226 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
227 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
228 ---------------------------------------------------------------------
229 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
231 type API = SwaggerFrontAPI :<|> GargAPI
233 ---------------------------------------------------------------------
234 -- | Server declaration
235 server :: Env -> IO (Server API)
237 -- orchestrator <- scrapyOrchestrator env
247 conn = env ^. env_conn
249 ---------------------------------------------------------------------
250 swaggerFront :: Server SwaggerFrontAPI
251 swaggerFront = schemaUiServer swaggerDoc
254 gargMock :: Server GargAPI
255 gargMock = mock apiGarg Proxy
257 ---------------------------------------------------------------------
258 makeApp :: Env -> IO Application
259 makeApp = fmap (serve api) . server
261 appMock :: Application
262 appMock = serve api (swaggerFront :<|> gargMock)
264 ---------------------------------------------------------------------
268 apiGarg :: Proxy GargAPI
270 ---------------------------------------------------------------------
272 schemaUiServer :: (Server api ~ Handler Swagger)
273 => Swagger -> Server (SwaggerSchemaUI' dir api)
274 schemaUiServer = swaggerSchemaUIServer
277 -- Type Family for the Documentation
278 type family TypeName (x :: *) :: Symbol where
280 TypeName Text = "Text"
281 TypeName x = GenericTypeName x (Rep x ())
283 type family GenericTypeName t (r :: *) :: Symbol where
284 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
286 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
289 -- | Swagger Specifications
290 swaggerDoc :: Swagger
291 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
292 & info.title .~ "Gargantext"
293 & info.version .~ "0.1.0"
294 -- & info.base_url ?~ (URL "http://gargantext.org/")
295 & info.description ?~ "REST API specifications"
296 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
297 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
298 ["Garg" & description ?~ "Main operations"]
299 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
301 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
303 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
304 swaggerWriteJSON :: IO ()
305 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
307 portRouteInfo :: PortNumber -> IO ()
308 portRouteInfo port = do
309 T.putStrLn " ----Main Routes----- "
310 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
311 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
313 -- | startGargantext takes as parameters port number and Ini file.
314 startGargantext :: PortNumber -> FilePath -> IO ()
315 startGargantext port file = do
316 env <- newEnv port file
318 app <- makeDevApp env
321 startGargantextMock :: PortNumber -> IO ()
322 startGargantextMock port = do
324 application <- makeMockApp . MockEnv $ FireWall False