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.Node.Contact (HyperdataContact)
84 import Gargantext.Database.Types.Node ()
85 import Gargantext.API.Count ( CountAPI, count, Query)
86 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
88 --import Gargantext.API.Orchestrator
89 --import Gargantext.API.Orchestrator.Types
91 ---------------------------------------------------------------------
93 import GHC.Base (Applicative)
94 -- import Control.Lens
96 import Data.List (lookup)
97 import Data.Text.Encoding (encodeUtf8)
99 --import Network.Wai (Request, requestHeaders, responseLBS)
100 import Network.Wai (Request, requestHeaders)
101 --import qualified Network.Wai.Handler.Warp as Warp
102 import Network.Wai.Middleware.Cors
104 import Network.Wai.Middleware.RequestLogger
105 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
107 import Network.HTTP.Types hiding (Query)
110 import Gargantext.API.Settings
112 fireWall :: Applicative f => Request -> FireWall -> f Bool
114 let origin = lookup "Origin" (requestHeaders req)
115 let host = lookup "Host" (requestHeaders req)
117 let hostOk = Just (encodeUtf8 "localhost:3000")
118 let originOk = Just (encodeUtf8 "http://localhost:8008")
120 if origin == originOk
122 || (not $ unFireWall fw)
128 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
129 makeMockApp :: MockEnv -> IO Application
131 let serverApp = appMock
133 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
134 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
135 let checkOriginAndHost app req resp = do
136 blocking <- fireWall req (env ^. menv_firewall)
139 False -> resp ( responseLBS status401 []
140 "Invalid Origin or Host header")
142 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
143 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
144 { corsOrigins = Nothing -- == /*
145 , corsMethods = [ methodGet , methodPost , methodPut
146 , methodDelete, methodOptions, methodHead]
147 , corsRequestHeaders = ["authorization", "content-type"]
148 , corsExposedHeaders = Nothing
149 , corsMaxAge = Just ( 60*60*24 ) -- one day
150 , corsVaryOrigin = False
151 , corsRequireOrigin = False
152 , corsIgnoreFailures = False
155 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
156 -- $ Warp.defaultSettings
158 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
159 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
163 makeDevApp :: Env -> IO Application
165 serverApp <- makeApp env
167 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
168 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
169 -- let checkOriginAndHost app req resp = do
170 -- blocking <- fireWall req (env ^. menv_firewall)
172 -- True -> app req resp
173 -- False -> resp ( responseLBS status401 []
174 -- "Invalid Origin or Host header")
176 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
177 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
178 { corsOrigins = Nothing -- == /*
179 , corsMethods = [ methodGet , methodPost , methodPut
180 , methodDelete, methodOptions, methodHead]
181 , corsRequestHeaders = ["authorization", "content-type"]
182 , corsExposedHeaders = Nothing
183 , corsMaxAge = Just ( 60*60*24 ) -- one day
184 , corsVaryOrigin = False
185 , corsRequireOrigin = False
186 , corsIgnoreFailures = False
189 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
190 -- $ Warp.defaultSettings
192 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
193 pure $ logStdoutDev $ corsMiddleware $ serverApp
195 ---------------------------------------------------------------------
198 -- | API for serving @swagger.json@
199 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
201 -- | API for serving main operational routes of @gargantext.org@
204 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
205 -- | TODO :<|> Summary "Latest API" :> GargAPI'
208 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
210 auth :: Connection -> AuthRequest -> Handler AuthResponse
211 auth conn ar = liftIO $ auth' conn ar
215 "auth" :> Summary "AUTH API"
216 :> ReqBody '[JSON] AuthRequest
217 :> Post '[JSON] AuthResponse
220 :<|> "user" :> Summary "First user endpoint"
224 :<|> "node" :> Summary "Node endpoint"
225 :> Capture "id" Int :> NodeAPI HyperdataAny
228 :<|> "corpus":> Summary "Corpus endpoint"
229 :> Capture "id" Int :> NodeAPI HyperdataCorpus
232 :<|> "annuaire":> Summary "Annuaire endpoint"
233 :> Capture "id" Int :> NodeAPI HyperdataAnnuaire
236 :<|> "nodes" :> Summary "Nodes endpoint"
237 :> ReqBody '[JSON] [Int] :> NodesAPI
239 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
241 :<|> "count" :> Summary "Count endpoint"
242 :> ReqBody '[JSON] Query :> CountAPI
245 :<|> "search":> Summary "Search endpoint"
246 :> ReqBody '[JSON] SearchQuery :> SearchAPI
248 :<|> "graph" :> Summary "Graph endpoint"
249 :> Capture "id" Int :> GraphAPI
252 :<|> "tree" :> Summary "Tree endpoint"
253 :> Capture "id" Int :> TreeAPI
256 -- :<|> "scraper" :> WithCallbacks ScraperAPI
262 -- :<|> "list" :> Capture "id" Int :> NodeAPI
263 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
264 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
265 ---------------------------------------------------------------------
266 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
268 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
270 ---------------------------------------------------------------------
271 -- | Server declarations
273 server :: Env -> IO (Server API)
275 gargAPI <- serverGargAPI env
280 serverGargAPI :: Env -> IO (Server GargAPI)
281 serverGargAPI env = do
282 -- orchestrator <- scrapyOrchestrator env
285 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAny)
286 :<|> nodeAPI conn (Proxy :: Proxy HyperdataCorpus)
287 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAnnuaire)
289 :<|> count -- TODO: undefined
291 :<|> graphAPI conn -- TODO: mock
295 conn = env ^. env_conn
297 serverIndex :: Server (Get '[HTML] Html)
298 serverIndex = $(do (Just s) <- liftIO (fileTypeToFileTree (FileTypeFile "purescript-gargantext/dist/index.html"))
301 ---------------------------------------------------------------------
302 swaggerFront :: Server SwaggerFrontAPI
303 swaggerFront = schemaUiServer swaggerDoc
306 gargMock :: Server GargAPI
307 gargMock = mock apiGarg Proxy
309 ---------------------------------------------------------------------
310 makeApp :: Env -> IO Application
311 makeApp = fmap (serve api) . server
313 appMock :: Application
314 appMock = serve api (swaggerFront :<|> gargMock :<|> serverIndex)
316 ---------------------------------------------------------------------
320 apiGarg :: Proxy GargAPI
322 ---------------------------------------------------------------------
324 schemaUiServer :: (Server api ~ Handler Swagger)
325 => Swagger -> Server (SwaggerSchemaUI' dir api)
326 schemaUiServer = swaggerSchemaUIServer
329 -- Type Family for the Documentation
330 type family TypeName (x :: *) :: Symbol where
332 TypeName Text = "Text"
333 TypeName x = GenericTypeName x (Rep x ())
335 type family GenericTypeName t (r :: *) :: Symbol where
336 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
338 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
341 -- | Swagger Specifications
342 swaggerDoc :: Swagger
343 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
344 & info.title .~ "Gargantext"
345 & info.version .~ "4.0.2" -- TODO same version as Gargantext
346 -- & info.base_url ?~ (URL "http://gargantext.org/")
347 & info.description ?~ "REST API specifications"
348 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
349 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
350 ["Gargantext" & description ?~ "Main operations"]
351 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
353 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
355 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
356 swaggerWriteJSON :: IO ()
357 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
359 portRouteInfo :: PortNumber -> IO ()
360 portRouteInfo port = do
361 T.putStrLn " ----Main Routes----- "
362 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
363 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
365 -- | startGargantext takes as parameters port number and Ini file.
366 startGargantext :: PortNumber -> FilePath -> IO ()
367 startGargantext port file = do
368 env <- newEnv port file
370 app <- makeDevApp env
373 startGargantextMock :: PortNumber -> IO ()
374 startGargantextMock port = do
376 application <- makeMockApp . MockEnv $ FireWall False