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
287 -- :<|> "scraper" :> WithCallbacks ScraperAPI
288 :<|> "new" :> New.Api
294 -- :<|> "list" :> Capture "node_id" Int :> NodeAPI
295 -- :<|> "ngrams" :> Capture "node_id" Int :> NodeAPI
296 -- :<|> "auth" :> Capture "node_id" Int :> NodeAPI
297 ---------------------------------------------------------------------
298 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
300 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
302 -- This is the concrete monad. It needs to be used as little as possible,
303 -- instead, prefer GargServer, GargServerT, GargServerC.
304 type GargServerM env err = ReaderT env (ExceptT err IO)
310 , HasJobEnv env ScraperStatus ScraperStatus
313 ---------------------------------------------------------------------
314 -- | Server declarations
316 server :: forall env. EnvC env => env -> IO (Server API)
318 -- orchestrator <- scrapyOrchestrator env
320 :<|> hoistServerWithContext (Proxy :: Proxy GargAPI) (Proxy :: Proxy AuthContext) transform serverGargAPI
323 transform :: forall a. GargServerM env GargError a -> Handler a
324 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
326 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
327 serverGargAPI -- orchestrator
328 = auth :<|> serverPrivateGargAPI
331 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
332 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
333 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
334 -- Here throwAll' requires a concrete type for the monad.
336 -- TODO-SECURITY admin only: withAdmin
337 -- Question: How do we mark admins?
338 serverGargAdminAPI :: GargServer GargAdminAPI
343 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
344 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
346 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
347 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
348 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
349 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
350 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
351 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid <$> PathNode <*> apiNgramsTableDoc
352 :<|> count -- TODO: undefined
353 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid <$> PathNode <*> searchPairs -- TODO: move elsewhere
354 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid <$> PathNode <*> graphAPI -- TODO: mock
355 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid <$> PathNode <*> treeAPI
357 :<|> New.api -- TODO-SECURITY
358 :<|> New.info uid -- TODO-SECURITY
360 addToCorpus :: GargServer New.API_v2
363 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
365 serverStatic :: Server (Get '[HTML] Html)
367 let path = "purescript-gargantext/dist/index.html"
368 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
372 ---------------------------------------------------------------------
373 swaggerFront :: Server SwaggerFrontAPI
374 swaggerFront = schemaUiServer swaggerDoc
377 --gargMock :: Server GargAPI
378 --gargMock = mock apiGarg Proxy
380 ---------------------------------------------------------------------
381 makeApp :: EnvC env => env -> IO Application
382 makeApp env = serveWithContext api cfg <$> server env
384 cfg :: Servant.Context AuthContext
385 cfg = env ^. settings . jwtSettings
386 :. env ^. settings . cookieSettings
390 --appMock :: Application
391 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
393 ---------------------------------------------------------------------
397 apiGarg :: Proxy GargAPI
399 ---------------------------------------------------------------------
401 schemaUiServer :: (Server api ~ Handler Swagger)
402 => Swagger -> Server (SwaggerSchemaUI' dir api)
403 schemaUiServer = swaggerSchemaUIServer
406 -- Type Family for the Documentation
407 type family TypeName (x :: *) :: Symbol where
409 TypeName Text = "Text"
410 TypeName x = GenericTypeName x (Rep x ())
412 type family GenericTypeName t (r :: *) :: Symbol where
413 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
415 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
418 -- | Swagger Specifications
419 swaggerDoc :: Swagger
420 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
421 & info.title .~ "Gargantext"
422 & info.version .~ "4.0.2" -- TODO same version as Gargantext
423 -- & info.base_url ?~ (URL "http://gargantext.org/")
424 & info.description ?~ "REST API specifications"
425 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
426 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
427 ["Gargantext" & description ?~ "Main operations"]
428 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
430 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
432 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
433 swaggerWriteJSON :: IO ()
434 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
436 portRouteInfo :: PortNumber -> IO ()
437 portRouteInfo port = do
438 T.putStrLn " ----Main Routes----- "
439 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
440 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
442 stopGargantext :: HasRepoSaver env => env -> IO ()
443 stopGargantext env = do
444 T.putStrLn "----- Stopping gargantext -----"
445 runReaderT saveRepo env
447 -- | startGargantext takes as parameters port number and Ini file.
448 startGargantext :: PortNumber -> FilePath -> IO ()
449 startGargantext port file = do
450 env <- newEnv port file
453 mid <- makeDevMiddleware
454 run port (mid app) `finally` stopGargantext env
457 startGargantextMock :: PortNumber -> IO ()
458 startGargantextMock port = do
460 application <- makeMockApp . MockEnv $ FireWall False