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 FlexibleContexts #-}
28 {-# LANGUAGE FlexibleInstances #-}
29 {-# LANGUAGE OverloadedStrings #-}
30 {-# LANGUAGE TemplateHaskell #-}
31 {-# LANGUAGE TypeOperators #-}
32 {-# LANGUAGE KindSignatures #-}
33 {-# LANGUAGE TypeFamilies #-}
34 {-# LANGUAGE UndecidableInstances #-}
36 ---------------------------------------------------------------------
39 ---------------------------------------------------------------------
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 Control.Monad.Reader (runReaderT)
49 import Data.Aeson.Encode.Pretty (encodePretty)
50 import qualified Data.ByteString.Lazy.Char8 as BL8
52 import Data.Text (Text)
53 import qualified Data.Text.IO as T
54 --import qualified Data.Set as Set
57 import Network.Wai.Handler.Warp hiding (defaultSettings)
60 import Servant.HTML.Blaze (HTML)
61 import Servant.Mock (mock)
62 --import Servant.Job.Server (WithCallbacks)
63 import Servant.Static.TH.Internal.Server (fileTreeToServer)
64 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
65 import Servant.Swagger
66 import Servant.Swagger.UI
67 -- import Servant.API.Stream
68 import Text.Blaze.Html (Html)
70 --import Gargantext.API.Swagger
71 import Gargantext.Prelude
72 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
74 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth)
75 import Gargantext.API.Ngrams (HasRepoVar)
76 import Gargantext.API.Node ( GargServer
86 --import Gargantext.Database.Node.Contact (HyperdataContact)
87 import Gargantext.Database.Utils (HasConnection)
88 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
89 import Gargantext.API.Count ( CountAPI, count, Query)
90 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
91 import Gargantext.Database.Facet
93 --import Gargantext.API.Orchestrator
94 --import Gargantext.API.Orchestrator.Types
96 ---------------------------------------------------------------------
98 import GHC.Base (Applicative)
99 -- import Control.Lens
101 import Data.List (lookup)
102 import Data.Text.Encoding (encodeUtf8)
104 --import Network.Wai (Request, requestHeaders, responseLBS)
105 import Network.Wai (Request, requestHeaders)
106 --import qualified Network.Wai.Handler.Warp as Warp
107 import Network.Wai.Middleware.Cors
109 import Network.Wai.Middleware.RequestLogger
110 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
112 import Network.HTTP.Types hiding (Query)
115 import Gargantext.API.Settings
117 fireWall :: Applicative f => Request -> FireWall -> f Bool
119 let origin = lookup "Origin" (requestHeaders req)
120 let host = lookup "Host" (requestHeaders req)
122 let hostOk = Just (encodeUtf8 "localhost:3000")
123 let originOk = Just (encodeUtf8 "http://localhost:8008")
125 if origin == originOk
127 || (not $ unFireWall fw)
133 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
134 makeMockApp :: MockEnv -> IO Application
136 let serverApp = appMock
138 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
139 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
140 let checkOriginAndHost app req resp = do
141 blocking <- fireWall req (env ^. menv_firewall)
144 False -> resp ( responseLBS status401 []
145 "Invalid Origin or Host header")
147 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
148 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
149 { corsOrigins = Nothing -- == /*
150 , corsMethods = [ methodGet , methodPost , methodPut
151 , methodDelete, methodOptions, methodHead]
152 , corsRequestHeaders = ["authorization", "content-type"]
153 , corsExposedHeaders = Nothing
154 , corsMaxAge = Just ( 60*60*24 ) -- one day
155 , corsVaryOrigin = False
156 , corsRequireOrigin = False
157 , corsIgnoreFailures = False
160 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
161 -- $ Warp.defaultSettings
163 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
164 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
168 makeDevMiddleware :: IO Middleware
169 makeDevMiddleware = do
171 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
172 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
173 -- let checkOriginAndHost app req resp = do
174 -- blocking <- fireWall req (env ^. menv_firewall)
176 -- True -> app req resp
177 -- False -> resp ( responseLBS status401 []
178 -- "Invalid Origin or Host header")
180 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
181 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
182 { corsOrigins = Nothing -- == /*
183 , corsMethods = [ methodGet , methodPost , methodPut
184 , methodDelete, methodOptions, methodHead]
185 , corsRequestHeaders = ["authorization", "content-type"]
186 , corsExposedHeaders = Nothing
187 , corsMaxAge = Just ( 60*60*24 ) -- one day
188 , corsVaryOrigin = False
189 , corsRequireOrigin = False
190 , corsIgnoreFailures = False
193 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
194 -- $ Warp.defaultSettings
196 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
197 pure $ logStdoutDev . corsMiddleware
199 ---------------------------------------------------------------------
202 -- | API for serving @swagger.json@
203 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
205 -- | API for serving main operational routes of @gargantext.org@
208 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
209 -- | TODO :<|> Summary "Latest API" :> GargAPI'
212 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
216 "auth" :> Summary "AUTH API"
217 :> ReqBody '[JSON] AuthRequest
218 :> Post '[JSON] AuthResponse
221 :<|> "user" :> Summary "First user endpoint"
225 :<|> "node" :> Summary "Node endpoint"
226 :> Capture "id" NodeId :> NodeAPI HyperdataAny
229 :<|> "corpus":> Summary "Corpus endpoint"
230 :> Capture "id" CorpusId :> NodeAPI HyperdataCorpus
233 :<|> "annuaire":> Summary "Annuaire endpoint"
234 :> Capture "id" AnnuaireId :> NodeAPI HyperdataAnnuaire
237 :<|> "nodes" :> Summary "Nodes endpoint"
238 :> ReqBody '[JSON] [NodeId] :> NodesAPI
240 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
242 :<|> "count" :> Summary "Count endpoint"
243 :> ReqBody '[JSON] Query :> CountAPI
246 :<|> "search":> Summary "Search endpoint"
247 :> ReqBody '[JSON] SearchQuery
248 :> QueryParam "offset" Int
249 :> QueryParam "limit" Int
250 :> QueryParam "order" OrderBy
253 -- TODO move to NodeAPI?
254 :<|> "graph" :> Summary "Graph endpoint"
255 :> Capture "id" NodeId :> GraphAPI
257 -- TODO move to NodeAPI?
259 :<|> "tree" :> Summary "Tree endpoint"
260 :> Capture "id" NodeId :> TreeAPI
263 -- :<|> "scraper" :> WithCallbacks ScraperAPI
269 -- :<|> "list" :> Capture "id" Int :> NodeAPI
270 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
271 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
272 ---------------------------------------------------------------------
273 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
275 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
277 ---------------------------------------------------------------------
278 -- | Server declarations
280 server :: (HasConnection env, HasRepoVar env) => env
283 -- orchestrator <- scrapyOrchestrator env
285 :<|> hoistServer (Proxy :: Proxy GargAPI) (`runReaderT` env) serverGargAPI
288 serverGargAPI :: GargServer GargAPI
289 serverGargAPI -- orchestrator
292 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) fakeUserId
293 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) fakeUserId
294 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) fakeUserId
296 :<|> count -- TODO: undefined
298 :<|> graphAPI -- TODO: mock
302 fakeUserId = 1 -- TODO
304 serverIndex :: Server (Get '[HTML] Html)
305 serverIndex = $(do (Just s) <- liftIO (fileTypeToFileTree (FileTypeFile "purescript-gargantext/dist/index.html"))
308 ---------------------------------------------------------------------
309 swaggerFront :: Server SwaggerFrontAPI
310 swaggerFront = schemaUiServer swaggerDoc
313 gargMock :: Server GargAPI
314 gargMock = mock apiGarg Proxy
316 ---------------------------------------------------------------------
317 makeApp :: (HasConnection env, HasRepoVar env) => env -> IO Application
318 makeApp = fmap (serve api) . server
320 appMock :: Application
321 appMock = serve api (swaggerFront :<|> gargMock :<|> serverIndex)
323 ---------------------------------------------------------------------
327 apiGarg :: Proxy GargAPI
329 ---------------------------------------------------------------------
331 schemaUiServer :: (Server api ~ Handler Swagger)
332 => Swagger -> Server (SwaggerSchemaUI' dir api)
333 schemaUiServer = swaggerSchemaUIServer
336 -- Type Family for the Documentation
337 type family TypeName (x :: *) :: Symbol where
339 TypeName Text = "Text"
340 TypeName x = GenericTypeName x (Rep x ())
342 type family GenericTypeName t (r :: *) :: Symbol where
343 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
345 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
348 -- | Swagger Specifications
349 swaggerDoc :: Swagger
350 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
351 & info.title .~ "Gargantext"
352 & info.version .~ "4.0.2" -- TODO same version as Gargantext
353 -- & info.base_url ?~ (URL "http://gargantext.org/")
354 & info.description ?~ "REST API specifications"
355 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
356 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
357 ["Gargantext" & description ?~ "Main operations"]
358 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
360 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
362 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
363 swaggerWriteJSON :: IO ()
364 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
366 portRouteInfo :: PortNumber -> IO ()
367 portRouteInfo port = do
368 T.putStrLn " ----Main Routes----- "
369 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
370 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
372 -- | startGargantext takes as parameters port number and Ini file.
373 startGargantext :: PortNumber -> FilePath -> IO ()
374 startGargantext port file = do
375 env <- newEnv port file
378 mid <- makeDevMiddleware
381 startGargantextMock :: PortNumber -> IO ()
382 startGargantextMock port = do
384 application <- makeMockApp . MockEnv $ FireWall False