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.ByteString (ByteString)
29 import Data.Either (either)
30 import Data.Maybe (fromMaybe)
31 import Data.Pool (Pool, createPool)
33 import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
35 import GHC.Generics (Generic)
36 import Gargantext.API.Admin.Orchestrator.Types
37 import Gargantext.API.Ngrams (NgramsRepo, HasRepoVar(..), HasRepoSaver(..), HasRepo(..), RepoEnv(..), r_version, saveRepo, initRepo, renv_var, renv_lock)
38 import Gargantext.Database.Prelude (databaseParameters, HasConnectionPool(..), Cmd', runCmd, HasConfig(..))
39 import Gargantext.Prelude
41 import Network.HTTP.Client (Manager)
42 import Network.HTTP.Client.TLS (newTlsManager)
43 import Prelude (Bounded(), fail)
45 import Servant.Auth.Server (defaultJWTSettings, JWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
46 import Servant.Client (BaseUrl, parseBaseUrl)
47 import Servant.Job.Async (newJobEnv, defaultSettings, HasJobEnv(..), Job)
48 import System.Directory
49 import System.Environment (lookupEnv)
50 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
51 import System.IO (FilePath, hClose)
52 import System.IO.Temp (withTempFile)
53 import System.Log.FastLogger
54 import Web.HttpApiData (parseUrlPiece)
55 import qualified Data.ByteString.Lazy as L
56 import qualified Servant.Job.Core
57 import Gargantext.Config (GargConfig(), readConfig, defaultConfig)
61 data SendEmailType = SendEmailViaAws
64 deriving (Show, Read, Enum, Bounded, Generic)
67 data Settings = Settings
68 { _allowedOrigin :: ByteString -- allowed origin for CORS
69 , _allowedHost :: ByteString -- allowed host for CORS
70 , _appPort :: PortNumber
71 , _logLevelLimit :: LogLevel -- log level from the monad-logger package
72 -- , _dbServer :: Text
73 -- ^ this is not used yet
74 , _jwtSettings :: JWTSettings
75 , _cookieSettings :: CookieSettings
76 , _sendLoginEmails :: SendEmailType
77 , _scrapydUrl :: BaseUrl
78 , _fileFolder :: FilePath
79 , _config :: GargConfig
84 class HasSettings env where
85 settings :: Getter env Settings
87 devSettings :: FilePath -> IO Settings
88 devSettings jwkFile = do
89 jwkExists <- doesFileExist jwkFile
90 when (not jwkExists) $ writeKey jwkFile
91 jwk <- readKey jwkFile
93 { _allowedOrigin = "http://localhost:8008"
94 , _allowedHost = "localhost:3000"
96 , _logLevelLimit = LevelDebug
97 -- , _dbServer = "localhost"
98 , _sendLoginEmails = LogEmailToConsole
99 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
100 , _fileFolder = "data"
101 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
102 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
103 , _config = defaultConfig
106 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
110 reqSetting :: FromHttpApiData a => Text -> IO a
112 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
113 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
115 optSetting :: FromHttpApiData a => Text -> a -> IO a
116 optSetting name d = do
117 me <- lookupEnv (unpack name)
120 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
122 --settingsFromEnvironment :: IO Settings
123 --settingsFromEnvironment =
124 -- Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
125 -- <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
126 -- <*> optSetting "PORT" 3000
127 -- <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
128 -- <*> reqSetting "DB_SERVER"
129 -- <*> (parseJwk <$> reqSetting "JWT_SECRET")
130 -- <*> optSetting "SEND_EMAIL" SendEmailViaAws
132 data FireWall = FireWall { unFireWall :: Bool }
135 { _env_settings :: !Settings
136 , _env_logger :: !LoggerSet
137 , _env_pool :: !(Pool Connection)
138 , _env_repo :: !RepoEnv
139 , _env_manager :: !Manager
140 , _env_self_url :: !BaseUrl
141 , _env_scrapers :: !ScrapersEnv
142 , _env_gargConfig :: !GargConfig
148 instance HasConfig Env where
149 hasConfig = env_gargConfig
151 instance HasConnectionPool Env where
154 instance HasRepoVar Env where
155 repoVar = repoEnv . repoVar
157 instance HasRepoSaver Env where
158 repoSaver = repoEnv . repoSaver
160 instance HasRepo Env where
163 instance HasSettings Env where
164 settings = env_settings
166 instance Servant.Job.Core.HasEnv Env (Job JobLog JobLog) where
167 _env = env_scrapers . Servant.Job.Core._env
169 instance HasJobEnv Env JobLog JobLog where
170 job_env = env_scrapers
172 data MockEnv = MockEnv
173 { _menv_firewall :: !FireWall
179 -- | TODO add this path in Settings
184 repoSnapshot :: FilePath
185 repoSnapshot = repoDir <> "/repo.json"
187 -- | TODO add hard coded file in Settings
188 -- This assumes we own the lock on repoSnapshot.
189 repoSaverAction :: Serialise a => a -> IO ()
190 repoSaverAction a = do
191 withTempFile "repos" "tmp-repo.json" $ \fp h -> do
192 printDebug "repoSaverAction" fp
193 L.hPut h $ serialise a
195 renameFile fp repoSnapshot
197 mkRepoSaver :: MVar NgramsRepo -> IO (IO ())
198 mkRepoSaver repo_var = mkDebounce settings
200 settings = defaultDebounceSettings
201 { debounceFreq = let n = 6 :: Int in 10^n -- 1 second
202 , debounceAction = withMVar repo_var repoSaverAction
203 -- Here this not only `readMVar` but `takeMVar`.
204 -- Namely while repoSaverAction is saving no other change
205 -- can be made to the MVar.
206 -- This might be not efficent and thus reconsidered later.
207 -- However this enables to safely perform a *final* save.
210 -- Add a new MVar just for saving.
213 readRepoEnv :: IO RepoEnv
215 -- Does file exist ? :: Bool
216 _repoDir <- createDirectoryIfMissing True repoDir
218 repoFile <- doesFileExist repoSnapshot
220 -- Is file not empty ? :: Bool
221 repoExists <- if repoFile
222 then (>0) <$> getFileSize repoSnapshot
225 mlock <- tryLockFile repoSnapshot Exclusive
226 lock <- maybe (panic "Repo file already locked") pure mlock
231 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
232 repo <- deserialise <$> L.readFile repoSnapshot
233 -- repo <- either fail pure e_repo
234 let archive = repoSnapshot <> ".v" <> show (repo ^. r_version)
235 copyFile repoSnapshot archive
239 -- TODO save in DB here
240 saver <- mkRepoSaver mvar
241 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
243 devJwkFile :: FilePath
244 devJwkFile = "dev.jwk"
246 newEnv :: PortNumber -> FilePath -> IO Env
247 newEnv port file = do
248 manager <- newTlsManager
249 settings <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
250 when (port /= settings ^. appPort) $
251 panic "TODO: conflicting settings of port"
253 self_url <- parseBaseUrl $ "http://0.0.0.0:" <> show port
254 param <- databaseParameters file
255 pool <- newPool param
257 scrapers_env <- newJobEnv defaultSettings manager
258 logger <- newStderrLoggerSet defaultBufSize
259 config <- readConfig file
262 { _env_settings = settings
263 , _env_logger = logger
266 , _env_manager = manager
267 , _env_scrapers = scrapers_env
268 , _env_self_url = self_url
269 , _env_gargConfig = config
272 newPool :: ConnectInfo -> IO (Pool Connection)
273 newPool param = createPool (connect param) close 1 (60*60) 8
276 { _dev_env_pool :: !(Pool Connection)
277 , _dev_env_repo :: !RepoEnv
278 , _dev_env_settings :: !Settings
279 , _dev_env_config :: !GargConfig
284 instance HasConfig DevEnv where
285 hasConfig = dev_env_config
287 instance HasConnectionPool DevEnv where
288 connPool = dev_env_pool
290 instance HasRepoVar DevEnv where
291 repoVar = repoEnv . repoVar
293 instance HasRepoSaver DevEnv where
294 repoSaver = repoEnv . repoSaver
296 instance HasRepo DevEnv where
297 repoEnv = dev_env_repo
299 instance HasSettings DevEnv where
300 settings = dev_env_settings
302 cleanEnv :: HasRepo env => env -> IO ()
304 r <- takeMVar (env ^. repoEnv . renv_var)
306 unlockFile (env ^. repoEnv . renv_lock)
308 withDevEnv :: FilePath -> (DevEnv -> IO a) -> IO a
309 withDevEnv iniPath k = do
311 k env `finally` cleanEnv env
315 param <- databaseParameters iniPath
316 pool <- newPool param
318 setts <- devSettings devJwkFile
319 config <- readConfig iniPath
321 { _dev_env_pool = pool
322 , _dev_env_repo = repo
323 , _dev_env_settings = setts
324 , _dev_env_config = config
327 -- | Run Cmd Sugar for the Repl (GHCI)
329 runCmdRepl :: Show err => Cmd' DevEnv err a -> IO a
330 runCmdRepl f = withDevEnv "gargantext.ini" $ \env -> runCmdDev env f
332 runCmdReplServantErr :: Cmd' DevEnv ServerError a -> IO a
333 runCmdReplServantErr = runCmdRepl
336 -- In particular this writes the repo file after running
338 -- This function is constrained to the DevEnv rather than
339 -- using HasConnectionPool and HasRepoVar.
340 runCmdDev :: Show err => DevEnv -> Cmd' DevEnv err a -> IO a
342 (either (fail . show) pure =<< runCmd env f)
344 runReaderT saveRepo env
347 runCmdDevNoErr :: DevEnv -> Cmd' DevEnv () a -> IO a
348 runCmdDevNoErr = runCmdDev
351 runCmdDevServantErr :: DevEnv -> Cmd' DevEnv ServerError a -> IO a
352 runCmdDevServantErr = runCmdDev