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 TypeFamilies #-}
34 {-# LANGUAGE UndecidableInstances #-}
36 ---------------------------------------------------------------------
39 ---------------------------------------------------------------------
41 import System.IO (FilePath)
43 import GHC.Generics (D1, Meta (..), Rep)
44 import GHC.TypeLits (AppendSymbol, Symbol)
47 import Control.Exception (finally)
48 import Control.Monad.IO.Class (liftIO)
49 import Control.Monad.Reader (runReaderT)
50 import Data.Aeson.Encode.Pretty (encodePretty)
51 import qualified Data.ByteString.Lazy.Char8 as BL8
53 import Data.Text (Text)
54 import qualified Data.Text.IO as T
55 --import qualified Data.Set as Set
58 import Network.Wai.Handler.Warp hiding (defaultSettings)
61 import Servant.HTML.Blaze (HTML)
62 import Servant.Mock (mock)
63 --import Servant.Job.Server (WithCallbacks)
64 import Servant.Static.TH.Internal.Server (fileTreeToServer)
65 import Servant.Static.TH.Internal.FileTree (fileTypeToFileTree, FileType(FileTypeFile))
66 import Servant.Swagger
67 import Servant.Swagger.UI
68 -- import Servant.API.Stream
69 import Text.Blaze.Html (Html)
71 --import Gargantext.API.Swagger
72 import Gargantext.Prelude
73 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
75 import Gargantext.API.Auth (AuthRequest, AuthResponse, auth)
76 import Gargantext.API.Ngrams (HasRepoVar(..), HasRepoSaver(..), saveRepo)
77 import Gargantext.API.Node ( GargServer
87 --import Gargantext.Database.Node.Contact (HyperdataContact)
88 import Gargantext.Database.Utils (HasConnection)
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
94 --import Gargantext.API.Orchestrator
95 --import Gargantext.API.Orchestrator.Types
97 ---------------------------------------------------------------------
99 import GHC.Base (Applicative)
100 -- import Control.Lens
102 import Data.List (lookup)
103 import Data.Text.Encoding (encodeUtf8)
105 --import Network.Wai (Request, requestHeaders, responseLBS)
106 import Network.Wai (Request, requestHeaders)
107 --import qualified Network.Wai.Handler.Warp as Warp
108 import Network.Wai.Middleware.Cors
110 import Network.Wai.Middleware.RequestLogger
111 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
113 import Network.HTTP.Types hiding (Query)
116 import Gargantext.API.Settings
118 fireWall :: Applicative f => Request -> FireWall -> f Bool
120 let origin = lookup "Origin" (requestHeaders req)
121 let host = lookup "Host" (requestHeaders req)
123 let hostOk = Just (encodeUtf8 "localhost:3000")
124 let originOk = Just (encodeUtf8 "http://localhost:8008")
126 if origin == originOk
128 || (not $ unFireWall fw)
134 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
135 makeMockApp :: MockEnv -> IO Application
137 let serverApp = appMock
139 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
140 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
141 let checkOriginAndHost app req resp = do
142 blocking <- fireWall req (env ^. menv_firewall)
145 False -> resp ( responseLBS status401 []
146 "Invalid Origin or Host header")
148 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
149 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
150 { corsOrigins = Nothing -- == /*
151 , corsMethods = [ methodGet , methodPost , methodPut
152 , methodDelete, methodOptions, methodHead]
153 , corsRequestHeaders = ["authorization", "content-type"]
154 , corsExposedHeaders = Nothing
155 , corsMaxAge = Just ( 60*60*24 ) -- one day
156 , corsVaryOrigin = False
157 , corsRequireOrigin = False
158 , corsIgnoreFailures = False
161 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
162 -- $ Warp.defaultSettings
164 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
165 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
169 makeDevMiddleware :: IO Middleware
170 makeDevMiddleware = do
172 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
173 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
174 -- let checkOriginAndHost app req resp = do
175 -- blocking <- fireWall req (env ^. menv_firewall)
177 -- True -> app req resp
178 -- False -> resp ( responseLBS status401 []
179 -- "Invalid Origin or Host header")
181 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
182 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
183 { corsOrigins = Nothing -- == /*
184 , corsMethods = [ methodGet , methodPost , methodPut
185 , methodDelete, methodOptions, methodHead]
186 , corsRequestHeaders = ["authorization", "content-type"]
187 , corsExposedHeaders = Nothing
188 , corsMaxAge = Just ( 60*60*24 ) -- one day
189 , corsVaryOrigin = False
190 , corsRequireOrigin = False
191 , corsIgnoreFailures = False
194 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
195 -- $ Warp.defaultSettings
197 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
198 pure $ logStdoutDev . corsMiddleware
200 ---------------------------------------------------------------------
203 -- | API for serving @swagger.json@
204 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
206 -- | API for serving main operational routes of @gargantext.org@
209 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
210 -- | TODO :<|> Summary "Latest API" :> GargAPI'
213 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
217 "auth" :> Summary "AUTH API"
218 :> ReqBody '[JSON] AuthRequest
219 :> Post '[JSON] AuthResponse
222 :<|> "user" :> Summary "First user endpoint"
226 :<|> "node" :> Summary "Node endpoint"
227 :> Capture "id" NodeId :> NodeAPI HyperdataAny
230 :<|> "corpus":> Summary "Corpus endpoint"
231 :> Capture "id" CorpusId :> NodeAPI HyperdataCorpus
234 :<|> "annuaire":> Summary "Annuaire endpoint"
235 :> Capture "id" AnnuaireId :> NodeAPI HyperdataAnnuaire
238 :<|> "nodes" :> Summary "Nodes endpoint"
239 :> ReqBody '[JSON] [NodeId] :> NodesAPI
241 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
243 :<|> "count" :> Summary "Count endpoint"
244 :> ReqBody '[JSON] Query :> CountAPI
247 :<|> "search":> Summary "Search endpoint"
248 :> ReqBody '[JSON] SearchQuery
249 :> QueryParam "offset" Int
250 :> QueryParam "limit" Int
251 :> QueryParam "order" OrderBy
254 -- TODO move to NodeAPI?
255 :<|> "graph" :> Summary "Graph endpoint"
256 :> Capture "id" NodeId :> GraphAPI
258 -- TODO move to NodeAPI?
260 :<|> "tree" :> Summary "Tree endpoint"
261 :> Capture "id" NodeId :> TreeAPI
264 -- :<|> "scraper" :> WithCallbacks ScraperAPI
270 -- :<|> "list" :> Capture "id" Int :> NodeAPI
271 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
272 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
273 ---------------------------------------------------------------------
274 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
276 type API = SwaggerFrontAPI :<|> GargAPI :<|> Get '[HTML] Html
278 ---------------------------------------------------------------------
279 -- | Server declarations
281 server :: (HasConnection env, HasRepoVar env, HasRepoSaver env)
282 => env -> IO (Server API)
284 -- orchestrator <- scrapyOrchestrator env
286 :<|> hoistServer (Proxy :: Proxy GargAPI) (`runReaderT` env) serverGargAPI
289 serverGargAPI :: GargServer GargAPI
290 serverGargAPI -- orchestrator
293 :<|> nodeAPI (Proxy :: Proxy HyperdataAny) fakeUserId
294 :<|> nodeAPI (Proxy :: Proxy HyperdataCorpus) fakeUserId
295 :<|> nodeAPI (Proxy :: Proxy HyperdataAnnuaire) fakeUserId
297 :<|> count -- TODO: undefined
299 :<|> graphAPI -- TODO: mock
303 fakeUserId = 1 -- TODO
305 serverStatic :: Server (Get '[HTML] Html)
307 let path = "purescript-gargantext/dist/index.html"
308 Just s <- liftIO (fileTypeToFileTree (FileTypeFile path))
312 ---------------------------------------------------------------------
313 swaggerFront :: Server SwaggerFrontAPI
314 swaggerFront = schemaUiServer swaggerDoc
317 gargMock :: Server GargAPI
318 gargMock = mock apiGarg Proxy
320 ---------------------------------------------------------------------
321 makeApp :: (HasConnection env, HasRepoVar env, HasRepoSaver env)
322 => env -> IO Application
323 makeApp = fmap (serve api) . server
325 appMock :: Application
326 appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
328 ---------------------------------------------------------------------
332 apiGarg :: Proxy GargAPI
334 ---------------------------------------------------------------------
336 schemaUiServer :: (Server api ~ Handler Swagger)
337 => Swagger -> Server (SwaggerSchemaUI' dir api)
338 schemaUiServer = swaggerSchemaUIServer
341 -- Type Family for the Documentation
342 type family TypeName (x :: *) :: Symbol where
344 TypeName Text = "Text"
345 TypeName x = GenericTypeName x (Rep x ())
347 type family GenericTypeName t (r :: *) :: Symbol where
348 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
350 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
353 -- | Swagger Specifications
354 swaggerDoc :: Swagger
355 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
356 & info.title .~ "Gargantext"
357 & info.version .~ "4.0.2" -- TODO same version as Gargantext
358 -- & info.base_url ?~ (URL "http://gargantext.org/")
359 & info.description ?~ "REST API specifications"
360 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
361 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
362 ["Gargantext" & description ?~ "Main operations"]
363 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
365 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
367 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
368 swaggerWriteJSON :: IO ()
369 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
371 portRouteInfo :: PortNumber -> IO ()
372 portRouteInfo port = do
373 T.putStrLn " ----Main Routes----- "
374 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
375 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
377 stopGargantext :: HasRepoSaver env => env -> IO ()
378 stopGargantext env = do
379 T.putStrLn "----- Stopping gargantext -----"
380 runReaderT saveRepo env
382 -- | startGargantext takes as parameters port number and Ini file.
383 startGargantext :: PortNumber -> FilePath -> IO ()
384 startGargantext port file = do
385 env <- newEnv port file
388 mid <- makeDevMiddleware
389 run port (mid app) `finally` stopGargantext env
391 startGargantextMock :: PortNumber -> IO ()
392 startGargantextMock port = do
394 application <- makeMockApp . MockEnv $ FireWall False