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)
12 TODO App type, the main monad in which the bot code is written with.
14 Provide config, state, logs and IO
15 type App m a = ( MonadState AppState m
17 , MonadLog (WithSeverity Doc) m
19 Thanks @yannEsposito for this.
22 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
24 {-# LANGUAGE NoImplicitPrelude #-}
25 {-# LANGUAGE DataKinds #-}
26 {-# LANGUAGE DeriveGeneric #-}
27 {-# LANGUAGE FlexibleContexts #-}
28 {-# LANGUAGE FlexibleInstances #-}
29 {-# LANGUAGE OverloadedStrings #-}
30 {-# LANGUAGE TemplateHaskell #-}
31 {-# LANGUAGE TypeOperators #-}
32 {-# LANGUAGE KindSignatures #-}
33 {-# LANGUAGE RankNTypes #-}
34 {-# LANGUAGE ScopedTypeVariables #-}
35 {-# LANGUAGE TypeFamilies #-}
36 {-# LANGUAGE UndecidableInstances #-}
38 ---------------------------------------------------------------------
41 ---------------------------------------------------------------------
43 import System.IO (FilePath)
45 import GHC.Generics (D1, Meta (..), Rep)
46 import GHC.TypeLits (AppendSymbol, Symbol)
49 import Control.Exception (finally)
50 import Control.Monad.Except (withExceptT, ExceptT)
51 import Control.Monad.IO.Class (liftIO)
52 import Control.Monad.Reader (ReaderT, runReaderT)
53 import Data.Aeson.Encode.Pretty (encodePretty)
54 import qualified Data.ByteString.Lazy.Char8 as BL8
56 import Data.Text (Text)
57 import qualified Data.Text.IO as T
58 --import qualified Data.Set as Set
62 import Network.Wai.Handler.Warp hiding (defaultSettings)
65 import Servant.HTML.Blaze (HTML)
66 import Servant.Mock (mock)
67 --import Servant.Job.Server (WithCallbacks)
68 import Servant.Static.TH.Internal.Server (fileTreeToServer)
69 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
70 import Servant.Swagger
71 import Servant.Swagger.UI
72 -- import Servant.API.Stream
73 import Text.Blaze.Html (Html)
75 --import Gargantext.API.Swagger
76 import Gargantext.Prelude
77 import Gargantext.Core.Types (HasInvalidError(..))
78 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
80 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth)
81 import Gargantext.API.Ngrams (HasRepo(..), HasRepoSaver(..), saveRepo)
82 import Gargantext.API.Types
83 import Gargantext.API.Node
84 import Gargantext.Database.Schema.Node (HasNodeError(..), NodeError)
85 --import Gargantext.Database.Node.Contact (HyperdataContact)
86 import Gargantext.Database.Types.Node
87 import Gargantext.Database.Utils (HasConnection)
88 import Gargantext.Database.Tree (HasTreeError(..), TreeError)
89 import Gargantext.Database.Types.Node (NodeId, CorpusId, AnnuaireId)
90 import Gargantext.API.Count ( CountAPI, count, Query)
91 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
92 import Gargantext.Database.Facet
93 import Gargantext.Viz.Graph.API
95 --import Gargantext.API.Orchestrator
96 --import Gargantext.API.Orchestrator.Types
98 ---------------------------------------------------------------------
100 import GHC.Base (Applicative)
101 -- import Control.Lens
103 import Data.List (lookup)
104 import Data.Text.Encoding (encodeUtf8)
106 --import Network.Wai (Request, requestHeaders, responseLBS)
107 import Network.Wai (Request, requestHeaders)
108 --import qualified Network.Wai.Handler.Warp as Warp
109 import Network.Wai.Middleware.Cors
111 import Network.Wai.Middleware.RequestLogger
112 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
114 import Network.HTTP.Types hiding (Query)
117 import Gargantext.API.Settings
120 = GargNodeError NodeError
121 | GargTreeError TreeError
122 | GargInvalidError Validation
125 makePrisms ''GargError
127 instance HasNodeError GargError where
128 _NodeError = _GargNodeError
130 instance HasInvalidError GargError where
131 _InvalidError = _GargInvalidError
133 instance HasTreeError GargError where
134 _TreeError = _GargTreeError
136 showAsServantErr :: Show a => a -> ServantErr
137 showAsServantErr a = err500 { errBody = BL8.pack $ show a }
139 fireWall :: Applicative f => Request -> FireWall -> f Bool
141 let origin = lookup "Origin" (requestHeaders req)
142 let host = lookup "Host" (requestHeaders req)
144 let hostOk = Just (encodeUtf8 "localhost:3000")
145 let originOk = Just (encodeUtf8 "http://localhost:8008")
147 if origin == originOk
149 || (not $ unFireWall fw)
155 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
156 makeMockApp :: MockEnv -> IO Application
158 let serverApp = appMock
160 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
161 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
162 let checkOriginAndHost app req resp = do
163 blocking <- fireWall req (env ^. menv_firewall)
166 False -> resp ( responseLBS status401 []
167 "Invalid Origin or Host header")
169 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
170 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
171 { corsOrigins = Nothing -- == /*
172 , corsMethods = [ methodGet , methodPost , methodPut
173 , methodDelete, methodOptions, methodHead]
174 , corsRequestHeaders = ["authorization", "content-type"]
175 , corsExposedHeaders = Nothing
176 , corsMaxAge = Just ( 60*60*24 ) -- one day
177 , corsVaryOrigin = False
178 , corsRequireOrigin = False
179 , corsIgnoreFailures = False
182 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
183 -- $ Warp.defaultSettings
185 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
186 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
190 makeDevMiddleware :: IO Middleware
191 makeDevMiddleware = do
193 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
194 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
195 -- let checkOriginAndHost app req resp = do
196 -- blocking <- fireWall req (env ^. menv_firewall)
198 -- True -> app req resp
199 -- False -> resp ( responseLBS status401 []
200 -- "Invalid Origin or Host header")
202 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
203 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
204 { corsOrigins = Nothing -- == /*
205 , corsMethods = [ methodGet , methodPost , methodPut
206 , methodDelete, methodOptions, methodHead]
207 , corsRequestHeaders = ["authorization", "content-type"]
208 , corsExposedHeaders = Nothing
209 , corsMaxAge = Just ( 60*60*24 ) -- one day
210 , corsVaryOrigin = False
211 , corsRequireOrigin = False
212 , corsIgnoreFailures = False
215 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
216 -- $ Warp.defaultSettings
218 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
219 pure $ logStdoutDev . corsMiddleware
221 ---------------------------------------------------------------------
224 -- | API for serving @swagger.json@
225 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
227 -- | API for serving main operational routes of @gargantext.org@
230 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
231 -- | TODO :<|> Summary "Latest API" :> GargAPI'
234 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
238 "auth" :> Summary "AUTH API"
239 :> ReqBody '[JSON] AuthRequest
240 :> Post '[JSON] AuthResponse
243 :<|> "user" :> Summary "First user endpoint"
247 :<|> "node" :> Summary "Node endpoint"
248 :> Capture "id" NodeId :> NodeAPI HyperdataAny
251 :<|> "corpus":> Summary "Corpus endpoint"
252 :> Capture "id" CorpusId :> NodeAPI HyperdataCorpus
255 :<|> "annuaire":> Summary "Annuaire endpoint"
256 :> Capture "id" AnnuaireId :> NodeAPI HyperdataAnnuaire
259 :<|> "nodes" :> Summary "Nodes endpoint"
260 :> ReqBody '[JSON] [NodeId] :> NodesAPI
262 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
264 :<|> "count" :> Summary "Count endpoint"
265 :> ReqBody '[JSON] Query :> CountAPI
268 :<|> "search":> Summary "Search endpoint"
269 :> ReqBody '[JSON] SearchQuery
270 :> QueryParam "offset" Int
271 :> QueryParam "limit" Int
272 :> QueryParam "order" OrderBy
275 -- TODO move to NodeAPI?
276 :<|> "graph" :> Summary "Graph endpoint"
277 :> Capture "id" NodeId :> GraphAPI
279 -- TODO move to NodeAPI?
281 :<|> "tree" :> Summary "Tree endpoint"
282 :> Capture "id" NodeId :> TreeAPI
285 -- :<|> "scraper" :> WithCallbacks ScraperAPI
291 -- :<|> "list" :> Capture "id" Int :> NodeAPI
292 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
293 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
294 ---------------------------------------------------------------------
295 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
297 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
299 ---------------------------------------------------------------------
300 -- | Server declarations
302 server :: forall env. (HasConnection env, HasRepo env, HasSettings env)
303 => env -> IO (Server API)
305 -- orchestrator <- scrapyOrchestrator env
307 :<|> hoistServer (Proxy :: Proxy GargAPI) transform serverGargAPI
310 transform :: forall a. ReaderT env (ExceptT GargError IO) a -> Handler a
311 transform = Handler . withExceptT showAsServantErr . (`runReaderT` env)
313 serverGargAPI :: GargServer GargAPI
314 serverGargAPI -- orchestrator
317 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) fakeUserId
318 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) fakeUserId
319 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) fakeUserId
321 :<|> count -- TODO: undefined
323 :<|> graphAPI -- TODO: mock
327 fakeUserId = 1 -- TODO
329 serverStatic :: Server (Get '[HTML] Html)
331 let path = "purescript-gargantext/dist/index.html"
332 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
336 ---------------------------------------------------------------------
337 swaggerFront :: Server SwaggerFrontAPI
338 swaggerFront = schemaUiServer swaggerDoc
341 gargMock :: Server GargAPI
342 gargMock = mock apiGarg Proxy
344 ---------------------------------------------------------------------
345 makeApp :: (HasConnection env, HasRepo env, HasSettings env)
346 => env -> IO Application
347 makeApp = fmap (serve api) . server
349 appMock :: Application
350 appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
352 ---------------------------------------------------------------------
356 apiGarg :: Proxy GargAPI
358 ---------------------------------------------------------------------
360 schemaUiServer :: (Server api ~ Handler Swagger)
361 => Swagger -> Server (SwaggerSchemaUI' dir api)
362 schemaUiServer = swaggerSchemaUIServer
365 -- Type Family for the Documentation
366 type family TypeName (x :: *) :: Symbol where
368 TypeName Text = "Text"
369 TypeName x = GenericTypeName x (Rep x ())
371 type family GenericTypeName t (r :: *) :: Symbol where
372 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
374 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
377 -- | Swagger Specifications
378 swaggerDoc :: Swagger
379 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
380 & info.title .~ "Gargantext"
381 & info.version .~ "4.0.2" -- TODO same version as Gargantext
382 -- & info.base_url ?~ (URL "http://gargantext.org/")
383 & info.description ?~ "REST API specifications"
384 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
385 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
386 ["Gargantext" & description ?~ "Main operations"]
387 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
389 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
391 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
392 swaggerWriteJSON :: IO ()
393 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
395 portRouteInfo :: PortNumber -> IO ()
396 portRouteInfo port = do
397 T.putStrLn " ----Main Routes----- "
398 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
399 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
401 stopGargantext :: HasRepoSaver env => env -> IO ()
402 stopGargantext env = do
403 T.putStrLn "----- Stopping gargantext -----"
404 runReaderT saveRepo env
406 -- | startGargantext takes as parameters port number and Ini file.
407 startGargantext :: PortNumber -> FilePath -> IO ()
408 startGargantext port file = do
409 env <- newEnv port file
412 mid <- makeDevMiddleware
413 run port (mid app) `finally` stopGargantext env
415 startGargantextMock :: PortNumber -> IO ()
416 startGargantextMock port = do
418 application <- makeMockApp . MockEnv $ FireWall False