2 Module : Gargantext.API.Settings
3 Description : Settings of the API (Server and Client)
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 TODO-SECURITY: Critical
13 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
15 {-# LANGUAGE DataKinds #-}
16 {-# LANGUAGE DeriveGeneric #-}
17 {-# LANGUAGE FlexibleContexts #-}
18 {-# LANGUAGE FlexibleInstances #-}
19 {-# LANGUAGE MultiParamTypeClasses #-}
20 {-# LANGUAGE NoImplicitPrelude #-}
21 {-# LANGUAGE OverloadedStrings #-}
22 {-# LANGUAGE RankNTypes #-}
23 {-# LANGUAGE ScopedTypeVariables #-}
24 {-# LANGUAGE TemplateHaskell #-}
26 module Gargantext.API.Settings
29 import System.Directory
30 import System.Log.FastLogger
32 import GHC.Generics (Generic)
33 import Prelude (Bounded(), fail)
34 import System.Environment (lookupEnv)
35 import System.IO (FilePath, hClose)
36 import System.IO.Temp (withTempFile)
37 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
38 import Database.PostgreSQL.Simple (Connection, connect)
39 import Network.HTTP.Client (Manager)
40 import Network.HTTP.Client.TLS (newTlsManager)
43 import Data.Maybe (fromMaybe)
44 import Data.Either (either)
46 --import Data.Text.Encoding (encodeUtf8)
47 import Data.ByteString (ByteString)
48 import qualified Data.ByteString.Lazy as L
51 import Servant.Auth.Server (defaultJWTSettings, JWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
52 import Servant.Client (BaseUrl, parseBaseUrl)
53 import qualified Servant.Job.Core
54 import Servant.Job.Async (newJobEnv, defaultSettings, HasJobEnv(..), Job)
55 import Web.HttpApiData (parseUrlPiece)
57 import Control.Concurrent
58 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
59 import Control.Exception (finally)
60 import Control.Monad.Logger
61 import Control.Monad.Reader
63 import Gargantext.Prelude
64 import Gargantext.Database.Utils (databaseParameters, HasConnection(..), Cmd', runCmd)
65 import Gargantext.API.Ngrams (NgramsRepo, HasRepoVar(..), HasRepoSaver(..), HasRepo(..), RepoEnv(..), r_version, saveRepo, initRepo, renv_var, renv_lock)
66 import Gargantext.API.Orchestrator.Types
70 data SendEmailType = SendEmailViaAws
73 deriving (Show, Read, Enum, Bounded, Generic)
76 data Settings = Settings
77 { _allowedOrigin :: ByteString -- allowed origin for CORS
78 , _allowedHost :: ByteString -- allowed host for CORS
79 , _appPort :: PortNumber
80 , _logLevelLimit :: LogLevel -- log level from the monad-logger package
81 -- , _dbServer :: Text
82 -- ^ this is not used yet
83 , _jwtSettings :: JWTSettings
84 , _cookieSettings :: CookieSettings
85 , _sendLoginEmails :: SendEmailType
86 , _scrapydUrl :: BaseUrl
87 , _fileFolder :: FilePath
92 class HasSettings env where
93 settings :: Getter env Settings
95 devSettings :: FilePath -> IO Settings
96 devSettings jwkFile = do
97 jwkExists <- doesFileExist jwkFile
98 when (not jwkExists) $ writeKey jwkFile
99 jwk <- readKey jwkFile
101 { _allowedOrigin = "http://localhost:8008"
102 , _allowedHost = "localhost:3000"
104 , _logLevelLimit = LevelDebug
105 -- , _dbServer = "localhost"
106 , _sendLoginEmails = LogEmailToConsole
107 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
108 , _fileFolder = "data"
109 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
110 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
113 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
117 reqSetting :: FromHttpApiData a => Text -> IO a
119 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
120 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
122 optSetting :: FromHttpApiData a => Text -> a -> IO a
123 optSetting name d = do
124 me <- lookupEnv (unpack name)
127 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
129 --settingsFromEnvironment :: IO Settings
130 --settingsFromEnvironment =
131 -- Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
132 -- <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
133 -- <*> optSetting "PORT" 3000
134 -- <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
135 -- <*> reqSetting "DB_SERVER"
136 -- <*> (parseJwk <$> reqSetting "JWT_SECRET")
137 -- <*> optSetting "SEND_EMAIL" SendEmailViaAws
139 data FireWall = FireWall { unFireWall :: Bool }
142 { _env_settings :: !Settings
143 , _env_logger :: !LoggerSet
144 , _env_conn :: !Connection
145 , _env_repo :: !RepoEnv
146 , _env_manager :: !Manager
147 , _env_self_url :: !BaseUrl
148 , _env_scrapers :: !ScrapersEnv
154 instance HasConnection Env where
155 connection = env_conn
157 instance HasRepoVar Env where
158 repoVar = repoEnv . repoVar
160 instance HasRepoSaver Env where
161 repoSaver = repoEnv . repoSaver
163 instance HasRepo Env where
166 instance HasSettings Env where
167 settings = env_settings
169 instance Servant.Job.Core.HasEnv Env (Job ScraperStatus ScraperStatus) where
170 _env = env_scrapers . Servant.Job.Core._env
172 instance HasJobEnv Env ScraperStatus ScraperStatus where
173 job_env = env_scrapers
175 data MockEnv = MockEnv
176 { _menv_firewall :: !FireWall
182 -- | TODO add this path in Settings
187 repoSnapshot :: FilePath
188 repoSnapshot = repoDir <> "/repo.json"
190 -- | TODO add hard coded file in Settings
191 -- This assumes we own the lock on repoSnapshot.
192 repoSaverAction :: ToJSON a => a -> IO ()
193 repoSaverAction a = do
194 withTempFile "repos" "tmp-repo.json" $ \fp h -> do
195 -- printDebug "repoSaverAction" fp
198 renameFile fp repoSnapshot
200 mkRepoSaver :: MVar NgramsRepo -> IO (IO ())
201 mkRepoSaver repo_var = mkDebounce settings
203 settings = defaultDebounceSettings
204 { debounceFreq = 1000000 -- 1 second
205 , debounceAction = withMVar repo_var repoSaverAction
206 -- Here this not only `readMVar` but `takeMVar`.
207 -- Namely while repoSaverAction is saving no other change
208 -- can be made to the MVar.
209 -- This might be not efficent and thus reconsidered later.
210 -- However this enables to safely perform a *final* save.
213 -- Add a new MVar just for saving.
216 readRepoEnv :: IO RepoEnv
218 -- Does file exist ? :: Bool
219 _repoDir <- createDirectoryIfMissing True repoDir
221 repoFile <- doesFileExist repoSnapshot
223 -- Is file not empty ? :: Bool
224 repoExists <- if repoFile
225 then (>0) <$> getFileSize repoSnapshot
228 mlock <- tryLockFile repoSnapshot Exclusive
229 lock <- maybe (panic "Repo file already locked") pure mlock
234 e_repo <- eitherDecodeFileStrict repoSnapshot
235 repo <- either fail pure e_repo
236 let archive = repoSnapshot <> ".v" <> show (repo ^. r_version)
237 copyFile repoSnapshot archive
241 -- TODO save in DB here
242 saver <- mkRepoSaver mvar
243 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
245 devJwkFile :: FilePath
246 devJwkFile = "dev.jwk"
248 newEnv :: PortNumber -> FilePath -> IO Env
249 newEnv port file = do
250 manager <- newTlsManager
251 settings <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
252 when (port /= settings ^. appPort) $
253 panic "TODO: conflicting settings of port"
255 self_url <- parseBaseUrl $ "http://0.0.0.0:" <> show port
256 param <- databaseParameters file
257 conn <- connect param
259 scrapers_env <- newJobEnv defaultSettings manager
260 logger <- newStderrLoggerSet defaultBufSize
263 { _env_settings = settings
264 , _env_logger = logger
267 , _env_manager = manager
268 , _env_scrapers = scrapers_env
269 , _env_self_url = self_url
273 { _dev_env_conn :: !Connection
274 , _dev_env_repo :: !RepoEnv
275 , _dev_env_settings :: !Settings
280 instance HasConnection DevEnv where
281 connection = dev_env_conn
283 instance HasRepoVar DevEnv where
284 repoVar = repoEnv . repoVar
286 instance HasRepoSaver DevEnv where
287 repoSaver = repoEnv . repoSaver
289 instance HasRepo DevEnv where
290 repoEnv = dev_env_repo
292 instance HasSettings DevEnv where
293 settings = dev_env_settings
295 cleanEnv :: HasRepo env => env -> IO ()
297 r <- takeMVar (env ^. repoEnv . renv_var)
299 unlockFile (env ^. repoEnv . renv_lock)
301 withDevEnv :: FilePath -> (DevEnv -> IO a) -> IO a
302 withDevEnv iniPath k = do
304 k env `finally` cleanEnv env
308 param <- databaseParameters iniPath
309 conn <- connect param
311 setts <- devSettings devJwkFile
313 { _dev_env_conn = conn
314 , _dev_env_repo = repo
315 , _dev_env_settings = setts
318 -- | Run Cmd Sugar for the Repl (GHCI)
319 runCmdRepl :: Show err => Cmd' DevEnv err a -> IO a
320 runCmdRepl f = withDevEnv "gargantext.ini" $ \env -> runCmdDev env f
322 runCmdReplServantErr :: Cmd' DevEnv ServerError a -> IO a
323 runCmdReplServantErr = runCmdRepl
326 -- In particular this writes the repo file after running
328 -- This function is constrained to the DevEnv rather than
329 -- using HasConnection and HasRepoVar.
330 runCmdDev :: Show err => DevEnv -> Cmd' DevEnv err a -> IO a
332 (either (fail . show) pure =<< runCmd env f)
334 runReaderT saveRepo env
337 runCmdDevNoErr :: DevEnv -> Cmd' DevEnv () a -> IO a
338 runCmdDevNoErr = runCmdDev
341 runCmdDevServantErr :: DevEnv -> Cmd' DevEnv ServerError a -> IO a
342 runCmdDevServantErr = runCmdDev