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 NoImplicitPrelude #-}
20 {-# LANGUAGE OverloadedStrings #-}
21 {-# LANGUAGE RankNTypes #-}
22 {-# LANGUAGE ScopedTypeVariables #-}
23 {-# LANGUAGE TemplateHaskell #-}
25 module Gargantext.API.Settings
28 import System.Directory
29 import System.Log.FastLogger
31 import GHC.Generics (Generic)
32 import Prelude (Bounded(), fail)
33 import System.Environment (lookupEnv)
34 import System.IO (FilePath, hClose)
35 import System.IO.Temp (withTempFile)
36 import System.FileLock (tryLockFile, unlockFile, SharedExclusive(Exclusive))
37 import Database.PostgreSQL.Simple (Connection, connect)
38 import Network.HTTP.Client (Manager)
39 import Network.HTTP.Client.TLS (newTlsManager)
42 import Data.Maybe (fromMaybe)
43 import Data.Either (either)
45 --import Data.Text.Encoding (encodeUtf8)
46 import Data.ByteString (ByteString)
47 import qualified Data.ByteString.Lazy as L
50 import Servant.Auth.Server (defaultJWTSettings, JWTSettings, CookieSettings, defaultCookieSettings, readKey, writeKey)
51 import Servant.Client (BaseUrl, parseBaseUrl)
52 --import Servant.Job.Async (newJobEnv, defaultSettings)
53 import Web.HttpApiData (parseUrlPiece)
55 import Control.Concurrent
56 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
57 import Control.Exception (finally)
58 import Control.Monad.Logger
59 import Control.Monad.Reader
61 import Gargantext.Prelude
62 import Gargantext.Database.Utils (databaseParameters, HasConnection(..), Cmd', runCmd)
63 import Gargantext.API.Ngrams (NgramsRepo, HasRepoVar(..), HasRepoSaver(..), HasRepo(..), RepoEnv(..), r_version, saveRepo, initRepo, renv_var, renv_lock)
64 --import Gargantext.API.Orchestrator.Types
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
94 devSettings :: FilePath -> IO Settings
95 devSettings jwkFile = do
96 jwkExists <- doesFileExist jwkFile
97 when (not jwkExists) $ writeKey jwkFile
98 jwk <- readKey jwkFile
100 { _allowedOrigin = "http://localhost:8008"
101 , _allowedHost = "localhost:3000"
103 , _logLevelLimit = LevelDebug
104 -- , _dbServer = "localhost"
105 , _sendLoginEmails = LogEmailToConsole
106 , _scrapydUrl = fromMaybe (panic "Invalid scrapy URL") $ parseBaseUrl "http://localhost:6800"
107 , _fileFolder = "data"
108 , _cookieSettings = defaultCookieSettings -- TODO-SECURITY tune
109 , _jwtSettings = defaultJWTSettings jwk -- TODO-SECURITY tune
114 reqSetting :: FromHttpApiData a => Text -> IO a
116 e <- fromMaybe (panic $ "Missing " <> name) <$> lookupEnv (unpack name)
117 pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
119 optSetting :: FromHttpApiData a => Text -> a -> IO a
120 optSetting name d = do
121 me <- lookupEnv (unpack name)
124 Just e -> pure $ either (panic $ "Unable to parse " <> name) identity $ parseUrlPiece $ pack e
126 --settingsFromEnvironment :: IO Settings
127 --settingsFromEnvironment =
128 -- Settings <$> (encodeUtf8 <$> reqSetting "ALLOWED_ORIGIN")
129 -- <*> (encodeUtf8 <$> reqSetting "ALLOWED_HOST")
130 -- <*> optSetting "PORT" 3000
131 -- <*> (parseLogLevel <$> optSetting "LOG_LEVEL" "warn")
132 -- <*> reqSetting "DB_SERVER"
133 -- <*> (parseJwk <$> reqSetting "JWT_SECRET")
134 -- <*> optSetting "SEND_EMAIL" SendEmailViaAws
136 data FireWall = FireWall { unFireWall :: Bool }
139 { _env_settings :: !Settings
140 , _env_logger :: !LoggerSet
141 , _env_conn :: !Connection
142 , _env_repo :: !RepoEnv
143 , _env_manager :: !Manager
144 , _env_self_url :: !BaseUrl
145 --, _env_scrapers :: !ScrapersEnv
151 instance HasConnection Env where
152 connection = env_conn
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 data MockEnv = MockEnv
167 { _menv_firewall :: !FireWall
173 -- | TODO add this path in Settings
174 repoSnapshot :: FilePath
175 repoSnapshot = "repo.json"
177 -- | TODO add hard coded file in Settings
178 -- This assumes we own the lock on repoSnapshot.
179 repoSaverAction :: ToJSON a => a -> IO ()
180 repoSaverAction a = do
181 withTempFile "." "tmp-repo.json" $ \fp h -> do
182 -- printDebug "repoSaverAction" fp
185 renameFile fp repoSnapshot
187 mkRepoSaver :: MVar NgramsRepo -> IO (IO ())
188 mkRepoSaver repo_var = mkDebounce settings
190 settings = defaultDebounceSettings
191 { debounceFreq = 1000000 -- 1 second
192 , debounceAction = withMVar repo_var repoSaverAction
193 -- Here this not only `readMVar` but `takeMVar`.
194 -- Namely while repoSaverAction is saving no other change
195 -- can be made to the MVar.
196 -- This might be not efficent and thus reconsidered later.
197 -- However this enables to safely perform a *final* save.
200 -- Add a new MVar just for saving.
203 readRepoEnv :: IO RepoEnv
205 -- Does file exist ? :: Bool
206 repoFile <- doesFileExist repoSnapshot
208 -- Is file not empty ? :: Bool
209 repoExists <- if repoFile
210 then (>0) <$> getFileSize repoSnapshot
213 mlock <- tryLockFile repoSnapshot Exclusive
214 lock <- maybe (panic "Repo file already locked") pure mlock
219 e_repo <- eitherDecodeFileStrict repoSnapshot
220 repo <- either fail pure e_repo
221 let archive = repoSnapshot <> ".v" <> show (repo ^. r_version)
222 copyFile repoSnapshot archive
227 saver <- mkRepoSaver mvar
228 pure $ RepoEnv { _renv_var = mvar, _renv_saver = saver, _renv_lock = lock }
230 devJwkFile :: FilePath
231 devJwkFile = "dev.jwk"
233 newEnv :: PortNumber -> FilePath -> IO Env
234 newEnv port file = do
235 manager <- newTlsManager
236 settings <- devSettings devJwkFile <&> appPort .~ port -- TODO read from 'file'
237 when (port /= settings ^. appPort) $
238 panic "TODO: conflicting settings of port"
240 self_url <- parseBaseUrl $ "http://0.0.0.0:" <> show port
241 param <- databaseParameters file
242 conn <- connect param
244 --scrapers_env <- newJobEnv defaultSettings manager
245 logger <- newStderrLoggerSet defaultBufSize
248 { _env_settings = settings
249 , _env_logger = logger
252 , _env_manager = manager
253 --, _env_scrapers = scrapers_env
254 , _env_self_url = self_url
258 { _dev_env_conn :: !Connection
259 , _dev_env_repo :: !RepoEnv
260 , _dev_env_settings :: !Settings
265 instance HasConnection DevEnv where
266 connection = dev_env_conn
268 instance HasRepoVar DevEnv where
269 repoVar = repoEnv . repoVar
271 instance HasRepoSaver DevEnv where
272 repoSaver = repoEnv . repoSaver
274 instance HasRepo DevEnv where
275 repoEnv = dev_env_repo
277 instance HasSettings DevEnv where
278 settings = dev_env_settings
280 cleanEnv :: HasRepo env => env -> IO ()
282 r <- takeMVar (env ^. repoEnv . renv_var)
284 unlockFile (env ^. repoEnv . renv_lock)
286 withDevEnv :: FilePath -> (DevEnv -> IO a) -> IO a
287 withDevEnv iniPath k = do
289 k env `finally` cleanEnv env
293 param <- databaseParameters iniPath
294 conn <- connect param
296 setts <- devSettings devJwkFile
298 { _dev_env_conn = conn
299 , _dev_env_repo = repo
300 , _dev_env_settings = setts
303 -- | Run Cmd Sugar for the Repl (GHCI)
304 runCmdRepl :: Show err => Cmd' DevEnv err a -> IO a
305 runCmdRepl f = withDevEnv "gargantext.ini" $ \env -> runCmdDev env f
307 runCmdReplServantErr :: Cmd' DevEnv ServerError a -> IO a
308 runCmdReplServantErr = runCmdRepl
311 -- In particular this writes the repo file after running
313 -- This function is constrained to the DevEnv rather than
314 -- using HasConnection and HasRepoVar.
315 runCmdDev :: Show err => DevEnv -> Cmd' DevEnv err a -> IO a
317 (either (fail . show) pure =<< runCmd env f)
319 runReaderT saveRepo env
322 runCmdDevNoErr :: DevEnv -> Cmd' DevEnv () a -> IO a
323 runCmdDevNoErr = runCmdDev
326 runCmdDevServantErr :: DevEnv -> Cmd' DevEnv ServerError a -> IO a
327 runCmdDevServantErr = runCmdDev