2026-09-25 17:41:51 +08:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
|
|
|
|
|
module Main (main) where
|
|
|
|
|
|
2026-10-01 20:48:38 +08:00
|
|
|
import ChuSQL.Web.API (AppEnv (..), Live (..), newAppEnvAt, setLive, webApp)
|
2026-10-03 02:17:46 +08:00
|
|
|
import ChuSQL.Interface.Auth (SessionPolicy (..), setPayloadPolicy)
|
2026-10-02 14:53:02 +08:00
|
|
|
import ChuSQL.Interface.Config (
|
|
|
|
|
ServerConfig (..),
|
2026-09-25 17:41:51 +08:00
|
|
|
WebConfig (..),
|
|
|
|
|
defaultUser,
|
2026-10-02 14:53:02 +08:00
|
|
|
loadServerConfigAt,
|
2026-10-01 20:48:38 +08:00
|
|
|
loadWebConfigAt,
|
2026-10-03 02:17:46 +08:00
|
|
|
legacyPasswordKey,
|
2026-09-25 17:41:51 +08:00
|
|
|
resolveStaticDir,
|
2026-10-02 14:53:02 +08:00
|
|
|
rootUserName,
|
2026-09-25 17:41:51 +08:00
|
|
|
staticDirCandidates,
|
|
|
|
|
)
|
2026-10-02 14:53:02 +08:00
|
|
|
import ChuSQL.Interface.Link (clientPing, closeClient, connectClient)
|
|
|
|
|
import ChuSQL.Interface.RateLimit (newRateLimiter)
|
|
|
|
|
import ChuSQL.Interface.Session (authenticateSession, newSession)
|
2026-10-05 00:15:01 +08:00
|
|
|
import ChuSQL.Interface.Settings (effectiveSettings)
|
2026-10-02 14:53:02 +08:00
|
|
|
import ChuSQL.Interface.TOML (resolveConfigPath)
|
|
|
|
|
import ChuSQL.Web.Demo (SeedReport (..), seedDemo)
|
|
|
|
|
import Control.Exception (IOException, try)
|
2026-09-25 17:41:51 +08:00
|
|
|
import Data.Char (toLower)
|
|
|
|
|
import Data.String (fromString)
|
|
|
|
|
import Data.Text (Text)
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import qualified Data.Text.IO as TIO
|
|
|
|
|
import Data.Time.Clock (getCurrentTime)
|
|
|
|
|
import Network.Wai (Application)
|
|
|
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
|
|
|
import System.Environment (getArgs)
|
|
|
|
|
import System.Exit (exitFailure)
|
|
|
|
|
import System.IO (BufferMode (LineBuffering), hSetBuffering, stdout)
|
|
|
|
|
import Text.Read (readMaybe)
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- Web 管理端入口:读配置,把请求转给拥有存储的 server,最后起 Warp。
|
2026-09-25 17:41:51 +08:00
|
|
|
|
|
|
|
|
data Options = Options
|
2026-10-01 20:48:38 +08:00
|
|
|
{ optConfig :: Maybe FilePath
|
|
|
|
|
, optHost :: Maybe String
|
2026-09-25 17:41:51 +08:00
|
|
|
, optPort :: Maybe Int
|
|
|
|
|
, optUser :: Maybe Text
|
|
|
|
|
, optCookieSecure :: Maybe Bool
|
|
|
|
|
, optBodyLimit :: Maybe Int
|
|
|
|
|
, optSessionIdle :: Maybe Int
|
|
|
|
|
, optSessionMax :: Maybe Int
|
|
|
|
|
, optLoginMaxAttempts :: Maybe Int
|
|
|
|
|
, optLoginWindow :: Maybe Int
|
|
|
|
|
, optPageSize :: Maybe Int
|
|
|
|
|
, optMaxPageSize :: Maybe Int
|
|
|
|
|
, optMaxRows :: Maybe Int
|
|
|
|
|
, optMaxSqlLength :: Maybe Int
|
|
|
|
|
, optSeedDemo :: Maybe Bool
|
|
|
|
|
, optHelp :: Bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- | 一个开关都没给
|
|
|
|
|
emptyOptions :: Options
|
|
|
|
|
emptyOptions =
|
|
|
|
|
Options
|
2026-10-01 20:48:38 +08:00
|
|
|
{ optConfig = Nothing
|
|
|
|
|
, optHost = Nothing
|
2026-09-25 17:41:51 +08:00
|
|
|
, optPort = Nothing
|
|
|
|
|
, optUser = Nothing
|
|
|
|
|
, optCookieSecure = Nothing
|
|
|
|
|
, optBodyLimit = Nothing
|
|
|
|
|
, optSessionIdle = Nothing
|
|
|
|
|
, optSessionMax = Nothing
|
|
|
|
|
, optLoginMaxAttempts = Nothing
|
|
|
|
|
, optLoginWindow = Nothing
|
|
|
|
|
, optPageSize = Nothing
|
|
|
|
|
, optMaxPageSize = Nothing
|
|
|
|
|
, optMaxRows = Nothing
|
|
|
|
|
, optMaxSqlLength = Nothing
|
|
|
|
|
, optSeedDemo = Nothing
|
|
|
|
|
, optHelp = False
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- | 解析参数
|
|
|
|
|
parseArgs :: [String] -> Either String Options
|
|
|
|
|
parseArgs = go emptyOptions
|
|
|
|
|
where
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 逐个参数累加进 Options
|
2026-09-25 17:41:51 +08:00
|
|
|
go opts [] = Right opts
|
|
|
|
|
go opts ("--help" : rest) = go opts{optHelp = True} rest
|
|
|
|
|
go opts ("-h" : rest) = go opts{optHelp = True} rest
|
2026-10-01 20:48:38 +08:00
|
|
|
go opts ("--config" : v : rest) = go opts{optConfig = Just v} rest
|
2026-10-05 00:15:01 +08:00
|
|
|
go opts ("--listen-host" : v : rest) = go opts{optHost = Just v} rest
|
2026-09-25 17:41:51 +08:00
|
|
|
go opts ("--user" : v : rest) = go opts{optUser = Just (T.pack v)} rest
|
|
|
|
|
go opts ("--port" : v : rest) = withInt "--port" v (\n -> go opts{optPort = Just n} rest)
|
|
|
|
|
go opts ("--body-limit" : v : rest) = withInt "--body-limit" v (\n -> go opts{optBodyLimit = Just n} rest)
|
|
|
|
|
go opts ("--session-idle" : v : rest) = withInt "--session-idle" v (\n -> go opts{optSessionIdle = Just n} rest)
|
|
|
|
|
go opts ("--session-max" : v : rest) = withInt "--session-max" v (\n -> go opts{optSessionMax = Just n} rest)
|
|
|
|
|
go opts ("--login-max-attempts" : v : rest) = withInt "--login-max-attempts" v (\n -> go opts{optLoginMaxAttempts = Just n} rest)
|
|
|
|
|
go opts ("--login-window" : v : rest) = withInt "--login-window" v (\n -> go opts{optLoginWindow = Just n} rest)
|
|
|
|
|
go opts ("--page-size" : v : rest) = withInt "--page-size" v (\n -> go opts{optPageSize = Just n} rest)
|
|
|
|
|
go opts ("--max-page-size" : v : rest) = withInt "--max-page-size" v (\n -> go opts{optMaxPageSize = Just n} rest)
|
|
|
|
|
go opts ("--max-rows" : v : rest) = withInt "--max-rows" v (\n -> go opts{optMaxRows = Just n} rest)
|
|
|
|
|
go opts ("--max-sql-length" : v : rest) = withInt "--max-sql-length" v (\n -> go opts{optMaxSqlLength = Just n} rest)
|
|
|
|
|
go opts ("--seed" : v : rest) = go opts{optSeedDemo = Just (isTrue v)} rest
|
|
|
|
|
go opts ("--cookie-secure" : v : rest) = go opts{optCookieSecure = Just (isTrue v)} rest
|
|
|
|
|
go _ (a : _) = Left ("unrecognized argument or missing value: " ++ a)
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 解析一个整数参数
|
2026-09-25 17:41:51 +08:00
|
|
|
withInt :: String -> String -> (Int -> Either String Options) -> Either String Options
|
|
|
|
|
withInt name v k = maybe (Left (name ++ " needs an integer, got: " ++ v)) k (readMaybe v)
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 把 1/true/yes/on 认成真
|
2026-09-25 17:41:51 +08:00
|
|
|
isTrue :: String -> Bool
|
|
|
|
|
isTrue s = map toLower s `elem` ["1", "true", "yes", "on"]
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 打印命令行用法
|
2026-09-25 17:41:51 +08:00
|
|
|
usage :: IO ()
|
|
|
|
|
usage = do
|
|
|
|
|
putStrLn "usage: chusql-web [options]"
|
|
|
|
|
putStrLn ""
|
2026-10-05 00:15:01 +08:00
|
|
|
putStrLn "config: settings.toml sits at a fixed place and its sections hold every knob below;"
|
|
|
|
|
putStrLn " Windows: %APPDATA%\\ChuSQL\\settings.toml"
|
|
|
|
|
putStrLn " other: $XDG_CONFIG_HOME/ChuSQL/settings.toml (or ~/.config/ChuSQL/settings.toml)"
|
|
|
|
|
putStrLn " [web] listen_host/port and the limits;"
|
|
|
|
|
putStrLn " [storage] data_dir/log_files; [page] size; [btree] order; [buffer] pool_size; [log] level"
|
2026-09-25 17:41:51 +08:00
|
|
|
putStrLn ""
|
2026-10-05 00:15:01 +08:00
|
|
|
putStrLn " --config FILE read that config file instead ([web] listen_host ...)"
|
|
|
|
|
putStrLn " --listen-host H listen address ([web] listen_host, default 127.0.0.1)"
|
2026-10-02 14:53:02 +08:00
|
|
|
putStrLn " --port N listen port ([web] port, default 7778)"
|
2026-10-05 00:15:01 +08:00
|
|
|
putStrLn (" --user NAME administrator name (default " ++ T.unpack defaultUser ++ ")")
|
2026-10-01 20:48:38 +08:00
|
|
|
putStrLn " --cookie-secure BOOL add Secure to the cookie ([web] cookie_secure, default false)"
|
|
|
|
|
putStrLn " --body-limit N max request body bytes ([web] body_limit, default 65536)"
|
|
|
|
|
putStrLn " --session-idle N session idle timeout, sec ([web] session_idle, default 28800)"
|
|
|
|
|
putStrLn " --session-max N session max age, sec ([web] session_max, default 86400)"
|
|
|
|
|
putStrLn " --login-max-attempts N failed logins before 429 ([web] login_max_attempts, default 5)"
|
|
|
|
|
putStrLn " --login-window N lockout window, sec ([web] login_window, default 300)"
|
|
|
|
|
putStrLn " --page-size N rows per page by default ([web] rows_per_page, default 25)"
|
|
|
|
|
putStrLn " --max-page-size N rows per page ceiling ([web] max_page_size, default 500)"
|
|
|
|
|
putStrLn " --max-rows N rows returned per query ([web] max_rows, default 1000)"
|
|
|
|
|
putStrLn " --max-sql-length N max SQL characters ([web] max_sql_length, default 20000)"
|
|
|
|
|
putStrLn " --seed BOOL load demo data when the db is empty ([web] seed, default false)"
|
|
|
|
|
putStrLn ""
|
2026-10-05 00:15:01 +08:00
|
|
|
putStrLn "administrator password: lives in the __system_users system table, never in settings.toml"
|
2026-10-03 02:17:46 +08:00
|
|
|
putStrLn " the installer always sets one; an account whose hash is still empty signs in with an empty password"
|
2026-10-05 00:15:01 +08:00
|
|
|
putStrLn " the administrator name comes from --user; settings.toml does not carry it"
|
2026-10-01 20:48:38 +08:00
|
|
|
putStrLn "ordinary accounts: CREATE USER / ALTER USER / DROP USER from the SQL console (administrator only)"
|
2026-10-02 14:53:02 +08:00
|
|
|
putStrLn "storage: the server owns the data directory and loads the chusql_core_storage library;"
|
|
|
|
|
putStrLn " this process is a plain TCP client: every statement, lookup and account request goes to it"
|
2026-10-05 00:15:01 +08:00
|
|
|
putStrLn " [server] listen_host / port says where it listens; [storage] data_dir, [page] size, [btree] order,"
|
2026-10-02 14:53:02 +08:00
|
|
|
putStrLn " [buffer] pool_size and [log] level are the server's own"
|
2026-09-25 17:41:51 +08:00
|
|
|
|
|
|
|
|
-- | 命令行覆盖配置
|
|
|
|
|
applyOptions :: WebConfig -> Options -> WebConfig
|
|
|
|
|
applyOptions cfg opts =
|
|
|
|
|
cfg
|
|
|
|
|
{ wcHost = orElse (optHost opts) (wcHost cfg)
|
|
|
|
|
, wcPort = orElse (optPort opts) (wcPort cfg)
|
|
|
|
|
, wcUser = orElse (optUser opts) (wcUser cfg)
|
|
|
|
|
, wcCookieSecure = orElse (optCookieSecure opts) (wcCookieSecure cfg)
|
|
|
|
|
, wcBodyLimit = orElse (optBodyLimit opts) (wcBodyLimit cfg)
|
|
|
|
|
, wcSessionIdle = orElse (optSessionIdle opts) (wcSessionIdle cfg)
|
|
|
|
|
, wcSessionMax = orElse (optSessionMax opts) (wcSessionMax cfg)
|
|
|
|
|
, wcLoginMaxAttempts = orElse (optLoginMaxAttempts opts) (wcLoginMaxAttempts cfg)
|
|
|
|
|
, wcLoginWindow = orElse (optLoginWindow opts) (wcLoginWindow cfg)
|
|
|
|
|
, wcPageSize = orElse (optPageSize opts) (wcPageSize cfg)
|
|
|
|
|
, wcMaxPageSize = orElse (optMaxPageSize opts) (wcMaxPageSize cfg)
|
|
|
|
|
, wcMaxRows = orElse (optMaxRows opts) (wcMaxRows cfg)
|
|
|
|
|
, wcMaxSqlLength = orElse (optMaxSqlLength opts) (wcMaxSqlLength cfg)
|
|
|
|
|
, wcSeedDemo = orElse (optSeedDemo opts) (wcSeedDemo cfg)
|
|
|
|
|
}
|
|
|
|
|
where
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 有就用新的,没有留旧值
|
2026-09-25 17:41:51 +08:00
|
|
|
orElse :: Maybe a -> a -> a
|
|
|
|
|
orElse (Just x) _ = x
|
|
|
|
|
orElse Nothing y = y
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 程序入口
|
2026-09-25 17:41:51 +08:00
|
|
|
main :: IO ()
|
|
|
|
|
main = do
|
|
|
|
|
hSetBuffering stdout LineBuffering
|
|
|
|
|
args <- getArgs
|
|
|
|
|
case parseArgs args of
|
|
|
|
|
Left err -> do
|
|
|
|
|
putStrLn ("bad arguments: " ++ err)
|
|
|
|
|
usage
|
|
|
|
|
exitFailure
|
|
|
|
|
Right opts
|
|
|
|
|
| optHelp opts -> usage
|
|
|
|
|
| otherwise -> run opts
|
|
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | 读配置装配环境,检查 server,起 Warp
|
2026-09-25 17:41:51 +08:00
|
|
|
run :: Options -> IO ()
|
|
|
|
|
run opts = do
|
2026-10-01 20:48:38 +08:00
|
|
|
configPath <- resolveConfigPath (optConfig opts)
|
|
|
|
|
base <- loadWebConfigAt configPath
|
2026-09-25 17:41:51 +08:00
|
|
|
let cfg = applyOptions base opts
|
|
|
|
|
foundStatic <- resolveStaticDir (wcStaticDir cfg)
|
|
|
|
|
case foundStatic of
|
|
|
|
|
Nothing -> do
|
|
|
|
|
putStrLn ("static directory not found; looked in: " ++ unwords (staticDirCandidates (wcStaticDir cfg)))
|
|
|
|
|
exitFailure
|
|
|
|
|
Just staticDir -> do
|
2026-10-01 20:48:38 +08:00
|
|
|
let settingsFile = configPath
|
2026-10-02 14:53:02 +08:00
|
|
|
policy = SessionPolicy (fromIntegral (wcSessionIdle cfg)) (fromIntegral (wcSessionMax cfg))
|
2026-10-03 02:17:46 +08:00
|
|
|
legacy <- legacyPasswordKey settingsFile
|
2026-10-02 14:53:02 +08:00
|
|
|
config <- loadServerConfigAt settingsFile
|
|
|
|
|
limiter <-
|
|
|
|
|
newRateLimiter
|
|
|
|
|
getCurrentTime
|
|
|
|
|
(wcLoginMaxAttempts cfg)
|
|
|
|
|
(fromIntegral (wcLoginWindow cfg))
|
|
|
|
|
env0 <- newAppEnvAt settingsFile (T.pack (scHost config)) (scPort config) limiter staticDir
|
|
|
|
|
let env =
|
|
|
|
|
env0
|
|
|
|
|
{ aeCookieSecure = wcCookieSecure cfg
|
|
|
|
|
, aeEffective = effectiveSettings cfg
|
|
|
|
|
, aeLog = TIO.putStrLn
|
|
|
|
|
}
|
|
|
|
|
setPayloadPolicy (aeSessions env) policy
|
|
|
|
|
setLive env (liveFromConfig cfg)
|
|
|
|
|
app <- webApp env
|
|
|
|
|
printBanner cfg configPath staticDir
|
2026-10-05 00:15:01 +08:00
|
|
|
reportCredential cfg legacy
|
2026-10-02 14:53:02 +08:00
|
|
|
checkServer (aeServerHost env) (aeServerPort env)
|
2026-10-05 00:15:01 +08:00
|
|
|
seedIfWanted cfg (aeServerHost env) (aeServerPort env)
|
2026-10-02 14:53:02 +08:00
|
|
|
let settings =
|
|
|
|
|
Warp.setPort (wcPort cfg) (Warp.setHost (fromString (wcHost cfg)) Warp.defaultSettings)
|
|
|
|
|
serve settings app
|
2026-09-25 17:41:51 +08:00
|
|
|
|
|
|
|
|
-- | 本次启动的配置摊成热改参数
|
|
|
|
|
liveFromConfig :: WebConfig -> Live
|
|
|
|
|
liveFromConfig cfg =
|
|
|
|
|
Live
|
|
|
|
|
{ lvBodyLimit = wcBodyLimit cfg
|
|
|
|
|
, lvSessionIdle = wcSessionIdle cfg
|
|
|
|
|
, lvSessionMax = wcSessionMax cfg
|
|
|
|
|
, lvLoginMaxAttempts = wcLoginMaxAttempts cfg
|
|
|
|
|
, lvLoginWindow = wcLoginWindow cfg
|
|
|
|
|
, lvPageSize = wcPageSize cfg
|
|
|
|
|
, lvMaxPageSize = wcMaxPageSize cfg
|
|
|
|
|
, lvMaxRows = wcMaxRows cfg
|
|
|
|
|
, lvMaxSqlLength = wcMaxSqlLength cfg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- | 起 HTTP 服务,绑不上端口给人话
|
|
|
|
|
serve :: Warp.Settings -> Application -> IO ()
|
|
|
|
|
serve settings app = do
|
|
|
|
|
result <- try (Warp.runSettings settings app) :: IO (Either IOException ())
|
|
|
|
|
case result of
|
|
|
|
|
Right () -> pure ()
|
|
|
|
|
Left e -> do
|
|
|
|
|
putStrLn ("failed to serve HTTP: " ++ show e)
|
|
|
|
|
putStrLn " the port is probably taken or reserved by the system; try another one: --port 7778"
|
|
|
|
|
putStrLn " on Windows: netsh interface ipv4 show excludedportrange protocol=tcp"
|
|
|
|
|
exitFailure
|
|
|
|
|
|
2026-10-03 02:17:46 +08:00
|
|
|
-- | 报告管理员账号状态:配置里还留着明文口令键就提醒
|
2026-10-05 00:15:01 +08:00
|
|
|
reportCredential :: WebConfig -> Bool -> IO ()
|
|
|
|
|
reportCredential cfg legacy = do
|
|
|
|
|
putStrLn ("administrator: " ++ T.unpack (rootUserName cfg))
|
2026-10-03 02:17:46 +08:00
|
|
|
if legacy
|
2026-10-02 14:53:02 +08:00
|
|
|
then do
|
2026-10-03 02:17:46 +08:00
|
|
|
putStrLn " !! the [web] section still carries a plaintext password key; it is ignored now"
|
|
|
|
|
putStrLn " !! remove it: the administrator password lives in the __system_users system table"
|
|
|
|
|
else pure ()
|
2026-09-25 17:41:51 +08:00
|
|
|
|
2026-10-03 02:17:46 +08:00
|
|
|
-- | 配了 seed 就补演示数据(管理员已设口令时跳过)
|
2026-10-05 00:15:01 +08:00
|
|
|
seedIfWanted :: WebConfig -> Text -> Int -> IO ()
|
|
|
|
|
seedIfWanted cfg host port
|
2026-09-25 17:41:51 +08:00
|
|
|
| not (wcSeedDemo cfg) = pure ()
|
|
|
|
|
| otherwise = do
|
2026-10-02 14:53:02 +08:00
|
|
|
opened <- connectClient host port
|
|
|
|
|
case opened of
|
|
|
|
|
Left message -> TIO.putStrLn ("demo data skipped: " <> message)
|
|
|
|
|
Right client -> do
|
|
|
|
|
session <- newSession client
|
2026-10-05 00:15:01 +08:00
|
|
|
signed <- authenticateSession session (rootUserName cfg) ""
|
2026-10-02 14:53:02 +08:00
|
|
|
case signed of
|
2026-10-03 02:17:46 +08:00
|
|
|
Left message -> TIO.putStrLn ("demo data skipped: " <> message <> " (set the administrator password after seeding)")
|
2026-10-02 14:53:02 +08:00
|
|
|
Right () -> do
|
|
|
|
|
result <- seedDemo session
|
|
|
|
|
case result of
|
|
|
|
|
Left message -> TIO.putStrLn ("demo data skipped: " <> message)
|
|
|
|
|
Right report ->
|
|
|
|
|
TIO.putStrLn
|
|
|
|
|
( "demo data: "
|
|
|
|
|
<> (if null (srCreated report) then "already present" else "created " <> T.pack (unwords (srCreated report)))
|
|
|
|
|
<> (if null (srSkipped report) then "" else ", kept " <> T.pack (unwords (srSkipped report)))
|
|
|
|
|
<> (if null (srIndexes report) then "" else ", index on " <> T.pack (unwords (srIndexes report)))
|
|
|
|
|
)
|
|
|
|
|
closeClient client
|
2026-09-25 17:41:51 +08:00
|
|
|
|
2026-10-02 14:53:02 +08:00
|
|
|
-- | ping 一次 server,报告它通不通
|
|
|
|
|
checkServer :: Text -> Int -> IO ()
|
|
|
|
|
checkServer host port = do
|
|
|
|
|
opened <- connectClient host port
|
|
|
|
|
case opened of
|
|
|
|
|
Left message -> putStrLn ("storage: DOWN (" ++ T.unpack message ++ ")")
|
|
|
|
|
Right client -> do
|
|
|
|
|
probe <- clientPing client
|
|
|
|
|
closeClient client
|
|
|
|
|
case probe of
|
|
|
|
|
Right () -> putStrLn "storage: up (the server answered ping)"
|
|
|
|
|
Left message -> putStrLn ("storage: DOWN (" ++ T.unpack message ++ ")")
|
2026-09-25 17:41:51 +08:00
|
|
|
|
|
|
|
|
-- | 启动横幅
|
2026-10-02 14:53:02 +08:00
|
|
|
printBanner :: WebConfig -> FilePath -> FilePath -> IO ()
|
|
|
|
|
printBanner cfg configPath staticDir = do
|
2026-09-25 17:41:51 +08:00
|
|
|
putStrLn "==================== ChuSQL Web ===================="
|
|
|
|
|
putStrLn ("url: http://" ++ wcHost cfg ++ ":" ++ show (wcPort cfg) ++ "/")
|
2026-10-01 20:48:38 +08:00
|
|
|
putStrLn ("config file: " ++ configPath)
|
2026-09-25 17:41:51 +08:00
|
|
|
putStrLn ("static dir: " ++ staticDir)
|
|
|
|
|
putStrLn
|
|
|
|
|
( "limits: body "
|
|
|
|
|
++ show (wcBodyLimit cfg)
|
|
|
|
|
++ "B, page "
|
|
|
|
|
++ show (wcPageSize cfg)
|
|
|
|
|
++ "/"
|
|
|
|
|
++ show (wcMaxPageSize cfg)
|
|
|
|
|
++ " rows, query rows "
|
|
|
|
|
++ show (wcMaxRows cfg)
|
|
|
|
|
++ ", sql "
|
|
|
|
|
++ show (wcMaxSqlLength cfg)
|
|
|
|
|
++ " chars"
|
|
|
|
|
)
|
|
|
|
|
putStrLn
|
|
|
|
|
( "auth: session idle "
|
|
|
|
|
++ show (wcSessionIdle cfg)
|
|
|
|
|
++ "s / max "
|
|
|
|
|
++ show (wcSessionMax cfg)
|
|
|
|
|
++ "s, lockout after "
|
|
|
|
|
++ show (wcLoginMaxAttempts cfg)
|
|
|
|
|
++ " failures in "
|
|
|
|
|
++ show (wcLoginWindow cfg)
|
|
|
|
|
++ "s"
|
|
|
|
|
)
|
2026-10-02 14:53:02 +08:00
|
|
|
putStrLn "storage: through the server that owns the data directory"
|