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 Codec.Serialise (Serialise(), serialise, deserialise)
22 import Control.Concurrent
23 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
25 import Control.Monad.Logger
26 import Control.Monad.Reader
27 import Data.Maybe (fromMaybe)
28 import Data.Pool (Pool, createPool)
29 import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
30 import Gargantext.Core.NodeStory
31 import Gargantext.Prelude.Config (GargConfig(..), {-gc_repofilepath,-} readConfig)
32 import Network.HTTP.Client.TLS (newTlsManager)
33 import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
34 import Servant.Client (parseBaseUrl)
35 import Servant.Job.Async (newJobEnv, defaultSettings)
36 import System.Directory
37 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
38 import System.IO (FilePath, hClose)
39 import System.IO.Temp (withTempFile)
40 import System.Log.FastLogger
41 import qualified Data.ByteString.Lazy as L
44 import Gargantext.API.Admin.EnvTypes
45 import Gargantext.API.Admin.Types
46 import Gargantext.API.Ngrams.Types (NgramsRepo, HasRepo(..), RepoEnv(..), r_version, initRepo, renv_var, renv_lock)
47 import Gargantext.Database.Prelude (databaseParameters, HasConfig(..))
48 import Gargantext.Prelude
49 import Gargantext.Prelude.Config (gc_repofilepath)
50 import qualified Gargantext.Prelude.Mail as Mail
52 devSettings :: FilePath -> IO Settings
53 devSettings jwkFile = do
54 jwkExists <- doesFileExist jwkFile
55 when (not jwkExists) $ writeKey jwkFile
56 jwk <- readKey jwkFile
58 { _allowedOrigin = "http://localhost:8008"
59 , _allowedHost = "localhost:3000"
61 , _logLevelLimit = LevelDebug
62 -- , _dbServer = "localhost"
63 , _sendLoginEmails = LogEmailToConsole
64 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
65 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
66 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
69 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
72 import System.Environment (lookupEnv)
74 reqSetting :: FromHttpApiData a => Text -> IO a
76 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
77 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
79 optSetting :: FromHttpApiData a => Text -> a -> IO a
80 optSetting name d = do
81 me <- lookupEnv (unpack name)
84 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
86 settingsFromEnvironment :: IO Settings
87 settingsFromEnvironment =
88 Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
89 <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
90 <*> optSetting "PORT" 3000
91 <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
92 <*> reqSetting "DB_SERVER"
93 <*> (parseJwk <$> reqSetting "JWT_SECRET")
94 <*> optSetting "SEND_EMAIL" SendEmailViaAws
97 -----------------------------------------------------------------------
98 -- | RepoDir FilePath configuration
99 type RepoDirFilePath = FilePath
101 repoSnapshot :: RepoDirFilePath -> FilePath
102 repoSnapshot repoDir = repoDir <> "/repo.cbor"
106 -- This assumes we own the lock on repoSnapshot.
107 repoSaverAction :: RepoDirFilePath -> Serialise a => a -> IO ()
108 repoSaverAction repoDir a = do
109 withTempFile repoDir "tmp-repo.cbor" $ \fp h -> do
110 printDebug "repoSaverAction" fp
111 L.hPut h $ serialise a
113 renameFile fp (repoSnapshot repoDir)
118 -- The use of mkDebounce makes sure that repoSaverAction is not called too often.
119 -- If repoSaverAction start taking more time than the debounceFreq then it should
121 mkRepoSaver :: RepoDirFilePath -> MVar NgramsRepo -> IO (IO ())
122 mkRepoSaver repoDir repo_var = mkDebounce settings'
124 settings' = defaultDebounceSettings
125 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
126 , debounceAction = withMVar repo_var (repoSaverAction repoDir)
127 -- Here this not only `readMVar` but `takeMVar`.
128 -- Namely while repoSaverAction is saving no other change
129 -- can be made to the MVar.
130 -- This might be not efficent and thus reconsidered later.
131 -- However this enables to safely perform a *final* save.
134 -- Add a new MVar just for saving.
137 readRepoEnv :: FilePath -> IO RepoEnv
138 readRepoEnv repoDir = do
139 -- Does file exist ? :: Bool
140 _repoDir <- createDirectoryIfMissing True repoDir
142 repoFile <- doesFileExist (repoSnapshot repoDir)
144 -- Is file not empty ? :: Bool
145 repoExists <- if repoFile
146 then (>0) <$> getFileSize (repoSnapshot repoDir)
149 mlock <- tryLockFile (repoSnapshot repoDir) Exclusive
150 lock <- maybe (panic "Repo file already locked") pure mlock
155 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
156 repo <- deserialise <$> L.readFile (repoSnapshot repoDir)
157 -- repo <- either fail pure e_repo
158 let archive = (repoSnapshot repoDir) <> ".v" <> show (repo ^. r_version)
159 copyFile (repoSnapshot repoDir) archive
163 -- TODO save in DB here
164 saver <- mkRepoSaver repoDir mvar
165 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
168 devJwkFile :: FilePath
169 devJwkFile = "dev.jwk"
171 newEnv :: PortNumber -> FilePath -> IO Env
172 newEnv port file = do
173 manager_env <- newTlsManager
174 settings' <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
175 when (port /= settings' ^. appPort) $
176 panic "TODO: conflicting settings of port"
178 config_env <- readConfig file
179 self_url_env <- parseBaseUrl $ "http://0.0.0.0:" <> show port
180 dbParam <- databaseParameters file
181 pool <- newPool dbParam
182 repo <- readRepoEnv (_gc_repofilepath config_env)
183 nodeStory_env <- readNodeStoryEnv (_gc_repofilepath config_env)
184 scrapers_env <- newJobEnv defaultSettings manager_env
185 logger <- newStderrLoggerSet defaultBufSize
186 config_mail <- Mail.readConfig file
189 { _env_settings = settings'
190 , _env_logger = logger
193 , _env_nodeStory = nodeStory_env
194 , _env_manager = manager_env
195 , _env_scrapers = scrapers_env
196 , _env_self_url = self_url_env
197 , _env_config = config_env
198 , _env_mail = config_mail
201 newPool :: ConnectInfo -> IO (Pool Connection)
202 newPool param = createPool (connect param) close 1 (60*60) 8
205 cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
207 r <- takeMVar (env ^. repoEnv . renv_var)
208 repoSaverAction (env ^. hasConfig . gc_repofilepath) r
209 unlockFile (env ^. repoEnv . renv_lock)