2 Module : Gargantext.API
3 Description : Server API
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.
13 Provide config, state, logs and IO
14 type App m a = ( MonadState AppState m
16 , MonadLog (WithSeverity Doc) m
18 Thanks @yannEsposito for this.
21 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
24 {-# LANGUAGE NoImplicitPrelude #-}
25 {-# LANGUAGE DataKinds #-}
26 {-# LANGUAGE DeriveGeneric #-}
27 {-# LANGUAGE FlexibleInstances #-}
28 {-# LANGUAGE OverloadedStrings #-}
29 {-# LANGUAGE TemplateHaskell #-}
30 {-# LANGUAGE TypeOperators #-}
31 {-# LANGUAGE KindSignatures #-}
32 {-# LANGUAGE TypeFamilies #-}
33 {-# LANGUAGE UndecidableInstances #-}
35 ---------------------------------------------------------------------
38 ---------------------------------------------------------------------
39 import Gargantext.Prelude
41 import System.IO (FilePath)
43 import GHC.Generics (D1, Meta (..), Rep)
44 import GHC.TypeLits (AppendSymbol, Symbol)
47 import Data.Aeson.Encode.Pretty (encodePretty)
48 import qualified Data.ByteString.Lazy.Char8 as BL8
50 import Data.Text (Text)
51 import qualified Data.Text.IO as T
52 --import qualified Data.Set as Set
55 import Network.Wai.Handler.Warp hiding (defaultSettings)
58 import Servant.Mock (mock)
59 import Servant.Job.Server (WithCallbacks)
60 import Servant.Swagger
61 import Servant.Swagger.UI
62 -- import Servant.API.Stream
64 --import Gargantext.API.Swagger
65 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
67 import Gargantext.API.Node ( Roots , roots
71 import Gargantext.API.Count ( CountAPI, count, Query)
72 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
73 import Gargantext.API.Orchestrator
74 import Gargantext.API.Orchestrator.Types
76 ---------------------------------------------------------------------
78 import GHC.Base (Applicative)
79 -- import Control.Lens
81 import Data.List (lookup)
82 import Data.Text.Encoding (encodeUtf8)
84 --import Network.Wai (Request, requestHeaders, responseLBS)
85 import Network.Wai (Request, requestHeaders)
86 --import qualified Network.Wai.Handler.Warp as Warp
87 import Network.Wai.Middleware.Cors
89 import Network.Wai.Middleware.RequestLogger
90 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
92 import Network.HTTP.Types hiding (Query)
95 import Gargantext.API.Settings
97 fireWall :: Applicative f => Request -> FireWall -> f Bool
99 let origin = lookup "Origin" (requestHeaders req)
100 let host = lookup "Host" (requestHeaders req)
102 let hostOk = Just (encodeUtf8 "localhost:3000")
103 let originOk = Just (encodeUtf8 "http://localhost:8008")
105 if origin == originOk
107 || (not $ unFireWall fw)
113 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
114 makeMockApp :: MockEnv -> IO Application
116 let serverApp = appMock
118 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
119 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
120 let checkOriginAndHost app req resp = do
121 blocking <- fireWall req (env ^. menv_firewall)
124 False -> resp ( responseLBS status401 []
125 "Invalid Origin or Host header")
127 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
128 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
129 { corsOrigins = Nothing -- == /*
130 , corsMethods = [ methodGet , methodPost , methodPut
131 , methodDelete, methodOptions, methodHead]
132 , corsRequestHeaders = ["authorization", "content-type"]
133 , corsExposedHeaders = Nothing
134 , corsMaxAge = Just ( 60*60*24 ) -- one day
135 , corsVaryOrigin = False
136 , corsRequireOrigin = False
137 , corsIgnoreFailures = False
140 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
141 -- $ Warp.defaultSettings
143 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
144 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
148 makeDevApp :: Env -> IO Application
150 serverApp <- makeApp env
152 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
153 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
154 -- let checkOriginAndHost app req resp = do
155 -- blocking <- fireWall req (env ^. menv_firewall)
157 -- True -> app req resp
158 -- False -> resp ( responseLBS status401 []
159 -- "Invalid Origin or Host header")
161 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
162 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
163 { corsOrigins = Nothing -- == /*
164 , corsMethods = [ methodGet , methodPost , methodPut
165 , methodDelete, methodOptions, methodHead]
166 , corsRequestHeaders = ["authorization", "content-type"]
167 , corsExposedHeaders = Nothing
168 , corsMaxAge = Just ( 60*60*24 ) -- one day
169 , corsVaryOrigin = False
170 , corsRequireOrigin = False
171 , corsIgnoreFailures = False
174 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
175 -- $ Warp.defaultSettings
177 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
178 pure $ logStdoutDev $ corsMiddleware $ serverApp
182 ---------------------------------------------------------------------
185 -- | API for serving @swagger.json@
186 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
188 -- | API for serving main operational routes of @gargantext.org@
189 type GargAPI = "user" :> Summary "First user endpoint"
192 :<|> "node" :> Summary "Node endpoint"
193 :> Capture "id" Int :> NodeAPI
195 :<|> "corpus":> Summary "Corpus endpoint"
196 :> Capture "id" Int :> NodeAPI
198 :<|> "nodes" :> Summary "Nodes endpoint"
199 :> ReqBody '[JSON] [Int] :> NodesAPI
201 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
202 :<|> "count" :> Summary "Count endpoint"
203 :> ReqBody '[JSON] Query :> CountAPI
205 :<|> "search":> Summary "Search endpoint"
206 :> ReqBody '[JSON] SearchQuery :> SearchAPI
208 -- :<|> "scraper" :> WithCallbacks ScraperAPI
214 -- :<|> "list" :> Capture "id" Int :> NodeAPI
215 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
216 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
217 ---------------------------------------------------------------------
218 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
220 type API = SwaggerFrontAPI :<|> GargAPI
222 ---------------------------------------------------------------------
223 -- | Server declaration
224 server :: Env -> IO (Server API)
226 -- orchestrator <- scrapyOrchestrator env
236 conn = env ^. env_conn
238 ---------------------------------------------------------------------
239 swaggerFront :: Server SwaggerFrontAPI
240 swaggerFront = schemaUiServer swaggerDoc
243 gargMock :: Server GargAPI
244 gargMock = mock apiGarg Proxy
246 ---------------------------------------------------------------------
247 makeApp :: Env -> IO Application
248 makeApp = fmap (serve api) . server
250 appMock :: Application
251 appMock = serve api (swaggerFront :<|> gargMock)
253 ---------------------------------------------------------------------
257 apiGarg :: Proxy GargAPI
259 ---------------------------------------------------------------------
261 schemaUiServer :: (Server api ~ Handler Swagger)
262 => Swagger -> Server (SwaggerSchemaUI' dir api)
263 schemaUiServer = swaggerSchemaUIServer
266 -- Type Family for the Documentation
267 type family TypeName (x :: *) :: Symbol where
269 TypeName Text = "Text"
270 TypeName x = GenericTypeName x (Rep x ())
272 type family GenericTypeName t (r :: *) :: Symbol where
273 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
275 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
278 -- | Swagger Specifications
279 swaggerDoc :: Swagger
280 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
281 & info.title .~ "Gargantext"
282 & info.version .~ "0.1.0"
283 -- & info.base_url ?~ (URL "http://gargantext.org/")
284 & info.description ?~ "REST API specifications"
285 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
286 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
287 ["Garg" & description ?~ "Main operations"]
288 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
290 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
292 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
293 swaggerWriteJSON :: IO ()
294 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
296 portRouteInfo :: PortNumber -> IO ()
297 portRouteInfo port = do
298 T.putStrLn " ----Main Routes----- "
299 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
300 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
302 -- | startGargantext takes as parameters port number and Ini file.
303 startGargantext :: PortNumber -> FilePath -> IO ()
304 startGargantext port file = do
305 env <- newEnv port file
307 app <- makeDevApp env
310 startGargantextMock :: PortNumber -> IO ()
311 startGargantextMock port = do
313 application <- makeMockApp . MockEnv $ FireWall False