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 BangPatterns #-}
30 {-# LANGUAGE NumericUnderscores #-}
31 {-# LANGUAGE ScopedTypeVariables #-}
32 {-# LANGUAGE TypeOperators #-}
36 import Control.Concurrent
37 import Control.Exception (catch, finally, SomeException, displayException, IOException)
39 import Control.Monad.Except
40 import Control.Monad.Reader (runReaderT)
42 import Data.Foldable (foldlM)
43 import Data.List (lookup)
44 import Data.Text (pack)
45 import Data.Text.Encoding (encodeUtf8)
46 import Data.Text.IO (putStrLn)
48 import GHC.Base (Applicative)
49 import GHC.Generics (Generic)
50 import Gargantext.API.Admin.Auth.Types (AuthContext)
51 import Gargantext.API.Admin.EnvTypes (Env)
52 import Gargantext.API.Admin.Settings (newEnv)
53 import Gargantext.API.Admin.Types (FireWall(..), PortNumber, cookieSettings, jwtSettings, settings)
54 import Gargantext.API.EKG
55 import Gargantext.API.Ngrams (saveNodeStoryImmediate)
56 import Gargantext.API.Routes
57 import Gargantext.API.Server (server)
58 import Gargantext.Core.NodeStory
59 import Gargantext.Database.Prelude (Cmd)
60 import Gargantext.Database.Action.Metrics.NgramsByContext (refreshNgramsMaterialized)
61 import Gargantext.Prelude hiding (putStrLn)
62 import Network.HTTP.Types hiding (Query)
64 import Network.Wai.Handler.Warp hiding (defaultSettings)
65 import Network.Wai.Middleware.Cors
66 import Network.Wai.Middleware.RequestLogger
67 import Paths_gargantext (getDataDir)
69 import System.FilePath
70 import qualified Gargantext.Database.Prelude as DB
71 import qualified System.Cron.Schedule as Cron
73 data Mode = Dev | Mock | Prod
74 deriving (Show, Read, Generic)
76 -- | startGargantext takes as parameters port number and Ini file.
77 startGargantext :: Mode -> PortNumber -> FilePath -> IO ()
78 startGargantext mode port file = do
79 env <- newEnv port file
83 mid <- makeDevMiddleware mode
84 periodicActions <- schedulePeriodicActions env
85 run port (mid app) `finally` stopGargantext env periodicActions
87 where runDbCheck env = do
88 r <- runExceptT (runReaderT DB.dbCheck env) `catch`
89 (\(_ :: SomeException) -> return $ Right False)
91 Right True -> return ()
93 "You must run 'gargantext-init " <> pack file <>
94 "' before running gargantext-server (only the first time)."
96 portRouteInfo :: PortNumber -> IO ()
97 portRouteInfo port = do
98 putStrLn " ----Main Routes----- "
99 putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
100 putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
102 -- | Stops the gargantext server and cancels all the periodic actions
103 -- scheduled to run up to that point.
104 -- TODO clean this Monad condition (more generic) ?
105 stopGargantext :: HasNodeStoryImmediateSaver env => env -> [ThreadId] -> IO ()
106 stopGargantext env scheduledPeriodicActions = do
107 forM_ scheduledPeriodicActions killThread
108 putStrLn "----- Stopping gargantext -----"
109 runReaderT saveNodeStoryImmediate env
112 startGargantextMock :: PortNumber -> IO ()
113 startGargantextMock port = do
115 application <- makeMockApp . MockEnv $ FireWall False
119 -- | Schedules all sorts of useful periodic actions to be run while
120 -- the server is alive accepting requests.
121 schedulePeriodicActions :: DB.CmdCommon env => env -> IO [ThreadId]
122 schedulePeriodicActions env =
123 -- Add your scheduled actions here.
127 in foldlM (\ !acc action -> (`mappend` acc) <$> Cron.execSchedule action) [] actions
131 refreshDBViews :: Cron.Schedule ()
134 res <- DB.runCmd env (refreshNgramsMaterialized :: Cmd IOException ())
136 Left e -> liftIO $ putStrLn $ pack ("Refreshing Ngrams materialized view failed: " <> displayException e)
138 _ <- liftIO $ putStrLn $ pack "Refresh Index Database done"
140 Cron.addJob doRefresh "* 2 * * *"
142 ----------------------------------------------------------------------
144 fireWall :: Applicative f => Request -> FireWall -> f Bool
146 let origin = lookup "Origin" (requestHeaders req)
147 let host = lookup "Host" (requestHeaders req)
149 if origin == Just (encodeUtf8 "http://localhost:8008")
150 && host == Just (encodeUtf8 "localhost:3000")
151 || (not $ unFireWall fw)
157 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
158 makeMockApp :: MockEnv -> IO Application
160 let serverApp = appMock
162 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
163 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
164 let checkOriginAndHost app req resp = do
165 blocking <- fireWall req (env ^. menv_firewall)
168 False -> resp ( responseLBS status401 []
169 "Invalid Origin or Host header")
171 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
172 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
173 { corsOrigins = Nothing -- == /*
174 , corsMethods = [ methodGet , methodPost , methodPut
175 , methodDelete, methodOptions, methodHead]
176 , corsRequestHeaders = ["authorization", "content-type"]
177 , corsExposedHeaders = Nothing
178 , corsMaxAge = Just ( 60*60*24 ) -- one day
179 , corsVaryOrigin = False
180 , corsRequireOrigin = False
181 , corsIgnoreFailures = False
184 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
185 -- $ Warp.defaultSettings
187 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
188 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
192 makeDevMiddleware :: Mode -> IO Middleware
193 makeDevMiddleware mode = do
194 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
195 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
196 -- let checkOriginAndHost app req resp = do
197 -- blocking <- fireWall req (env ^. menv_firewall)
199 -- True -> app req resp
200 -- False -> resp ( responseLBS status401 []
201 -- "Invalid Origin or Host header")
203 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
204 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
205 { corsOrigins = Nothing -- == /*
206 , corsMethods = [ methodGet , methodPost , methodPut
207 , methodDelete, methodOptions, methodHead]
208 , corsRequestHeaders = ["authorization", "content-type"]
209 , corsExposedHeaders = Nothing
210 , corsMaxAge = Just ( 60*60*24 ) -- one day
211 , corsVaryOrigin = False
212 , corsRequireOrigin = False
213 , corsIgnoreFailures = False
216 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
217 -- $ Warp.defaultSettings
219 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
221 Prod -> pure $ logStdout . corsMiddleware
222 _ -> pure $ logStdoutDev . corsMiddleware
224 ---------------------------------------------------------------------
226 ---------------------------------------------------------------------
228 ---------------------------
231 -- TODO-SECURITY admin only: withAdmin
232 -- Question: How do we mark admins?
234 serverGargAdminAPI :: GargServer GargAdminAPI
235 serverGargAdminAPI = roots
239 ---------------------------------------------------------------------
240 --gargMock :: Server GargAPI
241 --gargMock = mock apiGarg Proxy
242 ---------------------------------------------------------------------
244 makeApp :: Env -> IO Application
247 (ekgStore, ekgMid) <- newEkgStore api
248 ekgDir <- (</> "ekg-assets") <$> getDataDir
249 return $ ekgMid $ serveWithContext apiWithEkg cfg
250 (ekgServer ekgDir ekgStore :<|> serv)
252 cfg :: Servant.Context AuthContext
253 cfg = env ^. settings . jwtSettings
254 :. env ^. settings . cookieSettings
258 --appMock :: Application
259 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
260 ---------------------------------------------------------------------
264 apiWithEkg :: Proxy (EkgAPI :<|> API)
267 apiGarg :: Proxy GargAPI
269 ---------------------------------------------------------------------
272 --import GHC.Generics (D1, Meta (..), Rep, Generic)
273 --import GHC.TypeLits (AppendSymbol, Symbol)
274 ---------------------------------------------------------------------
275 -- Type Family for the Documentation
276 type family TypeName (x :: *) :: Symbol where
278 TypeName Text = "Text"
279 TypeName x = GenericTypeName x (Rep x ())
281 type family GenericTypeName t (r :: *) :: Symbol where
282 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
284 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))