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 ---------------------------------------------------------------------
50 import Control.Concurrent (threadDelay)
51 import Control.Exception (finally)
53 import Control.Monad.Except (withExceptT, ExceptT)
54 import Control.Monad.IO.Class (liftIO)
55 import Control.Monad.Reader (ReaderT, runReaderT)
56 import Data.Aeson.Encode.Pretty (encodePretty)
58 import Data.Text (Text)
60 import Data.Version (showVersion)
61 import GHC.Generics (D1, Meta (..), Rep)
62 import GHC.TypeLits (AppendSymbol, Symbol)
64 import Network.Wai.Handler.Warp hiding (defaultSettings)
65 import qualified Paths_gargantext as PG -- cabal magic build module
67 import Servant.Auth as SA
68 import Servant.Auth.Server (AuthResult(..))
69 import Servant.Auth.Swagger ()
70 import Servant.Job.Async
71 import Servant.Swagger
72 import Servant.Swagger.UI
73 import System.IO (FilePath)
74 import Data.List (lookup)
75 import Data.Text.Encoding (encodeUtf8)
76 import GHC.Base (Applicative)
77 import Gargantext.API.Auth (AuthRequest, AuthResponse, AuthenticatedUser(..), AuthContext, auth, withAccess, PathId(..))
78 import Gargantext.API.Count ( CountAPI, count, Query)
79 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
80 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo, TableNgramsApi, apiNgramsTableDoc)
81 import Gargantext.API.Node
82 import Gargantext.API.Orchestrator.Types
83 import Gargantext.API.Search (SearchPairsAPI, searchPairs)
84 import Gargantext.API.Settings
85 import Gargantext.API.Types
86 import Gargantext.Database.Node.Contact (HyperdataContact)
87 import Gargantext.Database.Types.Node
88 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
89 import Gargantext.Database.Utils (HasConnection)
90 import Gargantext.Prelude
91 import Gargantext.Viz.Graph.API
92 import Network.HTTP.Types hiding (Query)
93 import Network.Wai (Request, requestHeaders)
94 import Network.Wai.Middleware.Cors
95 import Network.Wai.Middleware.RequestLogger
96 import qualified Data.ByteString.Lazy.Char8 as BL8
97 import qualified Data.Text.IO as T
98 import qualified Gargantext.API.Annuaire as Annuaire
99 import qualified Gargantext.API.Corpus.New as New
100 import qualified Gargantext.API.Export as Export
101 import qualified Gargantext.API.Ngrams.List as List
103 showAsServantErr :: GargError -> ServerError
104 showAsServantErr (GargServerError err) = err
105 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
107 fireWall :: Applicative f => Request -> FireWall -> f Bool
109 let origin = lookup "Origin" (requestHeaders req)
110 let host = lookup "Host" (requestHeaders req)
112 let hostOk = Just (encodeUtf8 "localhost:3000")
113 let originOk = Just (encodeUtf8 "http://localhost:8008")
115 if origin == originOk
117 || (not $ unFireWall fw)
123 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
124 makeMockApp :: MockEnv -> IO Application
126 let serverApp = appMock
128 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
129 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
130 let checkOriginAndHost app req resp = do
131 blocking <- fireWall req (env ^. menv_firewall)
134 False -> resp ( responseLBS status401 []
135 "Invalid Origin or Host header")
137 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
138 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
139 { corsOrigins = Nothing -- == /*
140 , corsMethods = [ methodGet , methodPost , methodPut
141 , methodDelete, methodOptions, methodHead]
142 , corsRequestHeaders = ["authorization", "content-type"]
143 , corsExposedHeaders = Nothing
144 , corsMaxAge = Just ( 60*60*24 ) -- one day
145 , corsVaryOrigin = False
146 , corsRequireOrigin = False
147 , corsIgnoreFailures = False
150 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
151 -- $ Warp.defaultSettings
153 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
154 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
158 makeDevMiddleware :: IO Middleware
159 makeDevMiddleware = do
161 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
162 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
163 -- let checkOriginAndHost app req resp = do
164 -- blocking <- fireWall req (env ^. menv_firewall)
166 -- True -> app req resp
167 -- False -> resp ( responseLBS status401 []
168 -- "Invalid Origin or Host header")
170 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
171 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
172 { corsOrigins = Nothing -- == /*
173 , corsMethods = [ methodGet , methodPost , methodPut
174 , methodDelete, methodOptions, methodHead]
175 , corsRequestHeaders = ["authorization", "content-type"]
176 , corsExposedHeaders = Nothing
177 , corsMaxAge = Just ( 60*60*24 ) -- one day
178 , corsVaryOrigin = False
179 , corsRequireOrigin = False
180 , corsIgnoreFailures = False
183 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
184 -- $ Warp.defaultSettings
186 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
187 pure $ logStdoutDev . corsMiddleware
189 ---------------------------------------------------------------------
192 -- | API for serving @swagger.json@
193 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
195 -- | API for serving main operational routes of @gargantext.org@
198 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
199 -- | TODO :<|> Summary "Latest API" :> GargAPI'
202 type GargAPIVersion = "v1.0"
203 :> Summary "Garg API Version "
206 type GargVersion = "version"
207 :> Summary "Backend version"
212 "auth" :> Summary "AUTH API"
213 :> ReqBody '[JSON] AuthRequest
214 :> Post '[JSON] AuthResponse
216 -- TODO-ACCESS here we want to request a particular header for
217 -- auth and capabilities.
221 type GargPrivateAPI = SA.Auth '[SA.JWT, SA.Cookie] AuthenticatedUser :> GargPrivateAPI'
225 = "user" :> Summary "First user endpoint"
227 :<|> "nodes" :> Summary "Nodes endpoint"
228 :> ReqBody '[JSON] [NodeId] :> NodesAPI
230 ----------------------------------------
232 type WaitAPI = Get '[JSON] Text
234 waitAPI :: Int -> GargServer WaitAPI
237 m = (10 :: Int) ^ (6 :: Int)
238 _ <- liftIO $ threadDelay ( m * n)
239 pure $ "Waited: " <> (cs $ show n)
240 ----------------------------------------
243 type GargPrivateAPI' =
247 :<|> "node" :> Summary "Node endpoint"
248 :> Capture "node_id" NodeId
249 :> NodeAPI HyperdataAny
252 :<|> "corpus" :> Summary "Corpus endpoint"
253 :> Capture "corpus_id" CorpusId
254 :> NodeAPI HyperdataCorpus
256 :<|> "corpus" :> Summary "Corpus endpoint"
257 :> Capture "node1_id" NodeId
259 :> Capture "node2_id" NodeId
260 :> NodeNodeAPI HyperdataAny
262 :<|> "corpus" :> Capture "node_id" CorpusId
266 :<|> "annuaire" :> Summary "Annuaire endpoint"
267 :> Capture "annuaire_id" AnnuaireId
268 :> NodeAPI HyperdataAnnuaire
270 :<|> "annuaire" :> Summary "Contact endpoint"
271 :> Capture "annuaire_id" NodeId
273 :> Capture "contact_id" NodeId
274 :> NodeNodeAPI HyperdataContact
277 :<|> "document" :> Summary "Document endpoint"
278 :> Capture "doc_id" DocId
279 :> "ngrams" :> TableNgramsApi
281 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
283 :<|> "count" :> Summary "Count endpoint"
284 :> ReqBody '[JSON] Query
287 -- Corpus endpoint --> TODO rename s/search/filter/g
288 :<|> "search" :> Capture "corpus" NodeId
291 -- TODO move to NodeAPI?
292 :<|> "graph" :> Summary "Graph endpoint"
293 :> Capture "graph_id" NodeId
296 -- TODO move to NodeAPI?
298 :<|> "tree" :> Summary "Tree endpoint"
299 :> Capture "tree_id" NodeId
304 :<|> New.AddWithQuery
306 :<|> Annuaire.AddWithForm
307 -- :<|> New.AddWithFile
308 -- :<|> "scraper" :> WithCallbacks ScraperAPI
309 -- :<|> "new" :> New.Api
311 :<|> "lists" :> Summary "List export API"
312 :> Capture "listId" ListId
315 :<|> "wait" :> Summary "Wait test"
317 :> WaitAPI -- Get '[JSON] Int
323 -- :<|> "list" :> Capture "node_id" Int :> NodeAPI
324 -- :<|> "ngrams" :> Capture "node_id" Int :> NodeAPI
325 -- :<|> "auth" :> Capture "node_id" Int :> NodeAPI
326 ---------------------------------------------------------------------
328 type API = SwaggerAPI
332 -- This is the concrete monad. It needs to be used as little as possible,
333 -- instead, prefer GargServer, GargServerT, GargServerC.
334 type GargServerM env err = ReaderT env (ExceptT err IO)
340 , HasJobEnv env ScraperStatus ScraperStatus
343 ---------------------------------------------------------------------
344 -- | Server declarations
346 server :: forall env. EnvC env => env -> IO (Server API)
348 -- orchestrator <- scrapyOrchestrator env
349 pure $ schemaUiServer swaggerDoc
350 :<|> hoistServerWithContext
351 (Proxy :: Proxy GargAPI)
352 (Proxy :: Proxy AuthContext)
357 transform :: forall a. GargServerM env GargError a -> Handler a
358 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
360 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
361 serverGargAPI -- orchestrator
364 :<|> serverPrivateGargAPI
368 gargVersion :: GargServer GargVersion
369 gargVersion = pure (cs $ showVersion PG.version)
371 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
372 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
373 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
374 -- Here throwAll' requires a concrete type for the monad.
376 -- TODO-SECURITY admin only: withAdmin
377 -- Question: How do we mark admins?
378 serverGargAdminAPI :: GargServer GargAdminAPI
379 serverGargAdminAPI = roots
383 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
384 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
386 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
387 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
388 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
389 :<|> Export.getCorpus -- uid
390 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
391 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
393 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid
394 <$> PathNode <*> apiNgramsTableDoc
396 :<|> count -- TODO: undefined
398 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid
399 <$> PathNode <*> searchPairs -- TODO: move elsewhere
401 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid
402 <$> PathNode <*> graphAPI uid -- TODO: mock
404 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid
405 <$> PathNode <*> treeAPI
408 -- :<|> (\corpus -> addWithQuery corpus :<|> addWithFile corpus)
409 :<|> addCorpusWithForm
410 :<|> addCorpusWithQuery
412 :<|> addAnnuaireWithForm
413 -- :<|> New.api uid -- TODO-SECURITY
414 -- :<|> New.info uid -- TODO-SECURITY
420 addUpload :: GargServer New.Upload
421 addUpload cId = (serveJobsAPI $ JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log)))
422 :<|> (serveJobsAPI $ JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log)))
425 addCorpusWithQuery :: GargServer New.AddWithQuery
426 addCorpusWithQuery cid =
428 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
430 addWithFile :: GargServer New.AddWithFile
431 addWithFile cid i f =
433 JobFunction (\_i log -> New.addToCorpusWithFile cid i f (liftIO . log))
435 addCorpusWithForm :: GargServer New.AddWithForm
436 addCorpusWithForm cid =
438 JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log))
440 addAnnuaireWithForm :: GargServer Annuaire.AddWithForm
441 addAnnuaireWithForm cid =
443 JobFunction (\i log -> Annuaire.addToAnnuaireWithForm cid i (liftIO . log))
446 serverStatic :: Server (Get '[HTML] Html)
448 let path = "purescript-gargantext/dist/index.html"
449 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
453 ---------------------------------------------------------------------
454 --gargMock :: Server GargAPI
455 --gargMock = mock apiGarg Proxy
456 ---------------------------------------------------------------------
457 makeApp :: EnvC env => env -> IO Application
458 makeApp env = serveWithContext api cfg <$> server env
460 cfg :: Servant.Context AuthContext
461 cfg = env ^. settings . jwtSettings
462 :. env ^. settings . cookieSettings
466 --appMock :: Application
467 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
468 ---------------------------------------------------------------------
472 apiGarg :: Proxy GargAPI
474 ---------------------------------------------------------------------
475 schemaUiServer :: (Server api ~ Handler Swagger)
476 => Swagger -> Server (SwaggerSchemaUI' dir api)
477 schemaUiServer = swaggerSchemaUIServer
479 -- Type Family for the Documentation
480 type family TypeName (x :: *) :: Symbol where
482 TypeName Text = "Text"
483 TypeName x = GenericTypeName x (Rep x ())
485 type family GenericTypeName t (r :: *) :: Symbol where
486 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
488 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
491 -- | Swagger Specifications
492 swaggerDoc :: Swagger
493 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
494 & info.title .~ "Gargantext"
495 & info.version .~ "0.0.1.3.1" -- TODO same version as Gargantext
496 -- & info.base_url ?~ (URL "http://gargantext.org/")
497 & info.description ?~ "REST API specifications"
498 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
499 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
500 ["Gargantext" & description ?~ "Main operations"]
501 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
503 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
505 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
506 swaggerWriteJSON :: IO ()
507 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
509 portRouteInfo :: PortNumber -> IO ()
510 portRouteInfo port = do
511 T.putStrLn " ----Main Routes----- "
512 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
513 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
515 stopGargantext :: HasRepoSaver env => env -> IO ()
516 stopGargantext env = do
517 T.putStrLn "----- Stopping gargantext -----"
518 runReaderT saveRepo env
520 -- | startGargantext takes as parameters port number and Ini file.
521 startGargantext :: PortNumber -> FilePath -> IO ()
522 startGargantext port file = do
523 env <- newEnv port file
526 mid <- makeDevMiddleware
527 run port (mid app) `finally` stopGargantext env
530 startGargantextMock :: PortNumber -> IO ()
531 startGargantextMock port = do
533 application <- makeMockApp . MockEnv $ FireWall False