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:
19 - Database connection (long term)
20 - In Memory stack management (short term)
23 Thanks to @yannEsposito (at the start) and @np (after).
27 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
29 {-# LANGUAGE ConstraintKinds #-}
30 {-# LANGUAGE NoImplicitPrelude #-}
31 {-# LANGUAGE DataKinds #-}
32 {-# LANGUAGE DeriveGeneric #-}
33 {-# LANGUAGE FlexibleContexts #-}
34 {-# LANGUAGE FlexibleInstances #-}
35 {-# LANGUAGE OverloadedStrings #-}
36 {-# LANGUAGE TemplateHaskell #-}
37 {-# LANGUAGE TypeOperators #-}
38 {-# LANGUAGE KindSignatures #-}
39 {-# LANGUAGE RankNTypes #-}
40 {-# LANGUAGE ScopedTypeVariables #-}
41 {-# LANGUAGE TypeFamilies #-}
42 {-# LANGUAGE UndecidableInstances #-}
44 ---------------------------------------------------------------------
47 ---------------------------------------------------------------------
49 import System.IO (FilePath)
51 import GHC.Generics (D1, Meta (..), Rep)
52 import GHC.TypeLits (AppendSymbol, Symbol)
55 import Control.Exception (finally)
56 import Control.Monad.Except (withExceptT, ExceptT)
57 import Control.Monad.IO.Class (liftIO)
58 import Control.Monad.Reader (ReaderT, runReaderT)
59 import Data.Aeson.Encode.Pretty (encodePretty)
60 import qualified Data.ByteString.Lazy.Char8 as BL8
62 import Data.Text (Text)
63 import qualified Data.Text.IO as T
64 --import qualified Data.Set as Set
68 import Network.Wai.Handler.Warp hiding (defaultSettings)
71 import Servant.Auth as SA
72 import Servant.Auth.Server (AuthResult(..))
73 import Servant.Auth.Swagger ()
74 import Servant.HTML.Blaze (HTML)
75 --import Servant.Mock (mock)
76 --import Servant.Job.Server (WithCallbacks)
77 import Servant.Job.Async
78 import Servant.Static.TH.Internal.Server (fileTreeToServer)
79 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
80 import Servant.Swagger
81 import Servant.Swagger.UI
82 -- import Servant.API.Stream
83 import Text.Blaze.Html (Html)
85 --import Gargantext.API.Swagger
87 import Gargantext.Database.Node.Contact (HyperdataContact)
88 import Gargantext.API.Auth (AuthRequest, AuthResponse, AuthenticatedUser(..), AuthContext, auth, withAccess, PathId(..))
89 import Gargantext.API.Count ( CountAPI, count, Query)
90 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
91 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo, TableNgramsApi, apiNgramsTableDoc)
92 import Gargantext.API.Node
93 import Gargantext.API.Search (SearchPairsAPI, searchPairs)
94 import Gargantext.API.Types
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 ---------------------------------------------------------------------
107 import GHC.Base (Applicative)
108 -- import Control.Lens
110 import Data.List (lookup)
111 import Data.Text.Encoding (encodeUtf8)
113 --import Network.Wai (Request, requestHeaders, responseLBS)
114 import Network.Wai (Request, requestHeaders)
115 --import qualified Network.Wai.Handler.Warp as Warp
116 import Network.Wai.Middleware.Cors
118 import Network.Wai.Middleware.RequestLogger
119 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
121 import Network.HTTP.Types hiding (Query)
123 import Gargantext.API.Settings
125 showAsServantErr :: GargError -> ServerError
126 showAsServantErr (GargServerError err) = err
127 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
129 fireWall :: Applicative f => Request -> FireWall -> f Bool
131 let origin = lookup "Origin" (requestHeaders req)
132 let host = lookup "Host" (requestHeaders req)
134 let hostOk = Just (encodeUtf8 "localhost:3000")
135 let originOk = Just (encodeUtf8 "http://localhost:8008")
137 if origin == originOk
139 || (not $ unFireWall fw)
145 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
146 makeMockApp :: MockEnv -> IO Application
148 let serverApp = appMock
150 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
151 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
152 let checkOriginAndHost app req resp = do
153 blocking <- fireWall req (env ^. menv_firewall)
156 False -> resp ( responseLBS status401 []
157 "Invalid Origin or Host header")
159 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
160 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
161 { corsOrigins = Nothing -- == /*
162 , corsMethods = [ methodGet , methodPost , methodPut
163 , methodDelete, methodOptions, methodHead]
164 , corsRequestHeaders = ["authorization", "content-type"]
165 , corsExposedHeaders = Nothing
166 , corsMaxAge = Just ( 60*60*24 ) -- one day
167 , corsVaryOrigin = False
168 , corsRequireOrigin = False
169 , corsIgnoreFailures = False
172 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
173 -- $ Warp.defaultSettings
175 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
176 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
180 makeDevMiddleware :: IO Middleware
181 makeDevMiddleware = do
183 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
184 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
185 -- let checkOriginAndHost app req resp = do
186 -- blocking <- fireWall req (env ^. menv_firewall)
188 -- True -> app req resp
189 -- False -> resp ( responseLBS status401 []
190 -- "Invalid Origin or Host header")
192 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
193 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
194 { corsOrigins = Nothing -- == /*
195 , corsMethods = [ methodGet , methodPost , methodPut
196 , methodDelete, methodOptions, methodHead]
197 , corsRequestHeaders = ["authorization", "content-type"]
198 , corsExposedHeaders = Nothing
199 , corsMaxAge = Just ( 60*60*24 ) -- one day
200 , corsVaryOrigin = False
201 , corsRequireOrigin = False
202 , corsIgnoreFailures = False
205 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
206 -- $ Warp.defaultSettings
208 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
209 pure $ logStdoutDev . corsMiddleware
211 ---------------------------------------------------------------------
214 -- | API for serving @swagger.json@
215 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
217 -- | API for serving main operational routes of @gargantext.org@
220 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
221 -- | TODO :<|> Summary "Latest API" :> GargAPI'
224 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
228 "auth" :> Summary "AUTH API"
229 :> ReqBody '[JSON] AuthRequest
230 :> Post '[JSON] AuthResponse
231 -- TODO-ACCESS here we want to request a particular header for
232 -- auth and capabilities.
235 type GargPrivateAPI = SA.Auth '[SA.JWT] AuthenticatedUser :> GargPrivateAPI'
239 = "user" :> Summary "First user endpoint"
241 :<|> "nodes" :> Summary "Nodes endpoint"
242 :> ReqBody '[JSON] [NodeId] :> NodesAPI
244 type GargPrivateAPI' =
248 :<|> "node" :> Summary "Node endpoint"
249 :> Capture "node_id" NodeId
250 :> NodeAPI HyperdataAny
253 :<|> "corpus":> Summary "Corpus endpoint"
254 :> Capture "corpus_id" CorpusId
255 :> NodeAPI HyperdataCorpus
257 :<|> "corpus":> Summary "Corpus endpoint"
258 :> Capture "node1_id" NodeId
260 :> Capture "node2_id" NodeId
261 :> NodeNodeAPI HyperdataAny
264 :<|> "annuaire":> Summary "Annuaire endpoint"
265 :> Capture "annuaire_id" AnnuaireId
266 :> NodeAPI HyperdataAnnuaire
268 :<|> "annuaire" :> Summary "Contact endpoint"
269 :> Capture "annuaire_id" NodeId
270 :> "contact" :> Capture "contact_id" NodeId
271 :> NodeNodeAPI HyperdataContact
274 :<|> "document":> Summary "Document endpoint"
275 :> Capture "doc_id" DocId
276 :> "ngrams" :> TableNgramsApi
278 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
280 :<|> "count" :> Summary "Count endpoint"
281 :> ReqBody '[JSON] Query :> CountAPI
283 -- Corpus endpoint --> TODO rename s/search/filter/g
284 :<|> "search":> Capture "corpus" NodeId
287 -- TODO move to NodeAPI?
288 :<|> "graph" :> Summary "Graph endpoint"
289 :> Capture "graph_id" NodeId
292 -- TODO move to NodeAPI?
294 :<|> "tree" :> Summary "Tree endpoint"
295 :> Capture "tree_id" NodeId
298 :<|> New.AddWithQuery
301 -- :<|> "scraper" :> WithCallbacks ScraperAPI
302 -- :<|> "new" :> New.Api
308 -- :<|> "list" :> Capture "node_id" Int :> NodeAPI
309 -- :<|> "ngrams" :> Capture "node_id" Int :> NodeAPI
310 -- :<|> "auth" :> Capture "node_id" Int :> NodeAPI
311 ---------------------------------------------------------------------
312 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
314 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
316 -- This is the concrete monad. It needs to be used as little as possible,
317 -- instead, prefer GargServer, GargServerT, GargServerC.
318 type GargServerM env err = ReaderT env (ExceptT err IO)
324 , HasJobEnv env ScraperStatus ScraperStatus
327 ---------------------------------------------------------------------
328 -- | Server declarations
330 server :: forall env. EnvC env => env -> IO (Server API)
332 -- orchestrator <- scrapyOrchestrator env
334 :<|> hoistServerWithContext (Proxy :: Proxy GargAPI) (Proxy :: Proxy AuthContext) transform serverGargAPI
337 transform :: forall a. GargServerM env GargError a -> Handler a
338 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
340 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
341 serverGargAPI -- orchestrator
342 = auth :<|> serverPrivateGargAPI
345 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
346 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
347 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
348 -- Here throwAll' requires a concrete type for the monad.
350 -- TODO-SECURITY admin only: withAdmin
351 -- Question: How do we mark admins?
352 serverGargAdminAPI :: GargServer GargAdminAPI
357 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
358 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
360 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
361 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
362 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
363 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
364 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
366 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid
367 <$> PathNode <*> apiNgramsTableDoc
369 :<|> count -- TODO: undefined
371 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid
372 <$> PathNode <*> searchPairs -- TODO: move elsewhere
374 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid
375 <$> PathNode <*> graphAPI uid -- TODO: mock
377 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid
378 <$> PathNode <*> treeAPI
384 -- :<|> New.api uid -- TODO-SECURITY
385 -- :<|> New.info uid -- TODO-SECURITY
387 addWithQuery :: GargServer New.AddWithQuery
390 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
392 addWithFile :: GargServer New.AddWithFile
393 addWithFile cid i f =
395 JobFunction (\_i log -> New.addToCorpusWithFile cid i f (liftIO . log))
397 addWithForm :: GargServer New.AddWithForm
400 JobFunction (\i log -> New.addToCorpusWithForm cid i (liftIO . log))
402 serverStatic :: Server (Get '[HTML] Html)
404 let path = "purescript-gargantext/dist/index.html"
405 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
409 ---------------------------------------------------------------------
410 swaggerFront :: Server SwaggerFrontAPI
411 swaggerFront = schemaUiServer swaggerDoc
414 --gargMock :: Server GargAPI
415 --gargMock = mock apiGarg Proxy
417 ---------------------------------------------------------------------
418 makeApp :: EnvC env => env -> IO Application
419 makeApp env = serveWithContext api cfg <$> server env
421 cfg :: Servant.Context AuthContext
422 cfg = env ^. settings . jwtSettings
423 :. env ^. settings . cookieSettings
427 --appMock :: Application
428 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
430 ---------------------------------------------------------------------
434 apiGarg :: Proxy GargAPI
436 ---------------------------------------------------------------------
438 schemaUiServer :: (Server api ~ Handler Swagger)
439 => Swagger -> Server (SwaggerSchemaUI' dir api)
440 schemaUiServer = swaggerSchemaUIServer
443 -- Type Family for the Documentation
444 type family TypeName (x :: *) :: Symbol where
446 TypeName Text = "Text"
447 TypeName x = GenericTypeName x (Rep x ())
449 type family GenericTypeName t (r :: *) :: Symbol where
450 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
452 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
455 -- | Swagger Specifications
456 swaggerDoc :: Swagger
457 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
458 & info.title .~ "Gargantext"
459 & info.version .~ "4.0.2" -- TODO same version as Gargantext
460 -- & info.base_url ?~ (URL "http://gargantext.org/")
461 & info.description ?~ "REST API specifications"
462 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
463 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
464 ["Gargantext" & description ?~ "Main operations"]
465 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
467 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
469 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
470 swaggerWriteJSON :: IO ()
471 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
473 portRouteInfo :: PortNumber -> IO ()
474 portRouteInfo port = do
475 T.putStrLn " ----Main Routes----- "
476 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
477 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
479 stopGargantext :: HasRepoSaver env => env -> IO ()
480 stopGargantext env = do
481 T.putStrLn "----- Stopping gargantext -----"
482 runReaderT saveRepo env
484 -- | startGargantext takes as parameters port number and Ini file.
485 startGargantext :: PortNumber -> FilePath -> IO ()
486 startGargantext port file = do
487 env <- newEnv port file
490 mid <- makeDevMiddleware
491 run port (mid app) `finally` stopGargantext env
494 startGargantextMock :: PortNumber -> IO ()
495 startGargantextMock port = do
497 application <- makeMockApp . MockEnv $ FireWall False