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.Mock (mock)
77 --import Servant.Job.Server (WithCallbacks)
78 import Servant.Job.Async
79 import Servant.Swagger
80 import Servant.Swagger.UI
81 -- import Servant.API.Stream
83 --import Gargantext.API.Swagger
85 import Gargantext.Database.Node.Contact (HyperdataContact)
86 import Gargantext.API.Auth (AuthRequest, AuthResponse, AuthenticatedUser(..), AuthContext, auth, withAccess, PathId(..))
87 import Gargantext.API.Count ( CountAPI, count, Query)
88 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
89 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo, TableNgramsApi, apiNgramsTableDoc)
90 import Gargantext.API.Node
91 import Gargantext.API.Search (SearchPairsAPI, searchPairs)
92 import Gargantext.API.Types
93 import qualified Gargantext.API.Annuaire as Annuaire
94 import qualified Gargantext.API.Export as Export
95 import qualified Gargantext.API.Corpus.New as New
96 import Gargantext.Database.Types.Node
97 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
98 import Gargantext.Database.Utils (HasConnection)
99 import Gargantext.Prelude
100 import Gargantext.Viz.Graph.API
102 --import Gargantext.API.Orchestrator
103 import Gargantext.API.Orchestrator.Types
105 ---------------------------------------------------------------------
106 import GHC.Base (Applicative)
107 -- import Control.Lens
109 import Data.List (lookup)
110 import Data.Text.Encoding (encodeUtf8)
112 --import Network.Wai (Request, requestHeaders, responseLBS)
113 import Network.Wai (Request, requestHeaders)
114 --import qualified Network.Wai.Handler.Warp as Warp
115 import Network.Wai.Middleware.Cors
117 import Network.Wai.Middleware.RequestLogger
118 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
120 import Network.HTTP.Types hiding (Query)
122 import Gargantext.API.Settings
124 showAsServantErr :: GargError -> ServerError
125 showAsServantErr (GargServerError err) = err
126 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
128 fireWall :: Applicative f => Request -> FireWall -> f Bool
130 let origin = lookup "Origin" (requestHeaders req)
131 let host = lookup "Host" (requestHeaders req)
133 let hostOk = Just (encodeUtf8 "localhost:3000")
134 let originOk = Just (encodeUtf8 "http://localhost:8008")
136 if origin == originOk
138 || (not $ unFireWall fw)
144 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
145 makeMockApp :: MockEnv -> IO Application
147 let serverApp = appMock
149 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
150 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
151 let checkOriginAndHost app req resp = do
152 blocking <- fireWall req (env ^. menv_firewall)
155 False -> resp ( responseLBS status401 []
156 "Invalid Origin or Host header")
158 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
159 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
160 { corsOrigins = Nothing -- == /*
161 , corsMethods = [ methodGet , methodPost , methodPut
162 , methodDelete, methodOptions, methodHead]
163 , corsRequestHeaders = ["authorization", "content-type"]
164 , corsExposedHeaders = Nothing
165 , corsMaxAge = Just ( 60*60*24 ) -- one day
166 , corsVaryOrigin = False
167 , corsRequireOrigin = False
168 , corsIgnoreFailures = False
171 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
172 -- $ Warp.defaultSettings
174 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
175 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
179 makeDevMiddleware :: IO Middleware
180 makeDevMiddleware = do
182 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
183 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
184 -- let checkOriginAndHost app req resp = do
185 -- blocking <- fireWall req (env ^. menv_firewall)
187 -- True -> app req resp
188 -- False -> resp ( responseLBS status401 []
189 -- "Invalid Origin or Host header")
191 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
192 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
193 { corsOrigins = Nothing -- == /*
194 , corsMethods = [ methodGet , methodPost , methodPut
195 , methodDelete, methodOptions, methodHead]
196 , corsRequestHeaders = ["authorization", "content-type"]
197 , corsExposedHeaders = Nothing
198 , corsMaxAge = Just ( 60*60*24 ) -- one day
199 , corsVaryOrigin = False
200 , corsRequireOrigin = False
201 , corsIgnoreFailures = False
204 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
205 -- $ Warp.defaultSettings
207 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
208 pure $ logStdoutDev . corsMiddleware
210 ---------------------------------------------------------------------
213 -- | API for serving @swagger.json@
214 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
216 -- | API for serving main operational routes of @gargantext.org@
219 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
220 -- | TODO :<|> Summary "Latest API" :> GargAPI'
223 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
227 "auth" :> Summary "AUTH API"
228 :> ReqBody '[JSON] AuthRequest
229 :> Post '[JSON] AuthResponse
230 -- TODO-ACCESS here we want to request a particular header for
231 -- auth and capabilities.
234 type GargPrivateAPI = SA.Auth '[SA.JWT] AuthenticatedUser :> GargPrivateAPI'
238 = "user" :> Summary "First user endpoint"
240 :<|> "nodes" :> Summary "Nodes endpoint"
241 :> ReqBody '[JSON] [NodeId] :> NodesAPI
243 ----------------------------------------
245 type FibAPI = Get '[JSON] Int
247 fibAPI :: Int -> GargServer FibAPI
248 fibAPI n = pure (fib n)
249 ----------------------------------------
252 type GargPrivateAPI' =
256 :<|> "node" :> Summary "Node endpoint"
257 :> Capture "node_id" NodeId
258 :> NodeAPI HyperdataAny
261 :<|> "corpus":> Summary "Corpus endpoint"
262 :> Capture "corpus_id" CorpusId
263 :> NodeAPI HyperdataCorpus
265 :<|> "corpus":> Summary "Corpus endpoint"
266 :> Capture "node1_id" NodeId
268 :> Capture "node2_id" NodeId
269 :> NodeNodeAPI HyperdataAny
271 :<|> "corpus" :> Capture "node_id" CorpusId
275 :<|> "annuaire":> Summary "Annuaire endpoint"
276 :> Capture "annuaire_id" AnnuaireId
277 :> NodeAPI HyperdataAnnuaire
279 :<|> "annuaire" :> Summary "Contact endpoint"
280 :> Capture "annuaire_id" NodeId
281 :> "contact" :> Capture "contact_id" NodeId
282 :> NodeNodeAPI HyperdataContact
285 :<|> "document":> Summary "Document endpoint"
286 :> Capture "doc_id" DocId
287 :> "ngrams" :> TableNgramsApi
289 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
291 :<|> "count" :> Summary "Count endpoint"
292 :> ReqBody '[JSON] Query :> CountAPI
294 -- Corpus endpoint --> TODO rename s/search/filter/g
295 :<|> "search":> Capture "corpus" NodeId
298 -- TODO move to NodeAPI?
299 :<|> "graph" :> Summary "Graph endpoint"
300 :> Capture "graph_id" NodeId
303 -- TODO move to NodeAPI?
305 :<|> "tree" :> Summary "Tree endpoint"
306 :> Capture "tree_id" NodeId
311 :<|> New.AddWithQuery
313 :<|> Annuaire.AddWithForm
314 -- :<|> New.AddWithFile
315 -- :<|> "scraper" :> WithCallbacks ScraperAPI
316 -- :<|> "new" :> New.Api
317 :<|> "fib" :> Summary "Fib test"
319 :> FibAPI -- Get '[JSON] Int
325 -- :<|> "list" :> Capture "node_id" Int :> NodeAPI
326 -- :<|> "ngrams" :> Capture "node_id" Int :> NodeAPI
327 -- :<|> "auth" :> Capture "node_id" Int :> NodeAPI
328 ---------------------------------------------------------------------
330 type API = SwaggerAPI
334 -- This is the concrete monad. It needs to be used as little as possible,
335 -- instead, prefer GargServer, GargServerT, GargServerC.
336 type GargServerM env err = ReaderT env (ExceptT err IO)
342 , HasJobEnv env ScraperStatus ScraperStatus
345 ---------------------------------------------------------------------
346 -- | Server declarations
348 server :: forall env. EnvC env => env -> IO (Server API)
350 -- orchestrator <- scrapyOrchestrator env
351 pure $ schemaUiServer swaggerDoc
352 :<|> hoistServerWithContext (Proxy :: Proxy GargAPI) (Proxy :: Proxy AuthContext) transform serverGargAPI
355 transform :: forall a. GargServerM env GargError a -> Handler a
356 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
358 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
359 serverGargAPI -- orchestrator
360 = auth :<|> serverPrivateGargAPI
363 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
364 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
365 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
366 -- Here throwAll' requires a concrete type for the monad.
368 -- TODO-SECURITY admin only: withAdmin
369 -- Question: How do we mark admins?
370 serverGargAdminAPI :: GargServer GargAdminAPI
371 serverGargAdminAPI = roots
375 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
376 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
378 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
379 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
380 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
381 :<|> Export.getCorpus -- uid
382 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
383 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
385 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid
386 <$> PathNode <*> apiNgramsTableDoc
388 :<|> count -- TODO: undefined
390 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid
391 <$> PathNode <*> searchPairs -- TODO: move elsewhere
393 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid
394 <$> PathNode <*> graphAPI uid -- TODO: mock
396 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid
397 <$> PathNode <*> treeAPI
400 -- :<|> (\corpus -> addWithQuery corpus :<|> addWithFile corpus)
401 :<|> addCorpusWithForm
402 :<|> addCorpusWithQuery
404 :<|> addAnnuaireWithForm
405 -- :<|> New.api uid -- TODO-SECURITY
406 -- :<|> New.info uid -- TODO-SECURITY
411 addUpload :: GargServer New.Upload
412 addUpload cId = (serveJobsAPI $ JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log)))
413 :<|> (serveJobsAPI $ JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log)))
416 addCorpusWithQuery :: GargServer New.AddWithQuery
417 addCorpusWithQuery cid =
419 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
421 addWithFile :: GargServer New.AddWithFile
422 addWithFile cid i f =
424 JobFunction (\_i log -> New.addToCorpusWithFile cid i f (liftIO . log))
426 addCorpusWithForm :: GargServer New.AddWithForm
427 addCorpusWithForm cid =
429 JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log))
431 addAnnuaireWithForm :: GargServer Annuaire.AddWithForm
432 addAnnuaireWithForm cid =
434 JobFunction (\i log -> Annuaire.addToAnnuaireWithForm cid i (liftIO . log))
437 serverStatic :: Server (Get '[HTML] Html)
439 let path = "purescript-gargantext/dist/index.html"
440 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
444 ---------------------------------------------------------------------
445 --gargMock :: Server GargAPI
446 --gargMock = mock apiGarg Proxy
447 ---------------------------------------------------------------------
448 makeApp :: EnvC env => env -> IO Application
449 makeApp env = serveWithContext api cfg <$> server env
451 cfg :: Servant.Context AuthContext
452 cfg = env ^. settings . jwtSettings
453 :. env ^. settings . cookieSettings
457 --appMock :: Application
458 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
459 ---------------------------------------------------------------------
463 apiGarg :: Proxy GargAPI
465 ---------------------------------------------------------------------
466 schemaUiServer :: (Server api ~ Handler Swagger)
467 => Swagger -> Server (SwaggerSchemaUI' dir api)
468 schemaUiServer = swaggerSchemaUIServer
470 -- Type Family for the Documentation
471 type family TypeName (x :: *) :: Symbol where
473 TypeName Text = "Text"
474 TypeName x = GenericTypeName x (Rep x ())
476 type family GenericTypeName t (r :: *) :: Symbol where
477 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
479 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
482 -- | Swagger Specifications
483 swaggerDoc :: Swagger
484 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
485 & info.title .~ "Gargantext"
486 & info.version .~ "4.0.2" -- TODO same version as Gargantext
487 -- & info.base_url ?~ (URL "http://gargantext.org/")
488 & info.description ?~ "REST API specifications"
489 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
490 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
491 ["Gargantext" & description ?~ "Main operations"]
492 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
494 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
496 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
497 swaggerWriteJSON :: IO ()
498 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
500 portRouteInfo :: PortNumber -> IO ()
501 portRouteInfo port = do
502 T.putStrLn " ----Main Routes----- "
503 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
504 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
506 stopGargantext :: HasRepoSaver env => env -> IO ()
507 stopGargantext env = do
508 T.putStrLn "----- Stopping gargantext -----"
509 runReaderT saveRepo env
511 -- | startGargantext takes as parameters port number and Ini file.
512 startGargantext :: PortNumber -> FilePath -> IO ()
513 startGargantext port file = do
514 env <- newEnv port file
517 mid <- makeDevMiddleware
518 run port (mid app) `finally` stopGargantext env
521 startGargantextMock :: PortNumber -> IO ()
522 startGargantextMock port = do
524 application <- makeMockApp . MockEnv $ FireWall False