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.Exception (finally)
48 import Control.Monad.IO.Class (liftIO)
49 import Control.Monad.Reader (runReaderT)
50 import Data.Aeson.Encode.Pretty (encodePretty)
51 import qualified Data.ByteString.Lazy.Char8 as BL8
53 import Data.Text (Text)
54 import qualified Data.Text.IO as T
55 --import qualified Data.Set as Set
58 import Network.Wai.Handler.Warp hiding (defaultSettings)
61 import Servant.HTML.Blaze (HTML)
62 import Servant.Mock (mock)
63 --import Servant.Job.Server (WithCallbacks)
64 import Servant.Static.TH.Internal.Server (fileTreeToServer)
65 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
66 import Servant.Swagger
67 import Servant.Swagger.UI
68 -- import Servant.API.Stream
69 import Text.Blaze.Html (Html)
71 --import Gargantext.API.Swagger
72 import Gargantext.Prelude
73 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
75 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth)
76 import Gargantext.API.Ngrams (HasRepoVar(..), HasRepoSaver(..), saveRepo)
77 import Gargantext.API.Node ( GargServer
87 --import Gargantext.Database.Node.Contact (HyperdataContact)
88 import Gargantext.Database.Utils (HasConnection)
89 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
90 import Gargantext.API.Count ( CountAPI, count, Query)
91 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
92 import Gargantext.Database.Facet
94 --import Gargantext.API.Orchestrator
95 --import Gargantext.API.Orchestrator.Types
97 ---------------------------------------------------------------------
99 import GHC.Base (Applicative)
100 -- import Control.Lens
102 import Data.List (lookup)
103 import Data.Text.Encoding (encodeUtf8)
105 --import Network.Wai (Request, requestHeaders, responseLBS)
106 import Network.Wai (Request, requestHeaders)
107 --import qualified Network.Wai.Handler.Warp as Warp
108 import Network.Wai.Middleware.Cors
110 import Network.Wai.Middleware.RequestLogger
111 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
113 import Network.HTTP.Types hiding (Query)
116 import Gargantext.API.Settings
118 fireWall :: Applicative f => Request -> FireWall -> f Bool
120 let origin = lookup "Origin" (requestHeaders req)
121 let host = lookup "Host" (requestHeaders req)
123 let hostOk = Just (encodeUtf8 "localhost:3000")
124 let originOk = Just (encodeUtf8 "http://localhost:8008")
126 if origin == originOk
128 || (not $ unFireWall fw)
134 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
135 makeMockApp :: MockEnv -> IO Application
137 let serverApp = appMock
139 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
140 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
141 let checkOriginAndHost app req resp = do
142 blocking <- fireWall req (env ^. menv_firewall)
145 False -> resp ( responseLBS status401 []
146 "Invalid Origin or Host header")
148 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
149 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
150 { corsOrigins = Nothing -- == /*
151 , corsMethods = [ methodGet , methodPost , methodPut
152 , methodDelete, methodOptions, methodHead]
153 , corsRequestHeaders = ["authorization", "content-type"]
154 , corsExposedHeaders = Nothing
155 , corsMaxAge = Just ( 60*60*24 ) -- one day
156 , corsVaryOrigin = False
157 , corsRequireOrigin = False
158 , corsIgnoreFailures = False
161 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
162 -- $ Warp.defaultSettings
164 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
165 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
169 makeDevMiddleware :: IO Middleware
170 makeDevMiddleware = do
172 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
173 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
174 -- let checkOriginAndHost app req resp = do
175 -- blocking <- fireWall req (env ^. menv_firewall)
177 -- True -> app req resp
178 -- False -> resp ( responseLBS status401 []
179 -- "Invalid Origin or Host header")
181 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
182 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
183 { corsOrigins = Nothing -- == /*
184 , corsMethods = [ methodGet , methodPost , methodPut
185 , methodDelete, methodOptions, methodHead]
186 , corsRequestHeaders = ["authorization", "content-type"]
187 , corsExposedHeaders = Nothing
188 , corsMaxAge = Just ( 60*60*24 ) -- one day
189 , corsVaryOrigin = False
190 , corsRequireOrigin = False
191 , corsIgnoreFailures = False
194 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
195 -- $ Warp.defaultSettings
197 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
198 pure $ logStdoutDev . corsMiddleware
200 ---------------------------------------------------------------------
203 -- | API for serving @swagger.json@
204 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
206 -- | API for serving main operational routes of @gargantext.org@
209 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
210 -- | TODO :<|> Summary "Latest API" :> GargAPI'
213 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
217 "auth" :> Summary "AUTH API"
218 :> ReqBody '[JSON] AuthRequest
219 :> Post '[JSON] AuthResponse
222 :<|> "user" :> Summary "First user endpoint"
226 :<|> "node" :> Summary "Node endpoint"
227 :> Capture "id" NodeId :> NodeAPI HyperdataAny
230 :<|> "corpus":> Summary "Corpus endpoint"
231 :> Capture "id" CorpusId :> NodeAPI HyperdataCorpus
234 :<|> "annuaire":> Summary "Annuaire endpoint"
235 :> Capture "id" AnnuaireId :> NodeAPI HyperdataAnnuaire
238 :<|> "nodes" :> Summary "Nodes endpoint"
239 :> ReqBody '[JSON] [NodeId] :> NodesAPI
241 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
243 :<|> "count" :> Summary "Count endpoint"
244 :> ReqBody '[JSON] Query :> CountAPI
247 :<|> "search":> Summary "Search endpoint"
248 :> ReqBody '[JSON] SearchQuery
249 :> QueryParam "offset" Int
250 :> QueryParam "limit" Int
251 :> QueryParam "order" OrderBy
254 -- TODO move to NodeAPI?
255 :<|> "graph" :> Summary "Graph endpoint"
256 :> Capture "id" NodeId :> GraphAPI
258 -- TODO move to NodeAPI?
260 :<|> "tree" :> Summary "Tree endpoint"
261 :> Capture "id" NodeId :> TreeAPI
264 -- :<|> "scraper" :> WithCallbacks ScraperAPI
270 -- :<|> "list" :> Capture "id" Int :> NodeAPI
271 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
272 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
273 ---------------------------------------------------------------------
274 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
276 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
278 ---------------------------------------------------------------------
279 -- | Server declarations
281 server :: (HasConnection env, HasRepoVar env, HasRepoSaver env)
282 => env -> IO (Server API)
284 -- orchestrator <- scrapyOrchestrator env
286 :<|> hoistServer (Proxy :: Proxy GargAPI) (`runReaderT` env) serverGargAPI
289 serverGargAPI :: GargServer GargAPI
290 serverGargAPI -- orchestrator
293 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) fakeUserId
294 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) fakeUserId
295 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) fakeUserId
297 :<|> count -- TODO: undefined
299 :<|> graphAPI -- TODO: mock
303 fakeUserId = 1 -- TODO
305 serverIndex :: Server (Get '[HTML] Html)
306 serverIndex = $(do (Just s) <- liftIO (fileTypeToFileTree (FileTypeFile "purescript-gargantext/dist/index.html"))
309 ---------------------------------------------------------------------
310 swaggerFront :: Server SwaggerFrontAPI
311 swaggerFront = schemaUiServer swaggerDoc
314 gargMock :: Server GargAPI
315 gargMock = mock apiGarg Proxy
317 ---------------------------------------------------------------------
318 makeApp :: (HasConnection env, HasRepoVar env, HasRepoSaver env) => env -> IO Application
319 makeApp = fmap (serve api) . server
321 appMock :: Application
322 appMock = serve api (swaggerFront :<|> gargMock :<|> serverIndex)
324 ---------------------------------------------------------------------
328 apiGarg :: Proxy GargAPI
330 ---------------------------------------------------------------------
332 schemaUiServer :: (Server api ~ Handler Swagger)
333 => Swagger -> Server (SwaggerSchemaUI' dir api)
334 schemaUiServer = swaggerSchemaUIServer
337 -- Type Family for the Documentation
338 type family TypeName (x :: *) :: Symbol where
340 TypeName Text = "Text"
341 TypeName x = GenericTypeName x (Rep x ())
343 type family GenericTypeName t (r :: *) :: Symbol where
344 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
346 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
349 -- | Swagger Specifications
350 swaggerDoc :: Swagger
351 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
352 & info.title .~ "Gargantext"
353 & info.version .~ "4.0.2" -- TODO same version as Gargantext
354 -- & info.base_url ?~ (URL "http://gargantext.org/")
355 & info.description ?~ "REST API specifications"
356 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
357 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
358 ["Gargantext" & description ?~ "Main operations"]
359 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
361 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
363 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
364 swaggerWriteJSON :: IO ()
365 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
367 portRouteInfo :: PortNumber -> IO ()
368 portRouteInfo port = do
369 T.putStrLn " ----Main Routes----- "
370 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
371 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
373 stopGargantext :: HasRepoSaver env => env -> IO ()
374 stopGargantext env = do
375 T.putStrLn "----- Stopping gargantext -----"
376 runReaderT saveRepo env
378 -- | startGargantext takes as parameters port number and Ini file.
379 startGargantext :: PortNumber -> FilePath -> IO ()
380 startGargantext port file = do
381 env <- newEnv port file
384 mid <- makeDevMiddleware
385 run port (mid app) `finally` stopGargantext env
387 startGargantextMock :: PortNumber -> IO ()
388 startGargantextMock port = do
390 application <- makeMockApp . MockEnv $ FireWall False