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 "id" NodeId :> NodeAPI HyperdataAny
240 :<|> "corpus":> Summary "Corpus endpoint"
241 :> Capture "id" CorpusId :> NodeAPI HyperdataCorpus
244 :<|> "corpus":> Summary "Corpus endpoint"
245 :> Capture "node1_id" NodeId
247 :> Capture "node2_id" NodeId
248 :> NodeNodeAPI HyperdataAny
251 :<|> "annuaire":> Summary "Annuaire endpoint"
252 :> Capture "id" AnnuaireId :> NodeAPI HyperdataAnnuaire
254 :<|> "annuaire" :> Summary "Contact endpoint"
255 :> Capture "annuaire_id" NodeId
256 :> "contact" :> Capture "contact_id" NodeId
257 :> NodeNodeAPI HyperdataContact
260 :<|> "document":> Summary "Document endpoint"
261 :> Capture "id" DocId :> "ngrams" :> TableNgramsApi
263 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
265 :<|> "count" :> Summary "Count endpoint"
266 :> ReqBody '[JSON] Query :> CountAPI
268 -- Corpus endpoint --> TODO rename s/search/filter/g
269 :<|> "search":> Capture "corpus" NodeId
272 -- TODO move to NodeAPI?
273 :<|> "graph" :> Summary "Graph endpoint"
274 :> Capture "id" NodeId :> GraphAPI
276 -- TODO move to NodeAPI?
278 :<|> "tree" :> Summary "Tree endpoint"
279 :> Capture "id" NodeId :> TreeAPI
282 -- :<|> "scraper" :> WithCallbacks ScraperAPI
283 :<|> "new" :> New.Api
289 -- :<|> "list" :> Capture "id" Int :> NodeAPI
290 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
291 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
292 ---------------------------------------------------------------------
293 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
295 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
297 -- This is the concrete monad. It needs to be used as little as possible,
298 -- instead, prefer GargServer, GargServerT, GargServerC.
299 type GargServerM env err = ReaderT env (ExceptT err IO)
305 , HasJobEnv env ScraperStatus ScraperStatus
308 ---------------------------------------------------------------------
309 -- | Server declarations
311 server :: forall env. EnvC env => env -> IO (Server API)
313 -- orchestrator <- scrapyOrchestrator env
315 :<|> hoistServerWithContext (Proxy :: Proxy GargAPI) (Proxy :: Proxy AuthContext) transform serverGargAPI
318 transform :: forall a. GargServerM env GargError a -> Handler a
319 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
321 serverGargAPI :: GargServerT env err (GargServerM env err) GargAPI
322 serverGargAPI -- orchestrator
323 = auth :<|> serverPrivateGargAPI
326 serverPrivateGargAPI :: GargServerT env err (GargServerM env err) GargPrivateAPI
327 serverPrivateGargAPI (Authenticated auser) = serverPrivateGargAPI' auser
328 serverPrivateGargAPI _ = throwAll' (_ServerError # err401)
329 -- Here throwAll' requires a concrete type for the monad.
331 -- TODO-SECURITY admin only: withAdmin
332 -- Question: How do we mark admins?
333 serverGargAdminAPI :: GargServer GargAdminAPI
338 serverPrivateGargAPI' :: AuthenticatedUser -> GargServer GargPrivateAPI'
339 serverPrivateGargAPI' (AuthenticatedUser (NodeId uid))
341 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) uid
342 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) uid
343 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataAny) uid
344 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) uid
345 :<|> nodeNodeAPI (Proxy :: Proxy HyperdataContact) uid
346 :<|> withAccess (Proxy :: Proxy TableNgramsApi) Proxy uid <$> PathNode <*> apiNgramsTableDoc
347 :<|> count -- TODO: undefined
348 :<|> withAccess (Proxy :: Proxy SearchPairsAPI) Proxy uid <$> PathNode <*> searchPairs -- TODO: move elsewhere
349 :<|> withAccess (Proxy :: Proxy GraphAPI) Proxy uid <$> PathNode <*> graphAPI -- TODO: mock
350 :<|> withAccess (Proxy :: Proxy TreeAPI) Proxy uid <$> PathNode <*> treeAPI
352 :<|> New.api -- TODO-SECURITY
353 :<|> New.info uid -- TODO-SECURITY
355 addToCorpus :: GargServer New.API_v2
358 JobFunction (\i log -> New.addToCorpusJobFunction cid i (liftIO . log))
360 serverStatic :: Server (Get '[HTML] Html)
362 let path = "purescript-gargantext/dist/index.html"
363 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
367 ---------------------------------------------------------------------
368 swaggerFront :: Server SwaggerFrontAPI
369 swaggerFront = schemaUiServer swaggerDoc
372 --gargMock :: Server GargAPI
373 --gargMock = mock apiGarg Proxy
375 ---------------------------------------------------------------------
376 makeApp :: EnvC env => env -> IO Application
377 makeApp env = serveWithContext api cfg <$> server env
379 cfg :: Servant.Context AuthContext
380 cfg = env ^. settings . jwtSettings
381 :. env ^. settings . cookieSettings
385 --appMock :: Application
386 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
388 ---------------------------------------------------------------------
392 apiGarg :: Proxy GargAPI
394 ---------------------------------------------------------------------
396 schemaUiServer :: (Server api ~ Handler Swagger)
397 => Swagger -> Server (SwaggerSchemaUI' dir api)
398 schemaUiServer = swaggerSchemaUIServer
401 -- Type Family for the Documentation
402 type family TypeName (x :: *) :: Symbol where
404 TypeName Text = "Text"
405 TypeName x = GenericTypeName x (Rep x ())
407 type family GenericTypeName t (r :: *) :: Symbol where
408 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
410 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
413 -- | Swagger Specifications
414 swaggerDoc :: Swagger
415 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
416 & info.title .~ "Gargantext"
417 & info.version .~ "4.0.2" -- TODO same version as Gargantext
418 -- & info.base_url ?~ (URL "http://gargantext.org/")
419 & info.description ?~ "REST API specifications"
420 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
421 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
422 ["Gargantext" & description ?~ "Main operations"]
423 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
425 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
427 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
428 swaggerWriteJSON :: IO ()
429 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
431 portRouteInfo :: PortNumber -> IO ()
432 portRouteInfo port = do
433 T.putStrLn " ----Main Routes----- "
434 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
435 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
437 stopGargantext :: HasRepoSaver env => env -> IO ()
438 stopGargantext env = do
439 T.putStrLn "----- Stopping gargantext -----"
440 runReaderT saveRepo env
442 -- | startGargantext takes as parameters port number and Ini file.
443 startGargantext :: PortNumber -> FilePath -> IO ()
444 startGargantext port file = do
445 env <- newEnv port file
448 mid <- makeDevMiddleware
449 run port (mid app) `finally` stopGargantext env
452 startGargantextMock :: PortNumber -> IO ()
453 startGargantextMock port = do
455 application <- makeMockApp . MockEnv $ FireWall False