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 (RESTful) API of the instance Gargantext.
12 The Garg-API is typed to derive the documentation, the mock and tests.
14 This API is indeed typed in order to be able to derive both the server
17 The Garg-API-Monad enables:
20 - Database connection (long term)
21 - In Memory stack management (short term)
24 Thanks to Yann Esposito for our discussions at the start and to Nicolas
25 Pouillard (who mainly made it).
29 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
31 {-# LANGUAGE ConstraintKinds #-}
32 {-# LANGUAGE NoImplicitPrelude #-}
33 {-# LANGUAGE DataKinds #-}
34 {-# LANGUAGE DeriveGeneric #-}
35 {-# LANGUAGE FlexibleContexts #-}
36 {-# LANGUAGE FlexibleInstances #-}
37 {-# LANGUAGE OverloadedStrings #-}
38 {-# LANGUAGE TemplateHaskell #-}
39 {-# LANGUAGE TypeOperators #-}
40 {-# LANGUAGE KindSignatures #-}
41 {-# LANGUAGE RankNTypes #-}
42 {-# LANGUAGE ScopedTypeVariables #-}
43 {-# LANGUAGE TypeFamilies #-}
44 {-# LANGUAGE UndecidableInstances #-}
46 ---------------------------------------------------------------------
49 ---------------------------------------------------------------------
51 import System.IO (FilePath)
53 import GHC.Generics (D1, Meta (..), Rep)
54 import GHC.TypeLits (AppendSymbol, Symbol)
57 import Control.Exception (finally)
58 import Control.Monad.Except (withExceptT, ExceptT)
59 import Control.Monad.IO.Class (liftIO)
60 import Control.Monad.Reader (ReaderT, runReaderT)
61 import Data.Aeson.Encode.Pretty (encodePretty)
62 import qualified Data.ByteString.Lazy.Char8 as BL8
64 import Data.Text (Text)
65 import qualified Data.Text.IO as T
66 --import qualified Data.Set as Set
70 import Network.Wai.Handler.Warp hiding (defaultSettings)
73 import Servant.Auth as SA
74 import Servant.Auth.Server (AuthResult(..))
75 import Servant.Auth.Swagger ()
76 import Servant.HTML.Blaze (HTML)
77 --import Servant.Mock (mock)
78 --import Servant.Job.Server (WithCallbacks)
79 import Servant.Job.Async
80 import Servant.Static.TH.Internal.Server (fileTreeToServer)
81 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
82 import Servant.Swagger
83 import Servant.Swagger.UI
84 -- import Servant.API.Stream
85 import Text.Blaze.Html (Html)
87 --import Gargantext.API.Swagger
89 import Gargantext.Database.Node.Contact (HyperdataContact)
90 import Gargantext.API.Auth (AuthRequest, AuthResponse, AuthenticatedUser(..), AuthContext, auth, withAccess, PathId(..))
91 import Gargantext.API.Count ( CountAPI, count, Query)
92 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
93 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo, TableNgramsApi, apiNgramsTableDoc)
94 import Gargantext.API.Node
95 import Gargantext.API.Search (SearchPairsAPI, searchPairs)
96 import Gargantext.API.Types
97 import qualified Gargantext.API.Annuaire as Annuaire
98 import qualified Gargantext.API.Export as Export
99 import qualified Gargantext.API.Corpus.New as New
100 import Gargantext.Database.Types.Node
101 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
102 import Gargantext.Database.Utils (HasConnection)
103 import Gargantext.Prelude
104 import Gargantext.Viz.Graph.API
106 --import Gargantext.API.Orchestrator
107 import Gargantext.API.Orchestrator.Types
109 ---------------------------------------------------------------------
111 import GHC.Base (Applicative)
112 -- import Control.Lens
114 import Data.List (lookup)
115 import Data.Text.Encoding (encodeUtf8)
117 --import Network.Wai (Request, requestHeaders, responseLBS)
118 import Network.Wai (Request, requestHeaders)
119 --import qualified Network.Wai.Handler.Warp as Warp
120 import Network.Wai.Middleware.Cors
122 import Network.Wai.Middleware.RequestLogger
123 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
125 import Network.HTTP.Types hiding (Query)
127 import Gargantext.API.Settings
129 showAsServantErr :: GargError -> ServerError
130 showAsServantErr (GargServerError err) = err
131 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
133 fireWall :: Applicative f => Request -> FireWall -> f Bool
135 let origin = lookup "Origin" (requestHeaders req)
136 let host = lookup "Host" (requestHeaders req)
138 let hostOk = Just (encodeUtf8 "localhost:3000")
139 let originOk = Just (encodeUtf8 "http://localhost:8008")
141 if origin == originOk
143 || (not $ unFireWall fw)
149 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
150 makeMockApp :: MockEnv -> IO Application
152 let serverApp = appMock
154 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
155 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
156 let checkOriginAndHost app req resp = do
157 blocking <- fireWall req (env ^. menv_firewall)
160 False -> resp ( responseLBS status401 []
161 "Invalid Origin or Host header")
163 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
164 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
165 { corsOrigins = Nothing -- == /*
166 , corsMethods = [ methodGet , methodPost , methodPut
167 , methodDelete, methodOptions, methodHead]
168 , corsRequestHeaders = ["authorization", "content-type"]
169 , corsExposedHeaders = Nothing
170 , corsMaxAge = Just ( 60*60*24 ) -- one day
171 , corsVaryOrigin = False
172 , corsRequireOrigin = False
173 , corsIgnoreFailures = False
176 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
177 -- $ Warp.defaultSettings
179 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
180 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
184 makeDevMiddleware :: IO Middleware
185 makeDevMiddleware = do
187 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
188 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
189 -- let checkOriginAndHost app req resp = do
190 -- blocking <- fireWall req (env ^. menv_firewall)
192 -- True -> app req resp
193 -- False -> resp ( responseLBS status401 []
194 -- "Invalid Origin or Host header")
196 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
197 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
198 { corsOrigins = Nothing -- == /*
199 , corsMethods = [ methodGet , methodPost , methodPut
200 , methodDelete, methodOptions, methodHead]
201 , corsRequestHeaders = ["authorization", "content-type"]
202 , corsExposedHeaders = Nothing
203 , corsMaxAge = Just ( 60*60*24 ) -- one day
204 , corsVaryOrigin = False
205 , corsRequireOrigin = False
206 , corsIgnoreFailures = False
209 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
210 -- $ Warp.defaultSettings
212 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
213 pure $ logStdoutDev . corsMiddleware
215 ---------------------------------------------------------------------
218 -- | API for serving @swagger.json@
219 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
221 -- | API for serving main operational routes of @gargantext.org@
224 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
225 -- | TODO :<|> Summary "Latest API" :> GargAPI'
228 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
232 "auth" :> Summary "AUTH API"
233 :> ReqBody '[JSON] AuthRequest
234 :> Post '[JSON] AuthResponse
235 -- TODO-ACCESS here we want to request a particular header for
236 -- auth and capabilities.
239 type GargPrivateAPI = SA.Auth '[SA.JWT] AuthenticatedUser :> GargPrivateAPI'
243 = "user" :> Summary "First user endpoint"
245 :<|> "nodes" :> Summary "Nodes endpoint"
246 :> ReqBody '[JSON] [NodeId] :> NodesAPI
248 type GargPrivateAPI' =
252 :<|> "node" :> Summary "Node endpoint"
253 :> Capture "node_id" NodeId
254 :> NodeAPI HyperdataAny
257 :<|> "corpus":> Summary "Corpus endpoint"
258 :> Capture "corpus_id" CorpusId
259 :> NodeAPI HyperdataCorpus
261 :<|> "corpus":> Summary "Corpus endpoint"
262 :> Capture "node1_id" NodeId
264 :> Capture "node2_id" NodeId
265 :> NodeNodeAPI HyperdataAny
267 :<|> "corpus" :> Capture "node_id" CorpusId
271 :<|> "annuaire":> Summary "Annuaire endpoint"
272 :> Capture "annuaire_id" AnnuaireId
273 :> NodeAPI HyperdataAnnuaire
275 :<|> "annuaire" :> Summary "Contact endpoint"
276 :> Capture "annuaire_id" NodeId
277 :> "contact" :> Capture "contact_id" NodeId
278 :> NodeNodeAPI HyperdataContact
281 :<|> "document":> Summary "Document endpoint"
282 :> Capture "doc_id" DocId
283 :> "ngrams" :> TableNgramsApi
285 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
287 :<|> "count" :> Summary "Count endpoint"
288 :> ReqBody '[JSON] Query :> CountAPI
290 -- Corpus endpoint --> TODO rename s/search/filter/g
291 :<|> "search":> Capture "corpus" NodeId
294 -- TODO move to NodeAPI?
295 :<|> "graph" :> Summary "Graph endpoint"
296 :> Capture "graph_id" NodeId
299 -- TODO move to NodeAPI?
301 :<|> "tree" :> Summary "Tree endpoint"
302 :> Capture "tree_id" NodeId
307 :<|> New.AddWithQuery
309 :<|> Annuaire.AddWithForm
310 -- :<|> New.AddWithFile
311 -- :<|> "scraper" :> WithCallbacks ScraperAPI
312 -- :<|> "new" :> New.Api
318 -- :<|> "list" :> Capture "node_id" Int :> NodeAPI
319 -- :<|> "ngrams" :> Capture "node_id" Int :> NodeAPI
320 -- :<|> "auth" :> Capture "node_id" Int :> NodeAPI
321 ---------------------------------------------------------------------
323 type API = SwaggerAPI
325 :<|> Get '[HTML] Html
328 -- This is the concrete monad. It needs to be used as little as possible,
329 -- instead, prefer GargServer, GargServerT, GargServerC.
330 type GargServerM env err = ReaderT env (ExceptT err IO)
336 , HasJobEnv env ScraperStatus ScraperStatus
339 ---------------------------------------------------------------------
340 -- | Server declarations
342 server :: forall env. EnvC env => env -> IO (Server API)
344 -- orchestrator <- scrapyOrchestrator env
345 pure $ schemaUiServer swaggerDoc
348 :<|> hoistServerWithContext (Proxy :: Proxy GargAPI) (Proxy :: Proxy AuthContext) transform serverGargAPI
350 transform :: forall a. GargServerM env GargError a -> Handler a
351 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
353 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
354 serverGargAPI -- orchestrator
355 = auth :<|> serverPrivateGargAPI
358 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
359 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
360 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
361 -- Here throwAll' requires a concrete type for the monad.
363 -- TODO-SECURITY admin only: withAdmin
364 -- Question: How do we mark admins?
365 serverGargAdminAPI :: GargServer GargAdminAPI
370 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
371 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
373 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
374 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
375 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
376 :<|> Export.getCorpus -- uid
377 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
378 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
380 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid
381 <$> PathNode <*> apiNgramsTableDoc
383 :<|> count -- TODO: undefined
385 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid
386 <$> PathNode <*> searchPairs -- TODO: move elsewhere
388 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid
389 <$> PathNode <*> graphAPI uid -- TODO: mock
391 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid
392 <$> PathNode <*> treeAPI
395 -- :<|> (\corpus -> addWithQuery corpus :<|> addWithFile corpus)
396 :<|> addCorpusWithForm
397 :<|> addCorpusWithQuery
399 :<|> addAnnuaireWithForm
400 -- :<|> New.api uid -- TODO-SECURITY
401 -- :<|> New.info uid -- TODO-SECURITY
404 addUpload :: GargServer New.Upload
405 addUpload cId = (serveJobsAPI $ JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log)))
406 :<|> (serveJobsAPI $ JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log)))
409 addCorpusWithQuery :: GargServer New.AddWithQuery
410 addCorpusWithQuery cid =
412 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
414 addWithFile :: GargServer New.AddWithFile
415 addWithFile cid i f =
417 JobFunction (\_i log -> New.addToCorpusWithFile cid i f (liftIO . log))
419 addCorpusWithForm :: GargServer New.AddWithForm
420 addCorpusWithForm cid =
422 JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log))
424 addAnnuaireWithForm :: GargServer Annuaire.AddWithForm
425 addAnnuaireWithForm cid =
427 JobFunction (\i log -> Annuaire.addToAnnuaireWithForm cid i (liftIO . log))
429 serverStatic :: Server (Get '[HTML] Html)
431 let path = "purescript-gargantext/dist/index.html"
432 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
436 ---------------------------------------------------------------------
437 --gargMock :: Server GargAPI
438 --gargMock = mock apiGarg Proxy
439 ---------------------------------------------------------------------
440 makeApp :: EnvC env => env -> IO Application
441 makeApp env = serveWithContext api cfg <$> server env
443 cfg :: Servant.Context AuthContext
444 cfg = env ^. settings . jwtSettings
445 :. env ^. settings . cookieSettings
449 --appMock :: Application
450 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
451 ---------------------------------------------------------------------
455 apiGarg :: Proxy GargAPI
457 ---------------------------------------------------------------------
458 schemaUiServer :: (Server api ~ Handler Swagger)
459 => Swagger -> Server (SwaggerSchemaUI' dir api)
460 schemaUiServer = swaggerSchemaUIServer
462 -- Type Family for the Documentation
463 type family TypeName (x :: *) :: Symbol where
465 TypeName Text = "Text"
466 TypeName x = GenericTypeName x (Rep x ())
468 type family GenericTypeName t (r :: *) :: Symbol where
469 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
471 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
474 -- | Swagger Specifications
475 swaggerDoc :: Swagger
476 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
477 & info.title .~ "Gargantext"
478 & info.version .~ "4.0.2" -- TODO same version as Gargantext
479 -- & info.base_url ?~ (URL "http://gargantext.org/")
480 & info.description ?~ "REST API specifications"
481 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
482 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
483 ["Gargantext" & description ?~ "Main operations"]
484 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
486 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
488 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
489 swaggerWriteJSON :: IO ()
490 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
492 portRouteInfo :: PortNumber -> IO ()
493 portRouteInfo port = do
494 T.putStrLn " ----Main Routes----- "
495 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
496 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
498 stopGargantext :: HasRepoSaver env => env -> IO ()
499 stopGargantext env = do
500 T.putStrLn "----- Stopping gargantext -----"
501 runReaderT saveRepo env
503 -- | startGargantext takes as parameters port number and Ini file.
504 startGargantext :: PortNumber -> FilePath -> IO ()
505 startGargantext port file = do
506 env <- newEnv port file
509 mid <- makeDevMiddleware
510 run port (mid app) `finally` stopGargantext env
513 startGargantextMock :: PortNumber -> IO ()
514 startGargantextMock port = do
516 application <- makeMockApp . MockEnv $ FireWall False