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.HTML.Blaze (HTML)
60 import Servant.Mock (mock)
61 --import Servant.Job.Server (WithCallbacks)
62 import Servant.Static.TH.Internal.Server (fileTreeToServer)
63 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
64 import Servant.Swagger
65 import Servant.Swagger.UI
66 -- import Servant.API.Stream
67 import Text.Blaze.Html (Html)
69 --import Gargantext.API.Swagger
70 import Gargantext.Prelude
71 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
73 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth')
74 import Gargantext.API.Node ( Roots , roots
83 import Gargantext.Database.Types.Node ()
84 import Gargantext.API.Count ( CountAPI, count, Query)
85 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
87 --import Gargantext.API.Orchestrator
88 --import Gargantext.API.Orchestrator.Types
90 ---------------------------------------------------------------------
92 import GHC.Base (Applicative)
93 -- import Control.Lens
95 import Data.List (lookup)
96 import Data.Text.Encoding (encodeUtf8)
98 --import Network.Wai (Request, requestHeaders, responseLBS)
99 import Network.Wai (Request, requestHeaders)
100 --import qualified Network.Wai.Handler.Warp as Warp
101 import Network.Wai.Middleware.Cors
103 import Network.Wai.Middleware.RequestLogger
104 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
106 import Network.HTTP.Types hiding (Query)
109 import Gargantext.API.Settings
111 fireWall :: Applicative f => Request -> FireWall -> f Bool
113 let origin = lookup "Origin" (requestHeaders req)
114 let host = lookup "Host" (requestHeaders req)
116 let hostOk = Just (encodeUtf8 "localhost:3000")
117 let originOk = Just (encodeUtf8 "http://localhost:8008")
119 if origin == originOk
121 || (not $ unFireWall fw)
127 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
128 makeMockApp :: MockEnv -> IO Application
130 let serverApp = appMock
132 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
133 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
134 let checkOriginAndHost app req resp = do
135 blocking <- fireWall req (env ^. menv_firewall)
138 False -> resp ( responseLBS status401 []
139 "Invalid Origin or Host header")
141 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
142 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
143 { corsOrigins = Nothing -- == /*
144 , corsMethods = [ methodGet , methodPost , methodPut
145 , methodDelete, methodOptions, methodHead]
146 , corsRequestHeaders = ["authorization", "content-type"]
147 , corsExposedHeaders = Nothing
148 , corsMaxAge = Just ( 60*60*24 ) -- one day
149 , corsVaryOrigin = False
150 , corsRequireOrigin = False
151 , corsIgnoreFailures = False
154 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
155 -- $ Warp.defaultSettings
157 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
158 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
162 makeDevApp :: Env -> IO Application
164 serverApp <- makeApp env
166 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
167 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
168 -- let checkOriginAndHost app req resp = do
169 -- blocking <- fireWall req (env ^. menv_firewall)
171 -- True -> app req resp
172 -- False -> resp ( responseLBS status401 []
173 -- "Invalid Origin or Host header")
175 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
176 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
177 { corsOrigins = Nothing -- == /*
178 , corsMethods = [ methodGet , methodPost , methodPut
179 , methodDelete, methodOptions, methodHead]
180 , corsRequestHeaders = ["authorization", "content-type"]
181 , corsExposedHeaders = Nothing
182 , corsMaxAge = Just ( 60*60*24 ) -- one day
183 , corsVaryOrigin = False
184 , corsRequireOrigin = False
185 , corsIgnoreFailures = False
188 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
189 -- $ Warp.defaultSettings
191 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
192 pure $ logStdoutDev $ corsMiddleware $ serverApp
194 ---------------------------------------------------------------------
197 -- | API for serving @swagger.json@
198 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
200 -- | API for serving main operational routes of @gargantext.org@
203 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
204 -- | TODO :<|> Summary "Latest API" :> GargAPI'
207 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
209 auth :: Connection -> AuthRequest -> Handler AuthResponse
210 auth conn ar = liftIO $ auth' conn ar
214 "auth" :> Summary "AUTH API"
215 :> ReqBody '[JSON] AuthRequest
216 :> Post '[JSON] AuthResponse
219 :<|> "user" :> Summary "First user endpoint"
223 :<|> "node" :> Summary "Node endpoint"
224 :> Capture "id" Int :> NodeAPI HyperdataAny
227 :<|> "corpus":> Summary "Corpus endpoint"
228 :> Capture "id" Int :> NodeAPI HyperdataCorpus
231 :<|> "annuaire":> Summary "Annuaire endpoint"
232 :> Capture "id" Int :> NodeAPI HyperdataAnnuaire
235 :<|> "nodes" :> Summary "Nodes endpoint"
236 :> ReqBody '[JSON] [Int] :> NodesAPI
238 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
240 :<|> "count" :> Summary "Count endpoint"
241 :> ReqBody '[JSON] Query :> CountAPI
244 :<|> "search":> Summary "Search endpoint"
245 :> ReqBody '[JSON] SearchQuery :> SearchAPI
247 :<|> "graph" :> Summary "Graph endpoint"
248 :> Capture "id" Int :> GraphAPI
251 :<|> "tree" :> Summary "Tree endpoint"
252 :> Capture "id" Int :> TreeAPI
255 -- :<|> "scraper" :> WithCallbacks ScraperAPI
261 -- :<|> "list" :> Capture "id" Int :> NodeAPI
262 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
263 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
264 ---------------------------------------------------------------------
265 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
267 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
269 ---------------------------------------------------------------------
270 -- | Server declarations
272 server :: Env -> IO (Server API)
274 gargAPI <- serverGargAPI env
279 serverGargAPI :: Env -> IO (Server GargAPI)
280 serverGargAPI env = do
281 -- orchestrator <- scrapyOrchestrator env
284 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAny)
285 :<|> nodeAPI conn (Proxy :: Proxy HyperdataCorpus)
286 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAnnuaire)
288 :<|> count -- TODO: undefined
290 :<|> graphAPI conn -- TODO: mock
294 conn = env ^. env_conn
296 serverIndex :: Server (Get '[HTML] Html)
297 serverIndex = $(do (Just s) <- liftIO (fileTypeToFileTree (FileTypeFile "purescript-gargantext/dist/index.html"))
300 ---------------------------------------------------------------------
301 swaggerFront :: Server SwaggerFrontAPI
302 swaggerFront = schemaUiServer swaggerDoc
305 gargMock :: Server GargAPI
306 gargMock = mock apiGarg Proxy
308 ---------------------------------------------------------------------
309 makeApp :: Env -> IO Application
310 makeApp = fmap (serve api) . server
312 appMock :: Application
313 appMock = serve api (swaggerFront :<|> gargMock :<|> serverIndex)
315 ---------------------------------------------------------------------
319 apiGarg :: Proxy GargAPI
321 ---------------------------------------------------------------------
323 schemaUiServer :: (Server api ~ Handler Swagger)
324 => Swagger -> Server (SwaggerSchemaUI' dir api)
325 schemaUiServer = swaggerSchemaUIServer
328 -- Type Family for the Documentation
329 type family TypeName (x :: *) :: Symbol where
331 TypeName Text = "Text"
332 TypeName x = GenericTypeName x (Rep x ())
334 type family GenericTypeName t (r :: *) :: Symbol where
335 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
337 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
340 -- | Swagger Specifications
341 swaggerDoc :: Swagger
342 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
343 & info.title .~ "Gargantext"
344 & info.version .~ "4.0.2" -- TODO same version as Gargantext
345 -- & info.base_url ?~ (URL "http://gargantext.org/")
346 & info.description ?~ "REST API specifications"
347 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
348 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
349 ["Gargantext" & description ?~ "Main operations"]
350 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
352 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
354 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
355 swaggerWriteJSON :: IO ()
356 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
358 portRouteInfo :: PortNumber -> IO ()
359 portRouteInfo port = do
360 T.putStrLn " ----Main Routes----- "
361 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
362 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
364 -- | startGargantext takes as parameters port number and Ini file.
365 startGargantext :: PortNumber -> FilePath -> IO ()
366 startGargantext port file = do
367 env <- newEnv port file
369 app <- makeDevApp env
372 startGargantextMock :: PortNumber -> IO ()
373 startGargantextMock port = do
375 application <- makeMockApp . MockEnv $ FireWall False