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 (RESTful) API of the instance Gargantext.
12 The Garg-API is typed to derive the documentation, the mock and tests.
14 This API is indeed typed in order to be able to derive both the server
17 The Garg-API-Monad enables:
20 - Database connection (long term)
21 - In Memory stack management (short term)
24 Thanks to Yann Esposito for our discussions at the start and to Nicolas
25 Pouillard (who mainly made it).
29 {-# LANGUAGE ScopedTypeVariables #-}
30 {-# LANGUAGE TypeOperators #-}
34 import Control.Exception (finally)
36 import Control.Monad.Reader (runReaderT)
37 import Data.List (lookup)
38 import Data.Text.Encoding (encodeUtf8)
39 import Data.Text.IO (putStrLn)
41 import GHC.Base (Applicative)
42 import GHC.Generics (Generic)
43 import Gargantext.API.Admin.Auth.Types (AuthContext)
44 import Gargantext.API.Admin.Settings (newEnv)
45 import Gargantext.API.Admin.Types (FireWall(..), PortNumber, cookieSettings, jwtSettings, settings)
46 import Gargantext.API.EKG
47 import Gargantext.API.Ngrams (saveNodeStory)
48 import Gargantext.API.Prelude
49 import Gargantext.API.Routes
50 import Gargantext.API.Server (server)
51 import Gargantext.Core.NodeStory
52 import Gargantext.Prelude hiding (putStrLn)
53 import Network.HTTP.Types hiding (Query)
55 import Network.Wai.Handler.Warp hiding (defaultSettings)
56 import Network.Wai.Middleware.Cors
57 import Network.Wai.Middleware.RequestLogger
58 import Paths_gargantext (getDataDir)
60 import System.FilePath
62 data Mode = Dev | Mock | Prod
63 deriving (Show, Read, Generic)
65 -- | startGargantext takes as parameters port number and Ini file.
66 startGargantext :: Mode -> PortNumber -> FilePath -> IO ()
67 startGargantext mode port file = do
68 env <- newEnv port file
71 mid <- makeDevMiddleware mode
72 run port (mid app) `finally` stopGargantext env
74 portRouteInfo :: PortNumber -> IO ()
75 portRouteInfo port = do
76 putStrLn " ----Main Routes----- "
77 putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
78 putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
80 -- TODO clean this Monad condition (more generic) ?
81 stopGargantext :: HasNodeStorySaver env => env -> IO ()
82 stopGargantext env = do
83 putStrLn "----- Stopping gargantext -----"
84 runReaderT saveNodeStory env
87 startGargantextMock :: PortNumber -> IO ()
88 startGargantextMock port = do
90 application <- makeMockApp . MockEnv $ FireWall False
94 ----------------------------------------------------------------------
96 fireWall :: Applicative f => Request -> FireWall -> f Bool
98 let origin = lookup "Origin" (requestHeaders req)
99 let host = lookup "Host" (requestHeaders req)
101 if origin == Just (encodeUtf8 "http://localhost:8008")
102 && host == Just (encodeUtf8 "localhost:3000")
103 || (not $ unFireWall fw)
109 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
110 makeMockApp :: MockEnv -> IO Application
112 let serverApp = appMock
114 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
115 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
116 let checkOriginAndHost app req resp = do
117 blocking <- fireWall req (env ^. menv_firewall)
120 False -> resp ( responseLBS status401 []
121 "Invalid Origin or Host header")
123 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
124 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
125 { corsOrigins = Nothing -- == /*
126 , corsMethods = [ methodGet , methodPost , methodPut
127 , methodDelete, methodOptions, methodHead]
128 , corsRequestHeaders = ["authorization", "content-type"]
129 , corsExposedHeaders = Nothing
130 , corsMaxAge = Just ( 60*60*24 ) -- one day
131 , corsVaryOrigin = False
132 , corsRequireOrigin = False
133 , corsIgnoreFailures = False
136 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
137 -- $ Warp.defaultSettings
139 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
140 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
144 makeDevMiddleware :: Mode -> IO Middleware
145 makeDevMiddleware mode = do
146 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
147 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
148 -- let checkOriginAndHost app req resp = do
149 -- blocking <- fireWall req (env ^. menv_firewall)
151 -- True -> app req resp
152 -- False -> resp ( responseLBS status401 []
153 -- "Invalid Origin or Host header")
155 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
156 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
157 { corsOrigins = Nothing -- == /*
158 , corsMethods = [ methodGet , methodPost , methodPut
159 , methodDelete, methodOptions, methodHead]
160 , corsRequestHeaders = ["authorization", "content-type"]
161 , corsExposedHeaders = Nothing
162 , corsMaxAge = Just ( 60*60*24 ) -- one day
163 , corsVaryOrigin = False
164 , corsRequireOrigin = False
165 , corsIgnoreFailures = False
168 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
169 -- $ Warp.defaultSettings
171 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
173 Prod -> pure $ logStdout . corsMiddleware
174 _ -> pure $ logStdoutDev . corsMiddleware
176 ---------------------------------------------------------------------
178 ---------------------------------------------------------------------
180 ---------------------------
183 -- TODO-SECURITY admin only: withAdmin
184 -- Question: How do we mark admins?
186 serverGargAdminAPI :: GargServer GargAdminAPI
187 serverGargAdminAPI = roots
191 ---------------------------------------------------------------------
192 --gargMock :: Server GargAPI
193 --gargMock = mock apiGarg Proxy
194 ---------------------------------------------------------------------
196 makeApp :: (Typeable env, EnvC env) => env -> IO Application
199 (ekgStore, ekgMid) <- newEkgStore api
200 ekgDir <- (</> "ekg-assets") <$> getDataDir
201 return $ ekgMid $ serveWithContext apiWithEkg cfg
202 (ekgServer ekgDir ekgStore :<|> serv)
204 cfg :: Servant.Context AuthContext
205 cfg = env ^. settings . jwtSettings
206 :. env ^. settings . cookieSettings
210 --appMock :: Application
211 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
212 ---------------------------------------------------------------------
216 apiWithEkg :: Proxy (EkgAPI :<|> API)
219 apiGarg :: Proxy GargAPI
221 ---------------------------------------------------------------------
224 --import GHC.Generics (D1, Meta (..), Rep, Generic)
225 --import GHC.TypeLits (AppendSymbol, Symbol)
226 ---------------------------------------------------------------------
227 -- Type Family for the Documentation
228 type family TypeName (x :: *) :: Symbol where
230 TypeName Text = "Text"
231 TypeName x = GenericTypeName x (Rep x ())
233 type family GenericTypeName t (r :: *) :: Symbol where
234 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
236 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))