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 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.Admin.Settings
29 import Codec.Serialise (Serialise(), serialise, deserialise)
30 import Control.Concurrent
31 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
32 import Control.Exception (finally)
34 import Control.Monad.Logger
35 import Control.Monad.Reader
36 import Data.ByteString (ByteString)
37 import Data.Either (either)
38 import Data.Maybe (fromMaybe)
39 import Data.Pool (Pool, createPool)
41 import Database.PostgreSQL.Simple (Connection, connect, close, ConnectInfo)
43 import GHC.Generics (Generic)
44 import Gargantext.API.Admin.Orchestrator.Types
45 import Gargantext.API.Ngrams (NgramsRepo, HasRepoVar(..), HasRepoSaver(..), HasRepo(..), RepoEnv(..), r_version, saveRepo, initRepo, renv_var, renv_lock)
46 import Gargantext.Database.Prelude (databaseParameters, HasConnectionPool(..), Cmd', runCmd)
47 import Gargantext.Prelude
48 import Network.HTTP.Client (Manager)
49 import Network.HTTP.Client.TLS (newTlsManager)
50 import Prelude (Bounded(), fail)
52 import Servant.Auth.Server (defaultJWTSettings, JWTSettings, CookieSettings(..), XsrfCookieSettings(..), defaultCookieSettings, defaultXsrfCookieSettings, readKey, writeKey)
53 import Servant.Client (BaseUrl, parseBaseUrl)
54 import Servant.Job.Async (newJobEnv, defaultSettings, HasJobEnv(..), Job)
55 import System.Directory
56 import System.Environment (lookupEnv)
57 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
58 import System.IO (FilePath, hClose)
59 import System.IO.Temp (withTempFile)
60 import System.Log.FastLogger
61 import Web.HttpApiData (parseUrlPiece)
62 import qualified Data.ByteString.Lazy as L
63 import qualified Servant.Job.Core
68 data SendEmailType = SendEmailViaAws
71 deriving (Show, Read, Enum, Bounded, Generic)
74 data Settings = Settings
75 { _allowedOrigin :: ByteString -- allowed origin for CORS
76 , _allowedHost :: ByteString -- allowed host for CORS
77 , _appPort :: PortNumber
78 , _logLevelLimit :: LogLevel -- log level from the monad-logger package
79 -- , _dbServer :: Text
80 -- ^ this is not used yet
81 , _jwtSettings :: JWTSettings
82 , _cookieSettings :: CookieSettings
83 , _sendLoginEmails :: SendEmailType
84 , _scrapydUrl :: BaseUrl
85 , _fileFolder :: FilePath
90 class HasSettings env where
91 settings :: Getter env Settings
93 devSettings :: FilePath -> IO Settings
94 devSettings jwkFile = do
95 jwkExists <- doesFileExist jwkFile
96 when (not jwkExists) $ writeKey jwkFile
97 jwk <- readKey jwkFile
99 { _allowedOrigin = "http://localhost:8008"
100 , _allowedHost = "localhost:3000"
102 , _logLevelLimit = LevelDebug
103 -- , _dbServer = "localhost"
104 , _sendLoginEmails = LogEmailToConsole
105 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
106 , _fileFolder = "data"
107 , _cookieSettings = defaultCookieSettings { cookieXsrfSetting = Just xsrfCookieSetting } -- TODO-SECURITY tune
108 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
111 xsrfCookieSetting = defaultXsrfCookieSettings { xsrfExcludeGet = True }
115 reqSetting :: FromHttpApiData a => Text -> IO a
117 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
118 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
120 optSetting :: FromHttpApiData a => Text -> a -> IO a
121 optSetting name d = do
122 me <- lookupEnv (unpack name)
125 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
127 --settingsFromEnvironment :: IO Settings
128 --settingsFromEnvironment =
129 -- Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
130 -- <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
131 -- <*> optSetting "PORT" 3000
132 -- <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
133 -- <*> reqSetting "DB_SERVER"
134 -- <*> (parseJwk <$> reqSetting "JWT_SECRET")
135 -- <*> optSetting "SEND_EMAIL" SendEmailViaAws
137 data FireWall = FireWall { unFireWall :: Bool }
140 { _env_settings :: !Settings
141 , _env_logger :: !LoggerSet
142 , _env_pool :: !(Pool Connection)
143 , _env_repo :: !RepoEnv
144 , _env_manager :: !Manager
145 , _env_self_url :: !BaseUrl
146 , _env_scrapers :: !ScrapersEnv
152 instance HasConnectionPool Env where
155 instance HasRepoVar Env where
156 repoVar = repoEnv . repoVar
158 instance HasRepoSaver Env where
159 repoSaver = repoEnv . repoSaver
161 instance HasRepo Env where
164 instance HasSettings Env where
165 settings = env_settings
167 instance Servant.Job.Core.HasEnv Env (Job ScraperStatus ScraperStatus) where
168 _env = env_scrapers . Servant.Job.Core._env
170 instance HasJobEnv Env ScraperStatus ScraperStatus where
171 job_env = env_scrapers
173 data MockEnv = MockEnv
174 { _menv_firewall :: !FireWall
180 -- | TODO add this path in Settings
185 repoSnapshot :: FilePath
186 repoSnapshot = repoDir <> "/repo.json"
188 -- | TODO add hard coded file in Settings
189 -- This assumes we own the lock on repoSnapshot.
190 repoSaverAction :: Serialise a => a -> IO ()
191 repoSaverAction a = do
192 withTempFile "repos" "tmp-repo.json" $ \fp h -> do
193 printDebug "repoSaverAction" fp
194 L.hPut h $ serialise a
196 renameFile fp repoSnapshot
198 mkRepoSaver :: MVar NgramsRepo -> IO (IO ())
199 mkRepoSaver repo_var = mkDebounce settings
201 settings = defaultDebounceSettings
202 { debounceFreq = 1000000 -- 1 second
203 , debounceAction = withMVar repo_var repoSaverAction
204 -- Here this not only `readMVar` but `takeMVar`.
205 -- Namely while repoSaverAction is saving no other change
206 -- can be made to the MVar.
207 -- This might be not efficent and thus reconsidered later.
208 -- However this enables to safely perform a *final* save.
211 -- Add a new MVar just for saving.
214 readRepoEnv :: IO RepoEnv
216 -- Does file exist ? :: Bool
217 _repoDir <- createDirectoryIfMissing True repoDir
219 repoFile <- doesFileExist repoSnapshot
221 -- Is file not empty ? :: Bool
222 repoExists <- if repoFile
223 then (>0) <$> getFileSize repoSnapshot
226 mlock <- tryLockFile repoSnapshot Exclusive
227 lock <- maybe (panic "Repo file already locked") pure mlock
232 -- e_repo <- eitherDecodeStrict <$> deserialise <$> L.readFile repoSnapshot
233 repo <- deserialise <$> L.readFile repoSnapshot
234 -- repo <- either fail pure e_repo
235 let archive = repoSnapshot <> ".v" <> show (repo ^. r_version)
236 copyFile repoSnapshot archive
240 -- TODO save in DB here
241 saver <- mkRepoSaver mvar
242 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
244 devJwkFile :: FilePath
245 devJwkFile = "dev.jwk"
247 newEnv :: PortNumber -> FilePath -> IO Env
248 newEnv port file = do
249 manager <- newTlsManager
250 settings <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
251 when (port /= settings ^. appPort) $
252 panic "TODO: conflicting settings of port"
254 self_url <- parseBaseUrl $ "http://0.0.0.0:" <> show port
255 param <- databaseParameters file
256 pool <- newPool param
258 scrapers_env <- newJobEnv defaultSettings manager
259 logger <- newStderrLoggerSet defaultBufSize
262 { _env_settings = settings
263 , _env_logger = logger
266 , _env_manager = manager
267 , _env_scrapers = scrapers_env
268 , _env_self_url = self_url
271 newPool :: ConnectInfo -> IO (Pool Connection)
272 newPool param = createPool (connect param) close 1 (60*60) 8
275 { _dev_env_pool :: !(Pool Connection)
276 , _dev_env_repo :: !RepoEnv
277 , _dev_env_settings :: !Settings
282 instance HasConnectionPool DevEnv where
283 connPool = dev_env_pool
285 instance HasRepoVar DevEnv where
286 repoVar = repoEnv . repoVar
288 instance HasRepoSaver DevEnv where
289 repoSaver = repoEnv . repoSaver
291 instance HasRepo DevEnv where
292 repoEnv = dev_env_repo
294 instance HasSettings DevEnv where
295 settings = dev_env_settings
297 cleanEnv :: HasRepo env => env -> IO ()
299 r <- takeMVar (env ^. repoEnv . renv_var)
301 unlockFile (env ^. repoEnv . renv_lock)
303 withDevEnv :: FilePath -> (DevEnv -> IO a) -> IO a
304 withDevEnv iniPath k = do
306 k env `finally` cleanEnv env
310 param <- databaseParameters iniPath
311 pool <- newPool param
313 setts <- devSettings devJwkFile
315 { _dev_env_pool = pool
316 , _dev_env_repo = repo
317 , _dev_env_settings = setts
320 -- | Run Cmd Sugar for the Repl (GHCI)
321 runCmdRepl :: Show err => Cmd' DevEnv err a -> IO a
322 runCmdRepl f = withDevEnv "gargantext.ini" $ \env -> runCmdDev env f
324 runCmdReplServantErr :: Cmd' DevEnv ServerError a -> IO a
325 runCmdReplServantErr = runCmdRepl
328 -- In particular this writes the repo file after running
330 -- This function is constrained to the DevEnv rather than
331 -- using HasConnectionPool and HasRepoVar.
332 runCmdDev :: Show err => DevEnv -> Cmd' DevEnv err a -> IO a
334 (either (fail . show) pure =<< runCmd env f)
336 runReaderT saveRepo env
339 runCmdDevNoErr :: DevEnv -> Cmd' DevEnv () a -> IO a
340 runCmdDevNoErr = runCmdDev
343 runCmdDevServantErr :: DevEnv -> Cmd' DevEnv ServerError a -> IO a
344 runCmdDevServantErr = runCmdDev