2 Module : Gargantext.API.Admin.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
15 {-# LANGUAGE ScopedTypeVariables #-}
16 {-# LANGUAGE TemplateHaskell #-}
18 module Gargantext.API.Admin.Settings
21 -- import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
22 import Codec.Serialise (Serialise(), serialise)
24 import Control.Monad.Logger (LogLevel(..))
25 import Control.Monad.Reader
26 import Data.Maybe (fromMaybe)
27 import Data.Pool (Pool, createPool)
28 import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
29 import Gargantext.Core.NodeStory
30 import Gargantext.Prelude.Config ({-GargConfig(..),-} {-gc_repofilepath,-} readConfig)
31 import Network.HTTP.Client.TLS (newTlsManager)
32 import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
33 import Servant.Client (parseBaseUrl)
34 import Servant.Job.Async (newJobEnv, defaultSettings)
35 import System.Directory
36 -- import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
37 import System.IO (FilePath, hClose)
38 import System.IO.Temp (withTempFile)
39 import System.Log.FastLogger
40 import qualified Data.ByteString.Lazy as L
43 import Gargantext.API.Admin.EnvTypes
44 import Gargantext.API.Admin.Types
45 -- import Gargantext.API.Ngrams.Types (NgramsRepo, HasRepo(..), RepoEnv(..), r_version, initRepo, renv_var, renv_lock)
46 import Gargantext.Database.Prelude (databaseParameters)
47 import Gargantext.Prelude
48 -- import Gargantext.Prelude.Config (gc_repofilepath)
49 import qualified Gargantext.Prelude.Mail as Mail
50 import qualified Gargantext.Utils.Jobs as Jobs
51 import qualified Gargantext.Utils.Jobs.Monad as Jobs
52 import qualified Gargantext.Utils.Jobs.Queue as Jobs
54 devSettings :: FilePath -> IO Settings
55 devSettings jwkFile = do
56 jwkExists <- doesFileExist jwkFile
57 when (not jwkExists) $ writeKey jwkFile
58 jwk <- readKey jwkFile
60 { _allowedOrigin = "http://localhost:8008"
61 , _allowedHost = "localhost:3000"
63 , _logLevelLimit = LevelDebug
64 -- , _dbServer = "localhost"
65 , _sendLoginEmails = LogEmailToConsole
66 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
67 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
68 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
71 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
74 import System.Environment (lookupEnv)
76 reqSetting :: FromHttpApiData a => Text -> IO a
78 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
79 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
81 optSetting :: FromHttpApiData a => Text -> a -> IO a
82 optSetting name d = do
83 me <- lookupEnv (unpack name)
86 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
88 settingsFromEnvironment :: IO Settings
89 settingsFromEnvironment =
90 Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
91 <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
92 <*> optSetting "PORT" 3000
93 <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
94 <*> reqSetting "DB_SERVER"
95 <*> (parseJwk <$> reqSetting "JWT_SECRET")
96 <*> optSetting "SEND_EMAIL" SendEmailViaAws
99 -----------------------------------------------------------------------
100 -- | RepoDir FilePath configuration
101 type RepoDirFilePath = FilePath
103 repoSnapshot :: RepoDirFilePath -> FilePath
104 repoSnapshot repoDir = repoDir <> "/repo.cbor"
108 -- This assumes we own the lock on repoSnapshot.
109 repoSaverAction :: RepoDirFilePath -> Serialise a => a -> IO ()
110 repoSaverAction repoDir a = do
111 withTempFile repoDir "tmp-repo.cbor" $ \fp h -> do
112 -- printDebug "repoSaverAction" fp
113 L.hPut h $ serialise a
115 renameFile fp (repoSnapshot repoDir)
120 -- The use of mkDebounce makes sure that repoSaverAction is not called too often.
121 -- If repoSaverAction start taking more time than the debounceFreq then it should
123 mkRepoSaver :: RepoDirFilePath -> MVar NgramsRepo -> IO (IO ())
124 mkRepoSaver repoDir repo_var = mkDebounce settings'
126 settings' = defaultDebounceSettings
127 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
128 , debounceAction = withMVar repo_var (repoSaverAction repoDir)
129 -- Here this not only `readMVar` but `takeMVar`.
130 -- Namely while repoSaverAction is saving no other change
131 -- can be made to the MVar.
132 -- This might be not efficent and thus reconsidered later.
133 -- However this enables to safely perform a *final* save.
136 -- Add a new MVar just for saving.
141 readRepoEnv :: FilePath -> IO RepoEnv
142 readRepoEnv repoDir = do
143 -- Does file exist ? :: Bool
144 _repoDir <- createDirectoryIfMissing True repoDir
146 repoFile <- doesFileExist (repoSnapshot repoDir)
148 -- Is file not empty ? :: Bool
149 repoExists <- if repoFile
150 then (>0) <$> getFileSize (repoSnapshot repoDir)
153 mlock <- tryLockFile (repoSnapshot repoDir) Exclusive
154 lock <- maybe (panic "Repo file already locked") pure mlock
159 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
160 repo <- deserialise <$> L.readFile (repoSnapshot repoDir)
161 -- repo <- either fail pure e_repo
162 let archive = (repoSnapshot repoDir) <> ".v" <> show (repo ^. r_version)
163 copyFile (repoSnapshot repoDir) archive
167 -- TODO save in DB here
168 saver <- mkRepoSaver repoDir mvar
169 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
172 devJwkFile :: FilePath
173 devJwkFile = "dev.jwk"
175 newEnv :: PortNumber -> FilePath -> IO Env
176 newEnv port file = do
177 manager_env <- newTlsManager
178 settings' <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
179 when (port /= settings' ^. appPort) $
180 panic "TODO: conflicting settings of port"
182 config_env <- readConfig file
183 prios <- Jobs.readPrios (file <> ".jobs")
184 let prios' = Jobs.applyPrios prios Jobs.defaultPrios
185 putStrLn $ "Overrides: " <> show prios
186 putStrLn $ "New priorities: " <> show prios'
187 self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port
188 dbParam <- databaseParameters file
189 pool <- newPool dbParam
190 --nodeStory_env <- readNodeStoryEnv (_gc_repofilepath config_env)
191 nodeStory_env <- readNodeStoryEnv pool
192 scrapers_env <- newJobEnv defaultSettings manager_env
194 secret <- Jobs.genSecret
195 jobs_env <- Jobs.newJobEnv (Jobs.defaultJobSettings 1 secret) prios' manager_env
196 logger <- newStderrLoggerSet defaultBufSize
197 config_mail <- Mail.readConfig file
200 { _env_settings = settings'
201 , _env_logger = logger
203 , _env_nodeStory = nodeStory_env
204 , _env_manager = manager_env
205 , _env_scrapers = scrapers_env
206 , _env_jobs = jobs_env
207 , _env_self_url = self_url_env
208 , _env_config = config_env
209 , _env_mail = config_mail
212 newPool :: ConnectInfo -> IO (Pool Connection)
213 newPool param = createPool (connect param) close 1 (60*60) 8
216 cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
218 r <- takeMVar (env ^. repoEnv . renv_var)
219 repoSaverAction (env ^. hasConfig . gc_repofilepath) r
220 unlockFile (env ^. repoEnv . renv_lock)