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 #-}
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 ---------------------------------------------------------------------
40 import Database.PostgreSQL.Simple (Connection)
41 import System.IO (FilePath)
43 import GHC.Generics (D1, Meta (..), Rep)
44 import GHC.TypeLits (AppendSymbol, Symbol)
47 import Control.Monad.IO.Class (liftIO)
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.Prelude
67 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
69 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth')
70 import Gargantext.API.Node ( Roots , roots
79 import Gargantext.Database.Types.Node ()
80 import Gargantext.API.Count ( CountAPI, count, Query)
81 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
83 --import Gargantext.API.Orchestrator
84 --import Gargantext.API.Orchestrator.Types
86 ---------------------------------------------------------------------
88 import GHC.Base (Applicative)
89 -- import Control.Lens
91 import Data.List (lookup)
92 import Data.Text.Encoding (encodeUtf8)
94 --import Network.Wai (Request, requestHeaders, responseLBS)
95 import Network.Wai (Request, requestHeaders)
96 --import qualified Network.Wai.Handler.Warp as Warp
97 import Network.Wai.Middleware.Cors
99 import Network.Wai.Middleware.RequestLogger
100 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
102 import Network.HTTP.Types hiding (Query)
105 import Gargantext.API.Settings
107 fireWall :: Applicative f => Request -> FireWall -> f Bool
109 let origin = lookup "Origin" (requestHeaders req)
110 let host = lookup "Host" (requestHeaders req)
112 let hostOk = Just (encodeUtf8 "localhost:3000")
113 let originOk = Just (encodeUtf8 "http://localhost:8008")
115 if origin == originOk
117 || (not $ unFireWall fw)
123 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
124 makeMockApp :: MockEnv -> IO Application
126 let serverApp = appMock
128 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
129 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
130 let checkOriginAndHost app req resp = do
131 blocking <- fireWall req (env ^. menv_firewall)
134 False -> resp ( responseLBS status401 []
135 "Invalid Origin or Host header")
137 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
138 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
139 { corsOrigins = Nothing -- == /*
140 , corsMethods = [ methodGet , methodPost , methodPut
141 , methodDelete, methodOptions, methodHead]
142 , corsRequestHeaders = ["authorization", "content-type"]
143 , corsExposedHeaders = Nothing
144 , corsMaxAge = Just ( 60*60*24 ) -- one day
145 , corsVaryOrigin = False
146 , corsRequireOrigin = False
147 , corsIgnoreFailures = False
150 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
151 -- $ Warp.defaultSettings
153 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
154 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
158 makeDevApp :: Env -> IO Application
160 serverApp <- makeApp env
162 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
163 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
164 -- let checkOriginAndHost app req resp = do
165 -- blocking <- fireWall req (env ^. menv_firewall)
167 -- True -> app req resp
168 -- False -> resp ( responseLBS status401 []
169 -- "Invalid Origin or Host header")
171 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
172 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
173 { corsOrigins = Nothing -- == /*
174 , corsMethods = [ methodGet , methodPost , methodPut
175 , methodDelete, methodOptions, methodHead]
176 , corsRequestHeaders = ["authorization", "content-type"]
177 , corsExposedHeaders = Nothing
178 , corsMaxAge = Just ( 60*60*24 ) -- one day
179 , corsVaryOrigin = False
180 , corsRequireOrigin = False
181 , corsIgnoreFailures = False
184 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
185 -- $ Warp.defaultSettings
187 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
188 pure $ logStdoutDev $ corsMiddleware $ serverApp
192 ---------------------------------------------------------------------
195 -- | API for serving @swagger.json@
196 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
198 -- | API for serving main operational routes of @gargantext.org@
201 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
202 -- | TODO :<|> Summary "Latest API" :> GargAPI'
205 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
207 auth :: Connection -> AuthRequest -> Handler AuthResponse
208 auth conn ar = liftIO $ auth' conn ar
212 "auth" :> Summary "AUTH API"
213 :> ReqBody '[JSON] AuthRequest
214 :> Post '[JSON] AuthResponse
217 :<|> "user" :> Summary "First user endpoint"
221 :<|> "node" :> Summary "Node endpoint"
222 :> Capture "id" Int :> NodeAPI HyperdataAny
225 :<|> "corpus":> Summary "Corpus endpoint"
226 :> Capture "id" Int :> NodeAPI HyperdataCorpus
229 :<|> "annuaire":> Summary "Annuaire endpoint"
230 :> Capture "id" Int :> NodeAPI HyperdataAnnuaire
233 :<|> "nodes" :> Summary "Nodes endpoint"
234 :> ReqBody '[JSON] [Int] :> NodesAPI
236 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
238 :<|> "count" :> Summary "Count endpoint"
239 :> ReqBody '[JSON] Query :> CountAPI
242 :<|> "search":> Summary "Search endpoint"
243 :> ReqBody '[JSON] SearchQuery :> SearchAPI
245 :<|> "graph" :> Summary "Graph endpoint"
246 :> Capture "id" Int :> GraphAPI
249 :<|> "tree" :> Summary "Tree endpoint"
250 :> Capture "id" Int :> TreeAPI
253 -- :<|> "scraper" :> WithCallbacks ScraperAPI
259 -- :<|> "list" :> Capture "id" Int :> NodeAPI
260 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
261 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
262 ---------------------------------------------------------------------
263 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
266 type API = SwaggerFrontAPI :<|> GargAPI
267 ---------------------------------------------------------------------
268 -- | Server declaration
269 server :: Env -> IO (Server API)
271 -- orchestrator <- scrapyOrchestrator env
275 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAny)
276 :<|> nodeAPI conn (Proxy :: Proxy HyperdataCorpus)
277 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAnnuaire)
279 :<|> count -- TODO: undefined
281 :<|> graphAPI conn -- TODO: mock
285 conn = env ^. env_conn
287 ---------------------------------------------------------------------
288 swaggerFront :: Server SwaggerFrontAPI
289 swaggerFront = schemaUiServer swaggerDoc
292 gargMock :: Server GargAPI
293 gargMock = mock apiGarg Proxy
295 ---------------------------------------------------------------------
296 makeApp :: Env -> IO Application
297 makeApp = fmap (serve api) . server
299 appMock :: Application
300 appMock = serve api (swaggerFront :<|> gargMock)
302 ---------------------------------------------------------------------
306 apiGarg :: Proxy GargAPI
308 ---------------------------------------------------------------------
310 schemaUiServer :: (Server api ~ Handler Swagger)
311 => Swagger -> Server (SwaggerSchemaUI' dir api)
312 schemaUiServer = swaggerSchemaUIServer
315 -- Type Family for the Documentation
316 type family TypeName (x :: *) :: Symbol where
318 TypeName Text = "Text"
319 TypeName x = GenericTypeName x (Rep x ())
321 type family GenericTypeName t (r :: *) :: Symbol where
322 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
324 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
327 -- | Swagger Specifications
328 swaggerDoc :: Swagger
329 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
330 & info.title .~ "Gargantext"
331 & info.version .~ "4.0.2" -- TODO same version as Gargantext
332 -- & info.base_url ?~ (URL "http://gargantext.org/")
333 & info.description ?~ "REST API specifications"
334 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
335 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
336 ["Gargantext" & description ?~ "Main operations"]
337 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
339 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
341 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
342 swaggerWriteJSON :: IO ()
343 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
345 portRouteInfo :: PortNumber -> IO ()
346 portRouteInfo port = do
347 T.putStrLn " ----Main Routes----- "
348 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
349 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
351 -- | startGargantext takes as parameters port number and Ini file.
352 startGargantext :: PortNumber -> FilePath -> IO ()
353 startGargantext port file = do
354 env <- newEnv port file
356 app <- makeDevApp env
359 startGargantextMock :: PortNumber -> IO ()
360 startGargantextMock port = do
362 application <- makeMockApp . MockEnv $ FireWall False