Compare commits

..

2 Commits

Author SHA1 Message Date
root 77d542c282 feat(server): make tenant login guard toggleable via config
Frontend CI / Frontend (push) Successful in 3m38s
Backend CI / Backend (push) Successful in 14m43s
Add auth.login_guard.enabled config (default on) so the tenant login
brute-force / rate-limit guard can be disabled per environment.
Surfaces an AUTH_LOGIN_GUARD_ENABLED env override and a startup warning
when disabled. Wired through Diff() so hot reload picks up changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:32:39 +08:00
root 77f3e5ee99 perf(admin-web): relax template wizard assist poll cadence to 3s
Polling analyze/title/outline task results every 1.2s was over-eager
for AI tasks that typically run 15-30s. Bump interval to 3s and lower
max attempts to 40, extending total timeout from 72s to 120s while
roughly halving request volume.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:32:06 +08:00
5 changed files with 39 additions and 7 deletions
@@ -946,8 +946,11 @@ async function runOutlineTask(): Promise<void> {
}
}
const ASSIST_POLL_INTERVAL_MS = 3000
const ASSIST_POLL_MAX_ATTEMPTS = 40
async function pollAnalyzeTask(taskId: string): Promise<TemplateAnalyzeResult> {
for (let attempt = 0; attempt < 60; attempt += 1) {
for (let attempt = 0; attempt < ASSIST_POLL_MAX_ATTEMPTS; attempt += 1) {
const task = await templatesApi.getAnalyzeTaskResult(templateId.value, taskId)
advanceAssistProgress(attempt, task.status)
@@ -959,14 +962,14 @@ async function pollAnalyzeTask(taskId: string): Promise<TemplateAnalyzeResult> {
throw new Error(task.error_message || t('templates.wizard.messages.assistFailed'))
}
await sleep(1200)
await sleep(ASSIST_POLL_INTERVAL_MS)
}
throw new Error(t('templates.wizard.messages.assistTimeout'))
}
async function pollTitleTask(taskId: string): Promise<string[]> {
for (let attempt = 0; attempt < 60; attempt += 1) {
for (let attempt = 0; attempt < ASSIST_POLL_MAX_ATTEMPTS; attempt += 1) {
const task = await templatesApi.getTitleTaskResult(templateId.value, taskId)
advanceAssistProgress(attempt, task.status)
@@ -978,14 +981,14 @@ async function pollTitleTask(taskId: string): Promise<string[]> {
throw new Error(task.error_message || t('templates.wizard.messages.titleFailed'))
}
await sleep(1200)
await sleep(ASSIST_POLL_INTERVAL_MS)
}
throw new Error(t('templates.wizard.messages.titleTimeout'))
}
async function pollOutlineTask(taskId: string): Promise<TemplateOutlineNode[]> {
for (let attempt = 0; attempt < 60; attempt += 1) {
for (let attempt = 0; attempt < ASSIST_POLL_MAX_ATTEMPTS; attempt += 1) {
const task = await templatesApi.getOutlineTaskResult(templateId.value, taskId)
advanceAssistProgress(attempt, task.status)
@@ -997,7 +1000,7 @@ async function pollOutlineTask(taskId: string): Promise<TemplateOutlineNode[]> {
throw new Error(task.error_message || t('templates.wizard.messages.outlineFailed'))
}
await sleep(1200)
await sleep(ASSIST_POLL_INTERVAL_MS)
}
throw new Error(t('templates.wizard.messages.outlineTimeout'))
+4
View File
@@ -163,6 +163,10 @@ jwt:
access_ttl: 15m
refresh_ttl: 720h
auth:
login_guard:
enabled: true
log:
level: debug,info,warn,error
format: json
+6 -1
View File
@@ -113,7 +113,12 @@ func New(configPath string) (*App, error) {
jwtMgr := auth.NewManager(cfg.JWT.Secret, cfg.JWT.AccessTTL, cfg.JWT.RefreshTTL)
sessions := auth.NewSessionStore(rdb)
loginGuard := auth.NewLoginGuard(rdb, auth.DefaultLoginGuardConfig("tenant"))
loginGuardCfg := auth.DefaultLoginGuardConfig("tenant")
loginGuardCfg.Enabled = cfg.Auth.LoginGuard.Enabled
if !loginGuardCfg.Enabled {
logger.Warn("tenant login guard disabled by config; brute-force / rate-limit protection is off")
}
loginGuard := auth.NewLoginGuard(rdb, loginGuardCfg)
llmClient := llm.NewReloadableClient(llm.New(cfg.LLM))
retrievalProvider := retrieval.NewReloadableProvider(retrieval.NewProvider(cfg.Retrieval, logger))
vectorStore := retrieval.NewReloadableVectorStore(retrieval.NewQdrantStore(cfg.Qdrant, logger))
+17
View File
@@ -33,6 +33,7 @@ type Config struct {
ObjectStorage ObjectStorageConfig `mapstructure:"object_storage"`
Cache CacheConfig `mapstructure:"cache"`
JWT JWTConfig `mapstructure:"jwt"`
Auth AuthConfig `mapstructure:"auth"`
Log LogConfig `mapstructure:"log"`
LLM LLMConfig `mapstructure:"llm"`
Compliance ComplianceConfig `mapstructure:"compliance"`
@@ -273,6 +274,14 @@ type JWTConfig struct {
RefreshTTL time.Duration `mapstructure:"refresh_ttl"`
}
type AuthConfig struct {
LoginGuard LoginGuardConfig `mapstructure:"login_guard"`
}
type LoginGuardConfig struct {
Enabled bool `mapstructure:"enabled"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
@@ -476,6 +485,11 @@ func applyConfigDefaults(settings map[string]any) {
if _, ok := complianceSettings["enabled"]; !ok {
complianceSettings["enabled"] = true
}
authSettings := ensureMapSetting(settings, "auth")
loginGuardSettings := ensureMapSetting(authSettings, "login_guard")
if _, ok := loginGuardSettings["enabled"]; !ok {
loginGuardSettings["enabled"] = true
}
}
func ensureMapSetting(settings map[string]any, key string) map[string]any {
@@ -800,6 +814,9 @@ func applyEnvOverrides(cfg *Config) {
if ttl, ok := lookupDurationEnv("JWT_REFRESH_TTL"); ok {
cfg.JWT.RefreshTTL = ttl
}
if enabled, ok := lookupBoolEnv("AUTH_LOGIN_GUARD_ENABLED"); ok {
cfg.Auth.LoginGuard.Enabled = enabled
}
if apiKey, ok := lookupNonEmptyEnv("LLM_API_KEY"); ok {
cfg.LLM.APIKey = apiKey
}
+3
View File
@@ -74,6 +74,9 @@ func Diff(previous, current *Config) []FieldChange {
if previous.JWT != current.JWT {
addChange("jwt", true)
}
if previous.Auth != current.Auth {
addChange("auth", true)
}
if previous.LLM != current.LLM {
addChange("llm", true)
}