2 Module : Gargantext.API.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.Settings
29 import System.Directory
30 import System.Log.FastLogger
32 import GHC.Generics (Generic)
33 import Prelude (Bounded(), fail)
34 import System.Environment (lookupEnv)
35 import System.IO (FilePath, hClose)
36 import System.IO.Temp (withTempFile)
37 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
38 import Database.PostgreSQL.Simple (Connection, connect)
39 import Network.HTTP.Client (Manager)
40 import Network.HTTP.Client.TLS (newTlsManager)
43 import Data.Maybe (fromMaybe)
44 import Data.Either (either)
46 --import Data.Text.Encoding (encodeUtf8)
47 import Data.ByteString (ByteString)
48 import qualified Data.ByteString.Lazy as L
51 import Servant.Auth.Server (defaultJWTSettings, JWTSettings, CookieSettings, defaultCookieSettings, readKey, writeKey)
52 import Servant.Client (BaseUrl, parseBaseUrl)
53 import qualified Servant.Job.Core
54 import Servant.Job.Async (newJobEnv, defaultSettings, HasJobEnv(..), Job)
55 import Web.HttpApiData (parseUrlPiece)
57 import Control.Concurrent
58 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
59 import Control.Exception (finally)
60 import Control.Monad.Logger
61 import Control.Monad.Reader
63 import Gargantext.Prelude
64 import Gargantext.Database.Utils (databaseParameters, HasConnection(..), Cmd', runCmd)
65 import Gargantext.API.Ngrams (NgramsRepo, HasRepoVar(..), HasRepoSaver(..), HasRepo(..), RepoEnv(..), r_version, saveRepo, initRepo, renv_var, renv_lock)
66 import Gargantext.API.Orchestrator.Types
70 data SendEmailType = SendEmailViaAws
73 deriving (Show, Read, Enum, Bounded, Generic)
76 data Settings = Settings
77 { _allowedOrigin :: ByteString -- allowed origin for CORS
78 , _allowedHost :: ByteString -- allowed host for CORS
79 , _appPort :: PortNumber
80 , _logLevelLimit :: LogLevel -- log level from the monad-logger package
81 -- , _dbServer :: Text
82 -- ^ this is not used yet
83 , _jwtSettings :: JWTSettings
84 , _cookieSettings :: CookieSettings
85 , _sendLoginEmails :: SendEmailType
86 , _scrapydUrl :: BaseUrl
87 , _fileFolder :: FilePath
92 class HasSettings env where
93 settings :: Getter env Settings
95 devSettings :: FilePath -> IO Settings
96 devSettings jwkFile = do
97 jwkExists <- doesFileExist jwkFile
98 when (not jwkExists) $ writeKey jwkFile
99 jwk <- readKey jwkFile
101 { _allowedOrigin = "http://localhost:8008"
102 , _allowedHost = "localhost:3000"
104 , _logLevelLimit = LevelDebug
105 -- , _dbServer = "localhost"
106 , _sendLoginEmails = LogEmailToConsole
107 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
108 , _fileFolder = "data"
109 , _cookieSettings = defaultCookieSettings -- TODO-SECURITY tune
110 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
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_conn :: !Connection
143 , _env_repo :: !RepoEnv
144 , _env_manager :: !Manager
145 , _env_self_url :: !BaseUrl
146 , _env_scrapers :: !ScrapersEnv
152 instance HasConnection Env where
153 connection = env_conn
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 :: ToJSON a => a -> IO ()
191 repoSaverAction a = do
192 withTempFile "repos" "tmp-repo.json" $ \fp h -> do
193 -- printDebug "repoSaverAction" fp
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 <- eitherDecodeFileStrict 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 conn <- connect param
257 scrapers_env <- newJobEnv defaultSettings manager
258 logger <- newStderrLoggerSet defaultBufSize
261 { _env_settings = settings
262 , _env_logger = logger
265 , _env_manager = manager
266 , _env_scrapers = scrapers_env
267 , _env_self_url = self_url
271 { _dev_env_conn :: !Connection
272 , _dev_env_repo :: !RepoEnv
273 , _dev_env_settings :: !Settings
278 instance HasConnection DevEnv where
279 connection = dev_env_conn
281 instance HasRepoVar DevEnv where
282 repoVar = repoEnv . repoVar
284 instance HasRepoSaver DevEnv where
285 repoSaver = repoEnv . repoSaver
287 instance HasRepo DevEnv where
288 repoEnv = dev_env_repo
290 instance HasSettings DevEnv where
291 settings = dev_env_settings
293 cleanEnv :: HasRepo env => env -> IO ()
295 r <- takeMVar (env ^. repoEnv . renv_var)
297 unlockFile (env ^. repoEnv . renv_lock)
299 withDevEnv :: FilePath -> (DevEnv -> IO a) -> IO a
300 withDevEnv iniPath k = do
302 k env `finally` cleanEnv env
306 param <- databaseParameters iniPath
307 conn <- connect param
309 setts <- devSettings devJwkFile
311 { _dev_env_conn = conn
312 , _dev_env_repo = repo
313 , _dev_env_settings = setts
316 -- | Run Cmd Sugar for the Repl (GHCI)
317 runCmdRepl :: Show err => Cmd' DevEnv err a -> IO a
318 runCmdRepl f = withDevEnv "gargantext.ini" $ \env -> runCmdDev env f
320 runCmdReplServantErr :: Cmd' DevEnv ServerError a -> IO a
321 runCmdReplServantErr = runCmdRepl
324 -- In particular this writes the repo file after running
326 -- This function is constrained to the DevEnv rather than
327 -- using HasConnection and HasRepoVar.
328 runCmdDev :: Show err => DevEnv -> Cmd' DevEnv err a -> IO a
330 (either (fail . show) pure =<< runCmd env f)
332 runReaderT saveRepo env
335 runCmdDevNoErr :: DevEnv -> Cmd' DevEnv () a -> IO a
336 runCmdDevNoErr = runCmdDev
339 runCmdDevServantErr :: DevEnv -> Cmd' DevEnv ServerError a -> IO a
340 runCmdDevServantErr = runCmdDev