/ src / Gargantext / API.hs
API.hs
  1  {-|
  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
  8  Portability : POSIX
  9  
 10  Main (RESTful) API of the instance Gargantext.
 11  
 12  The Garg-API is typed to derive the documentation, the mock and tests.
 13  
 14  This API is indeed typed in order to be able to derive both the server
 15  and the client sides.
 16  
 17  The Garg-API-Monad enables:
 18    - Security (WIP)
 19    - Features (WIP)
 20    - Database connection (long term)
 21    - In Memory stack management (short term)
 22    - Logs (WIP)
 23  
 24  Thanks to Yann Esposito for our discussions at the start and to Nicolas
 25  Pouillard (who mainly made it).
 26  
 27  -}
 28  
 29  {-# LANGUAGE BangPatterns         #-}
 30  {-# LANGUAGE NumericUnderscores   #-}
 31  {-# LANGUAGE ScopedTypeVariables  #-}
 32  {-# LANGUAGE TypeFamilies         #-}
 33  {-# LANGUAGE TypeOperators        #-}
 34  module Gargantext.API
 35        where
 36  
 37  import Control.Concurrent
 38  import Control.Lens hiding (Level)
 39  import Data.List (lookup)
 40  import Data.Text (pack)
 41  import Data.Text.Encoding qualified as TE
 42  import Data.Text.IO (putStrLn)
 43  import Data.Validity
 44  import Gargantext.API.Admin.Auth.Types (AuthContext)
 45  import Gargantext.API.Admin.EnvTypes (Env, Mode(..))
 46  import Gargantext.API.Admin.Settings (newEnv)
 47  import Gargantext.API.Admin.Settings.CORS
 48  import Gargantext.API.Admin.Types (FireWall(..), PortNumber, cookieSettings, jwtSettings, settings, corsSettings)
 49  import Gargantext.API.EKG
 50  import Gargantext.API.Middleware (logStdoutDevSanitised)
 51  import Gargantext.API.Routes
 52  import Gargantext.API.Server (server)
 53  import Gargantext.Database.Prelude qualified as DB
 54  import Gargantext.Prelude hiding (putStrLn)
 55  import Gargantext.System.Logging
 56  import Network.HTTP.Types hiding (Query)
 57  import Network.Wai
 58  import Network.Wai.Handler.Warp hiding (defaultSettings)
 59  import Network.Wai.Middleware.Cors
 60  import Network.Wai.Middleware.RequestLogger
 61  import Paths_gargantext (getDataDir)
 62  import Servant hiding (Header)
 63  import System.Cron.Schedule qualified as Cron
 64  import System.FilePath
 65  
 66  -- | startGargantext takes as parameters port number and Ini file.
 67  startGargantext :: Mode -> PortNumber -> FilePath -> IO ()
 68  startGargantext mode port file = withLoggerHoisted mode $ \logger -> do
 69    env <- newEnv logger port file
 70    runDbCheck env
 71    portRouteInfo port
 72    app <- makeApp env
 73    mid <- makeGargMiddleware (env ^. settings.corsSettings) mode
 74    periodicActions <- schedulePeriodicActions env
 75    run port (mid app) `finally` stopGargantext periodicActions
 76  
 77    where runDbCheck env = do
 78            r <- runExceptT (runReaderT DB.dbCheck env) `catch`
 79              (\(_ :: SomeException) -> pure $ Right False)
 80            case r of
 81              Right True -> pure ()
 82              _ -> panicTrace $
 83                "You must run 'gargantext-init " <> pack file <>
 84                "' before running gargantext-server (only the first time)."
 85  
 86  portRouteInfo :: PortNumber -> IO ()
 87  portRouteInfo port = do
 88    putStrLn   "=========================================================================================================="
 89    putStrLn   " GarganText Main Routes"
 90    putStrLn   "=========================================================================================================="
 91    putStrLn $ " - Web GarganText Frontend..................: " <> "http://localhost:" <> toUrlPiece port <> "/index.html"
 92    putStrLn $ " - Swagger UI (API documentation)...........: " <> "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
 93    putStrLn $ " - Playground GraphQL (API documentation)...: " <> "http://localhost:" <> toUrlPiece port <> "/gql"
 94    putStrLn   "=========================================================================================================="
 95  
 96  -- | Stops the gargantext server and cancels all the periodic actions
 97  -- scheduled to run up to that point.
 98  -- TODO clean this Monad condition (more generic) ?
 99  stopGargantext :: [ThreadId] -> IO ()
100  stopGargantext scheduledPeriodicActions = do
101    forM_ scheduledPeriodicActions killThread
102    putStrLn "----- Stopping gargantext -----"
103  
104  -- | Schedules all sorts of useful periodic actions to be run while
105  -- the server is alive accepting requests.
106  schedulePeriodicActions :: DB.CmdCommon env => env -> IO [ThreadId]
107  schedulePeriodicActions _env =
108    -- Add your scheduled actions here.
109    let actions = [
110            -- refreshDBViews
111          ]
112    in foldlM (\ !acc action -> (`mappend` acc) <$> Cron.execSchedule action) [] actions
113  
114  {-
115    where
116  
117      refreshDBViews :: Cron.Schedule ()
118      refreshDBViews = do
119        let doRefresh = do
120              res <- DB.runCmd env (refreshNgramsMaterialized :: Cmd IOException ())
121              case res of
122                Left e   -> liftIO $ putStrLn $ pack ("Refreshing Ngrams materialized view failed: " <> displayException e)
123                Right () ->  do
124                  _ <- liftIO $ putStrLn $ pack "Refresh Index Database done"
125                  pure ()
126        Cron.addJob doRefresh "* 2 * * *"
127  -}
128  
129  ----------------------------------------------------------------------
130  
131  fireWall :: Applicative f => Request -> FireWall -> f Bool
132  fireWall req fw = do
133      let origin = lookup "Origin" (requestHeaders req)
134      let host   = lookup "Host"   (requestHeaders req)
135  
136      if  origin == Just (encodeUtf8 "http://localhost:8008")
137         && host == Just (encodeUtf8 "localhost:3000")
138         || (not $ unFireWall fw)
139  
140         then pure True
141         else pure False
142  
143  makeGargMiddleware :: CORSSettings -> Mode -> IO Middleware
144  makeGargMiddleware crsSettings mode = do
145      let corsMiddleware = cors $ \_incomingRq -> Just
146            simpleCorsResourcePolicy
147              { corsOrigins = Just (map mkCorsOrigin (crsSettings ^. corsAllowedOrigins), True)
148              , corsMethods = [ methodGet   , methodPost   , methodPut
149                              , methodDelete, methodOptions, methodHead]
150              , corsIgnoreFailures = False
151              , corsRequestHeaders = ["authorization", "content-type", "x-garg-error-scheme"]
152              , corsMaxAge         = Just ( 60*60*24 ) -- one day
153              }
154      case mode of
155        Prod -> pure $ logStdout . corsMiddleware
156        _    -> do
157          loggerMiddleware <- logStdoutDevSanitised
158          pure $ loggerMiddleware . corsMiddleware
159    where
160      mkCorsOrigin :: CORSOrigin -> Origin
161      mkCorsOrigin = TE.encodeUtf8 . _CORSOrigin
162  
163  ---------------------------------------------------------------------
164  -- | API Global
165  ---------------------------------------------------------------------
166  
167  makeApp :: Env -> IO Application
168  makeApp env = do
169    serv <- server env
170    (ekgStore, ekgMid) <- newEkgStore api
171    ekgDir <- (</> "ekg-assets") <$> getDataDir
172    pure $ ekgMid $ serveWithContext apiWithEkg cfg
173      (ekgServer ekgDir ekgStore :<|> serv)
174    where
175      cfg :: Servant.Context AuthContext
176      cfg = env ^. settings . jwtSettings
177         :. env ^. settings . cookieSettings
178         :. EmptyContext
179  
180  ---------------------------------------------------------------------
181  api :: Proxy API
182  api  = Proxy
183  
184  apiWithEkg :: Proxy (EkgAPI :<|> API)
185  apiWithEkg = Proxy
186  
187  apiGarg :: Proxy GargAPI
188  apiGarg  = Proxy
189  ---------------------------------------------------------------------