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
132 refreshDBViews :: Cron.Schedule ()
135 res <- DB.runCmd env (refreshNgramsMaterialized :: Cmd IOException ())
137 Left e -> liftIO $ putStrLn $ pack ("Refreshing Ngrams materialized view failed: " <> displayException e)
139 _ <- liftIO $ putStrLn $ pack "Refresh Index Database done"
141 Cron.addJob doRefresh "* 2 * * *"
144 ----------------------------------------------------------------------
146 fireWall :: Applicative f => Request -> FireWall -> f Bool
148 let origin = lookup "Origin" (requestHeaders req)
149 let host = lookup "Host" (requestHeaders req)
151 if origin == Just (encodeUtf8 "http://localhost:8008")
152 && host == Just (encodeUtf8 "localhost:3000")
153 || (not $ unFireWall fw)
159 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
160 makeMockApp :: MockEnv -> IO Application
162 let serverApp = appMock
164 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
165 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
166 let checkOriginAndHost app req resp = do
167 blocking <- fireWall req (env ^. menv_firewall)
170 False -> resp ( responseLBS status401 []
171 "Invalid Origin or Host header")
173 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
174 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
175 { corsOrigins = Nothing -- == /*
176 , corsMethods = [ methodGet , methodPost , methodPut
177 , methodDelete, methodOptions, methodHead]
178 , corsRequestHeaders = ["authorization", "content-type"]
179 , corsExposedHeaders = Nothing
180 , corsMaxAge = Just ( 60*60*24 ) -- one day
181 , corsVaryOrigin = False
182 , corsRequireOrigin = False
183 , corsIgnoreFailures = False
186 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
187 -- $ Warp.defaultSettings
189 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
190 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
194 makeDevMiddleware :: Mode -> IO Middleware
195 makeDevMiddleware mode = do
196 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
197 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
198 -- let checkOriginAndHost app req resp = do
199 -- blocking <- fireWall req (env ^. menv_firewall)
201 -- True -> app req resp
202 -- False -> resp ( responseLBS status401 []
203 -- "Invalid Origin or Host header")
205 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
206 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
207 { corsOrigins = Nothing -- == /*
208 , corsMethods = [ methodGet , methodPost , methodPut
209 , methodDelete, methodOptions, methodHead]
210 , corsRequestHeaders = ["authorization", "content-type"]
211 , corsExposedHeaders = Nothing
212 , corsMaxAge = Just ( 60*60*24 ) -- one day
213 , corsVaryOrigin = False
214 , corsRequireOrigin = False
215 , corsIgnoreFailures = False
218 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
219 -- $ Warp.defaultSettings
221 --pure (warpS, logWare . checkOriginAndHost . corsMiddleware)
223 Prod -> pure $ logStdout . corsMiddleware
224 _ -> pure $ logStdoutDev . corsMiddleware
226 ---------------------------------------------------------------------
228 ---------------------------------------------------------------------
230 ---------------------------
233 -- TODO-SECURITY admin only: withAdmin
234 -- Question: How do we mark admins?
236 serverGargAdminAPI :: GargServer GargAdminAPI
237 serverGargAdminAPI = roots
241 ---------------------------------------------------------------------
242 --gargMock :: Server GargAPI
243 --gargMock = mock apiGarg Proxy
244 ---------------------------------------------------------------------
246 makeApp :: Env -> IO Application
249 (ekgStore, ekgMid) <- newEkgStore api
250 ekgDir <- (</> "ekg-assets") <$> getDataDir
251 return $ ekgMid $ serveWithContext apiWithEkg cfg
252 (ekgServer ekgDir ekgStore :<|> serv)
254 cfg :: Servant.Context AuthContext
255 cfg = env ^. settings . jwtSettings
256 :. env ^. settings . cookieSettings
260 --appMock :: Application
261 --appMock = serve api (swaggerFront :<|> gargMock :<|> serverStatic)
262 ---------------------------------------------------------------------
266 apiWithEkg :: Proxy (EkgAPI :<|> API)
269 apiGarg :: Proxy GargAPI
271 ---------------------------------------------------------------------
274 --import GHC.Generics (D1, Meta (..), Rep, Generic)
275 --import GHC.TypeLits (AppendSymbol, Symbol)
276 ---------------------------------------------------------------------
277 -- Type Family for the Documentation
278 type family TypeName (x :: *) :: Symbol where
280 TypeName Text = "Text"
281 TypeName x = GenericTypeName x (Rep x ())
283 type family GenericTypeName t (r :: *) :: Symbol where
284 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
286 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))