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)
11 Thanks @yannEsposito for our discussions at the beginning of this project :).
15 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
17 {-# LANGUAGE ConstraintKinds #-}
18 {-# LANGUAGE NoImplicitPrelude #-}
19 {-# LANGUAGE DataKinds #-}
20 {-# LANGUAGE DeriveGeneric #-}
21 {-# LANGUAGE FlexibleContexts #-}
22 {-# LANGUAGE FlexibleInstances #-}
23 {-# LANGUAGE OverloadedStrings #-}
24 {-# LANGUAGE TemplateHaskell #-}
25 {-# LANGUAGE TypeOperators #-}
26 {-# LANGUAGE KindSignatures #-}
27 {-# LANGUAGE RankNTypes #-}
28 {-# LANGUAGE ScopedTypeVariables #-}
29 {-# LANGUAGE TypeFamilies #-}
30 {-# LANGUAGE UndecidableInstances #-}
32 ---------------------------------------------------------------------
35 ---------------------------------------------------------------------
37 import System.IO (FilePath)
39 import GHC.Generics (D1, Meta (..), Rep)
40 import GHC.TypeLits (AppendSymbol, Symbol)
43 import Control.Exception (finally)
44 import Control.Monad.Except (withExceptT, ExceptT)
45 import Control.Monad.IO.Class (liftIO)
46 import Control.Monad.Reader (ReaderT, runReaderT)
47 import Data.Aeson.Encode.Pretty (encodePretty)
48 import qualified Data.ByteString.Lazy.Char8 as BL8
50 import Data.Text (Text)
51 import qualified Data.Text.IO as T
52 --import qualified Data.Set as Set
56 import Network.Wai.Handler.Warp hiding (defaultSettings)
59 import Servant.Auth as SA
60 import Servant.Auth.Server (AuthResult(..))
61 import Servant.Auth.Swagger ()
62 import Servant.HTML.Blaze (HTML)
63 --import Servant.Mock (mock)
64 --import Servant.Job.Server (WithCallbacks)
65 import Servant.Job.Async
66 import Servant.Static.TH.Internal.Server (fileTreeToServer)
67 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
68 import Servant.Swagger
69 import Servant.Swagger.UI
70 -- import Servant.API.Stream
71 import Text.Blaze.Html (Html)
73 --import Gargantext.API.Swagger
75 import Gargantext.Database.Node.Contact (HyperdataContact)
76 import Gargantext.API.Auth (AuthRequest, AuthResponse, AuthenticatedUser(..), AuthContext, auth, withAccess, PathId(..))
77 import Gargantext.API.Count ( CountAPI, count, Query)
78 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
79 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo, TableNgramsApi, apiNgramsTableDoc)
80 import Gargantext.API.Node
81 import Gargantext.API.Search (SearchPairsAPI, searchPairs)
82 import Gargantext.API.Types
83 import qualified Gargantext.API.Corpus.New as New
84 import Gargantext.Database.Types.Node
85 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
86 import Gargantext.Database.Utils (HasConnection)
87 import Gargantext.Prelude
88 import Gargantext.Viz.Graph.API
90 --import Gargantext.API.Orchestrator
91 import Gargantext.API.Orchestrator.Types
93 ---------------------------------------------------------------------
95 import GHC.Base (Applicative)
96 -- import Control.Lens
98 import Data.List (lookup)
99 import Data.Text.Encoding (encodeUtf8)
101 --import Network.Wai (Request, requestHeaders, responseLBS)
102 import Network.Wai (Request, requestHeaders)
103 --import qualified Network.Wai.Handler.Warp as Warp
104 import Network.Wai.Middleware.Cors
106 import Network.Wai.Middleware.RequestLogger
107 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
109 import Network.HTTP.Types hiding (Query)
111 import Gargantext.API.Settings
113 showAsServantErr :: GargError -> ServerError
114 showAsServantErr (GargServerError err) = err
115 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
117 fireWall :: Applicative f => Request -> FireWall -> f Bool
119 let origin = lookup "Origin" (requestHeaders req)
120 let host = lookup "Host" (requestHeaders req)
122 let hostOk = Just (encodeUtf8 "localhost:3000")
123 let originOk = Just (encodeUtf8 "http://localhost:8008")
125 if origin == originOk
127 || (not $ unFireWall fw)
133 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
134 makeMockApp :: MockEnv -> IO Application
136 let serverApp = appMock
138 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
139 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
140 let checkOriginAndHost app req resp = do
141 blocking <- fireWall req (env ^. menv_firewall)
144 False -> resp ( responseLBS status401 []
145 "Invalid Origin or Host header")
147 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
148 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
149 { corsOrigins = Nothing -- == /*
150 , corsMethods = [ methodGet , methodPost , methodPut
151 , methodDelete, methodOptions, methodHead]
152 , corsRequestHeaders = ["authorization", "content-type"]
153 , corsExposedHeaders = Nothing
154 , corsMaxAge = Just ( 60*60*24 ) -- one day
155 , corsVaryOrigin = False
156 , corsRequireOrigin = False
157 , corsIgnoreFailures = False
160 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
161 -- $ Warp.defaultSettings
163 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
164 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
168 makeDevMiddleware :: IO Middleware
169 makeDevMiddleware = do
171 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
172 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
173 -- let checkOriginAndHost app req resp = do
174 -- blocking <- fireWall req (env ^. menv_firewall)
176 -- True -> app req resp
177 -- False -> resp ( responseLBS status401 []
178 -- "Invalid Origin or Host header")
180 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
181 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
182 { corsOrigins = Nothing -- == /*
183 , corsMethods = [ methodGet , methodPost , methodPut
184 , methodDelete, methodOptions, methodHead]
185 , corsRequestHeaders = ["authorization", "content-type"]
186 , corsExposedHeaders = Nothing
187 , corsMaxAge = Just ( 60*60*24 ) -- one day
188 , corsVaryOrigin = False
189 , corsRequireOrigin = False
190 , corsIgnoreFailures = False
193 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
194 -- $ Warp.defaultSettings
196 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
197 pure $ logStdoutDev . corsMiddleware
199 ---------------------------------------------------------------------
202 -- | API for serving @swagger.json@
203 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
205 -- | API for serving main operational routes of @gargantext.org@
208 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
209 -- | TODO :<|> Summary "Latest API" :> GargAPI'
212 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
216 "auth" :> Summary "AUTH API"
217 :> ReqBody '[JSON] AuthRequest
218 :> Post '[JSON] AuthResponse
219 -- TODO-ACCESS here we want to request a particular header for
220 -- auth and capabilities.
223 type GargPrivateAPI = SA.Auth '[SA.JWT] AuthenticatedUser :> GargPrivateAPI'
227 = "user" :> Summary "First user endpoint"
229 :<|> "nodes" :> Summary "Nodes endpoint"
230 :> ReqBody '[JSON] [NodeId] :> NodesAPI
232 type GargPrivateAPI' =
236 :<|> "node" :> Summary "Node endpoint"
237 :> Capture "node_id" NodeId
238 :> NodeAPI HyperdataAny
241 :<|> "corpus":> Summary "Corpus endpoint"
242 :> Capture "corpus_id" CorpusId
243 :> NodeAPI HyperdataCorpus
245 :<|> "corpus":> Summary "Corpus endpoint"
246 :> Capture "node1_id" NodeId
248 :> Capture "node2_id" NodeId
249 :> NodeNodeAPI HyperdataAny
252 :<|> "annuaire":> Summary "Annuaire endpoint"
253 :> Capture "annuaire_id" AnnuaireId
254 :> NodeAPI HyperdataAnnuaire
256 :<|> "annuaire" :> Summary "Contact endpoint"
257 :> Capture "annuaire_id" NodeId
258 :> "contact" :> Capture "contact_id" NodeId
259 :> NodeNodeAPI HyperdataContact
262 :<|> "document":> Summary "Document endpoint"
263 :> Capture "doc_id" DocId
264 :> "ngrams" :> TableNgramsApi
266 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
268 :<|> "count" :> Summary "Count endpoint"
269 :> ReqBody '[JSON] Query :> CountAPI
271 -- Corpus endpoint --> TODO rename s/search/filter/g
272 :<|> "search":> Capture "corpus" NodeId
275 -- TODO move to NodeAPI?
276 :<|> "graph" :> Summary "Graph endpoint"
277 :> Capture "graph_id" NodeId
280 -- TODO move to NodeAPI?
282 :<|> "tree" :> Summary "Tree endpoint"
283 :> Capture "tree_id" NodeId
286 :<|> New.AddWithQuery
289 -- :<|> "scraper" :> WithCallbacks ScraperAPI
290 -- :<|> "new" :> New.Api
296 -- :<|> "list" :> Capture "node_id" Int :> NodeAPI
297 -- :<|> "ngrams" :> Capture "node_id" Int :> NodeAPI
298 -- :<|> "auth" :> Capture "node_id" Int :> NodeAPI
299 ---------------------------------------------------------------------
300 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
302 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
304 -- This is the concrete monad. It needs to be used as little as possible,
305 -- instead, prefer GargServer, GargServerT, GargServerC.
306 type GargServerM env err = ReaderT env (ExceptT err IO)
312 , HasJobEnv env ScraperStatus ScraperStatus
315 ---------------------------------------------------------------------
316 -- | Server declarations
318 server :: forall env. EnvC env => env -> IO (Server API)
320 -- orchestrator <- scrapyOrchestrator env
322 :<|> hoistServerWithContext (Proxy :: Proxy GargAPI) (Proxy :: Proxy AuthContext) transform serverGargAPI
325 transform :: forall a. GargServerM env GargError a -> Handler a
326 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
328 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
329 serverGargAPI -- orchestrator
330 = auth :<|> serverPrivateGargAPI
333 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
334 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
335 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
336 -- Here throwAll' requires a concrete type for the monad.
338 -- TODO-SECURITY admin only: withAdmin
339 -- Question: How do we mark admins?
340 serverGargAdminAPI :: GargServer GargAdminAPI
345 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
346 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
348 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
349 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
350 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
351 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
352 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
354 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid
355 <$> PathNode <*> apiNgramsTableDoc
357 :<|> count -- TODO: undefined
359 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid
360 <$> PathNode <*> searchPairs -- TODO: move elsewhere
362 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid
363 <$> PathNode <*> graphAPI uid -- TODO: mock
365 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid
366 <$> PathNode <*> treeAPI
372 -- :<|> New.api uid -- TODO-SECURITY
373 -- :<|> New.info uid -- TODO-SECURITY
375 addWithQuery :: GargServer New.AddWithQuery
378 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
380 addWithFile :: GargServer New.AddWithFile
381 addWithFile cid i f =
383 JobFunction (\_i log -> New.addToCorpusWithFile cid i f (liftIO . log))
385 addWithForm :: GargServer New.AddWithForm
388 JobFunction (\_i log -> New.addToCorpusWithForm cid f (liftIO . log))
390 serverStatic :: Server (Get '[HTML] Html)
392 let path = "purescript-gargantext/dist/index.html"
393 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
397 ---------------------------------------------------------------------
398 swaggerFront :: Server SwaggerFrontAPI
399 swaggerFront = schemaUiServer swaggerDoc
402 --gargMock :: Server GargAPI
403 --gargMock = mock apiGarg Proxy
405 ---------------------------------------------------------------------
406 makeApp :: EnvC env => env -> IO Application
407 makeApp env = serveWithContext api cfg <$> server env
409 cfg :: Servant.Context AuthContext
410 cfg = env ^. settings . jwtSettings
411 :. env ^. settings . cookieSettings
415 --appMock :: Application
416 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
418 ---------------------------------------------------------------------
422 apiGarg :: Proxy GargAPI
424 ---------------------------------------------------------------------
426 schemaUiServer :: (Server api ~ Handler Swagger)
427 => Swagger -> Server (SwaggerSchemaUI' dir api)
428 schemaUiServer = swaggerSchemaUIServer
431 -- Type Family for the Documentation
432 type family TypeName (x :: *) :: Symbol where
434 TypeName Text = "Text"
435 TypeName x = GenericTypeName x (Rep x ())
437 type family GenericTypeName t (r :: *) :: Symbol where
438 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
440 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
443 -- | Swagger Specifications
444 swaggerDoc :: Swagger
445 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
446 & info.title .~ "Gargantext"
447 & info.version .~ "4.0.2" -- TODO same version as Gargantext
448 -- & info.base_url ?~ (URL "http://gargantext.org/")
449 & info.description ?~ "REST API specifications"
450 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
451 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
452 ["Gargantext" & description ?~ "Main operations"]
453 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
455 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
457 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
458 swaggerWriteJSON :: IO ()
459 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
461 portRouteInfo :: PortNumber -> IO ()
462 portRouteInfo port = do
463 T.putStrLn " ----Main Routes----- "
464 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
465 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
467 stopGargantext :: HasRepoSaver env => env -> IO ()
468 stopGargantext env = do
469 T.putStrLn "----- Stopping gargantext -----"
470 runReaderT saveRepo env
472 -- | startGargantext takes as parameters port number and Ini file.
473 startGargantext :: PortNumber -> FilePath -> IO ()
474 startGargantext port file = do
475 env <- newEnv port file
478 mid <- makeDevMiddleware
479 run port (mid app) `finally` stopGargantext env
482 startGargantextMock :: PortNumber -> IO ()
483 startGargantextMock port = do
485 application <- makeMockApp . MockEnv $ FireWall False