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
13 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
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)
24 import Control.Exception (finally)
26 import Control.Monad.Logger
27 import Control.Monad.Reader
28 import Data.Maybe (fromMaybe)
29 import Data.Pool (Pool, createPool)
31 import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
32 import Network.HTTP.Client.TLS (newTlsManager)
34 import Servant.Auth.Server (defaultJWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
35 import Servant.Client (parseBaseUrl)
36 import Servant.Job.Async (newJobEnv, defaultSettings)
37 import System.Directory
38 import System.Environment (lookupEnv)
39 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
40 import System.IO (FilePath, hClose)
41 import System.IO.Temp (withTempFile)
42 import System.Log.FastLogger
43 import qualified Data.ByteString.Lazy as L
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.API.Ngrams (saveRepo)
48 import Gargantext.Database.Prelude (databaseParameters, Cmd', runCmd, HasConfig(..))
49 import Gargantext.Prelude
50 import Gargantext.Prelude.Config (GargConfig(..), gc_repofilepath, readConfig, defaultConfig)
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
67 , _config = defaultConfig
70 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
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
96 -----------------------------------------------------------------------
97 -- | RepoDir FilePath configuration
98 type RepoDirFilePath = FilePath
100 repoSnapshot :: RepoDirFilePath -> FilePath
101 repoSnapshot repoDir = repoDir <> "/repo.cbor"
103 -- | TODO add hard coded file in Settings
104 -- This assumes we own the lock on repoSnapshot.
105 repoSaverAction :: RepoDirFilePath -> Serialise a => a -> IO ()
106 repoSaverAction repoDir a = do
107 withTempFile "repos" "tmp-repo.cbor" $ \fp h -> do
108 printDebug "repoSaverAction" fp
109 L.hPut h $ serialise a
111 renameFile fp (repoSnapshot repoDir)
113 mkRepoSaver :: RepoDirFilePath -> MVar NgramsRepo -> IO (IO ())
114 mkRepoSaver repoDir repo_var = mkDebounce settings
116 settings = defaultDebounceSettings
117 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
118 , debounceAction = withMVar repo_var (repoSaverAction repoDir)
119 -- Here this not only `readMVar` but `takeMVar`.
120 -- Namely while repoSaverAction is saving no other change
121 -- can be made to the MVar.
122 -- This might be not efficent and thus reconsidered later.
123 -- However this enables to safely perform a *final* save.
126 -- Add a new MVar just for saving.
129 readRepoEnv :: FilePath -> IO RepoEnv
130 readRepoEnv repoDir = do
131 -- Does file exist ? :: Bool
132 _repoDir <- createDirectoryIfMissing True repoDir
134 repoFile <- doesFileExist (repoSnapshot repoDir)
136 -- Is file not empty ? :: Bool
137 repoExists <- if repoFile
138 then (>0) <$> getFileSize (repoSnapshot repoDir)
141 mlock <- tryLockFile (repoSnapshot repoDir) Exclusive
142 lock <- maybe (panic "Repo file already locked") pure mlock
147 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
148 repo <- deserialise <$> L.readFile (repoSnapshot repoDir)
149 -- repo <- either fail pure e_repo
150 let archive = (repoSnapshot repoDir) <> ".v" <> show (repo ^. r_version)
151 copyFile (repoSnapshot repoDir) archive
155 -- TODO save in DB here
156 saver <- mkRepoSaver repoDir mvar
157 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
159 devJwkFile :: FilePath
160 devJwkFile = "dev.jwk"
162 newEnv :: PortNumber -> FilePath -> IO Env
163 newEnv port file = do
164 manager <- newTlsManager
165 settings <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
166 when (port /= settings ^. appPort) $
167 panic "TODO: conflicting settings of port"
169 config <- readConfig file
170 self_url <- parseBaseUrl $ "http://0.0.0.0:" <> show port
171 dbParam <- databaseParameters file
172 pool <- newPool dbParam
173 repo <- readRepoEnv (_gc_repofilepath config)
174 scrapers_env <- newJobEnv defaultSettings manager
175 logger <- newStderrLoggerSet defaultBufSize
178 { _env_settings = settings
179 , _env_logger = logger
182 , _env_manager = manager
183 , _env_scrapers = scrapers_env
184 , _env_self_url = self_url
185 , _env_gargConfig = config
188 newPool :: ConnectInfo -> IO (Pool Connection)
189 newPool param = createPool (connect param) close 1 (60*60) 8
191 cleanEnv :: (HasConfig env, HasRepo env) => env -> IO ()
193 r <- takeMVar (env ^. repoEnv . renv_var)
194 repoSaverAction (env ^. hasConfig . gc_repofilepath) r
195 unlockFile (env ^. repoEnv . renv_lock)
197 type IniPath = FilePath
198 withDevEnv :: IniPath -> (DevEnv -> IO a) -> IO a
199 withDevEnv iniPath k = do
201 k env `finally` cleanEnv env
205 config <- readConfig iniPath
206 dbParam <- databaseParameters iniPath
207 pool <- newPool dbParam
208 repo <- readRepoEnv (_gc_repofilepath config)
209 setts <- devSettings devJwkFile
211 { _dev_env_pool = pool
212 , _dev_env_repo = repo
213 , _dev_env_settings = setts
214 , _dev_env_config = config
217 -- | Run Cmd Sugar for the Repl (GHCI)
219 runCmdRepl :: Show err => Cmd' DevEnv err a -> IO a
220 runCmdRepl f = withDevEnv "gargantext.ini" $ \env -> runCmdDev env f
222 runCmdReplServantErr :: Cmd' DevEnv ServerError a -> IO a
223 runCmdReplServantErr = runCmdRepl
226 -- In particular this writes the repo file after running
228 -- This function is constrained to the DevEnv rather than
229 -- using HasConnectionPool and HasRepoVar.
230 runCmdDev :: Show err => DevEnv -> Cmd' DevEnv err a -> IO a
232 (either (fail . show) pure =<< runCmd env f)
234 runReaderT saveRepo env
237 runCmdDevNoErr :: DevEnv -> Cmd' DevEnv () a -> IO a
238 runCmdDevNoErr = runCmdDev
241 runCmdDevServantErr :: DevEnv -> Cmd' DevEnv ServerError a -> IO a
242 runCmdDevServantErr = runCmdDev