package web import ( "encoding/json" "net/http" "strings" ) func withSecurity(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "same-origin") next.ServeHTTP(w, r) }) } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) } func writeSSE(w http.ResponseWriter, event string, payload any) { data, _ := json.Marshal(payload) _, _ = w.Write([]byte("event: " + event + "\n")) _, _ = w.Write([]byte("data: " + string(data) + "\n\n")) } func writeError(w http.ResponseWriter, status int, code string, err error) { message := "" if err != nil { message = err.Error() } writeJSON(w, status, map[string]any{"ok": false, "error": code, "message": localizedErrorMessage(code, message)}) } func localizedErrorMessage(code, message string) string { raw := strings.TrimSpace(message) lower := strings.ToLower(raw) exact := map[string]string{ "current password is invalid": "当前密码不正确", "new password is required": "新密码不能为空", "new password must be at least 8 characters": "新密码至少需要 8 位", "new password cannot be admin": "新密码不能为 admin", "new password must be different from current password": "新密码不能与当前密码相同", "invalid password or captcha": "密码或验证码不正确", "login required": "需要登录后继续操作", "csrf token required": "页面安全令牌已失效,请刷新后重试", "csrf token invalid": "页面安全令牌无效,请刷新后重试", "code is required": "缺少反馈编号", "revisionid is required": "请选择要恢复的历史版本", "post required": "该操作需要使用 POST 请求", "get required": "该操作需要使用 GET 请求", "file is required": "请选择要上传的文件", "invalid filename": "文件名不合法", "path escape rejected": "文件路径不合法", "check job not found": "未找到心跳检测任务", "streaming is not supported": "当前运行环境不支持实时事件流", "source api_url is empty": "接口地址不能为空", "database is not available": "数据库当前不可用", "provider must be sqlite or mysql": "数据库类型必须是 SQLite 或 MySQL", "mysql connection is required": "请填写 MySQL 连接信息", "sqlite path is required": "请填写 SQLite 路径", "mysql_dsn is required": "请填写 MySQL DSN", "release notices are not configured": "版本日志功能尚未配置", "legacy sync service is not configured": "旧项目同步服务尚未配置", } if translated, ok := exact[lower]; ok { return translated } byCode := map[string]string{ "UNAUTHORIZED": "需要登录后继续操作", "LOGIN_FAILED": "登录失败,请检查密码和验证码", "PASSWORD_CHANGE_FAILED": "密码修改失败", "INVALID_PAYLOAD": "提交内容格式不正确", "DATABASE_TEST_FAILED": "数据库连接测试失败", "DATABASE_IMPORT_FAILED": "SQLite 导入远端库失败", "DATABASE_SYNC_FAILED": "远端库同步回本地失败", "LEGACY_SAVE_FAILED": "兼容 JSON 保存失败", "LEGACY_VALIDATE_FAILED": "兼容 JSON 校验失败", "LEGACY_RESTORE_FAILED": "兼容 JSON 恢复失败", "NOTICE_SAVE_FAILED": "版本日志保存失败", "NOTICE_VALIDATE_FAILED": "版本日志校验失败", "NOTICE_RESTORE_FAILED": "版本日志恢复失败", "PACKAGE_UPLOAD_FAILED": "发布包上传失败", "SOURCE_SAVE_FAILED": "接口源保存失败", "CHECK_FAILED": "接口健康检测失败", "SYNC_FAILED": "同步操作失败", "FORBIDDEN": "没有权限执行该操作", "METHOD_NOT_ALLOWED": "请求方法不正确", "FILE_REQUIRED": "请选择要上传的文件", "CHECK_JOB_NOT_FOUND": "未找到心跳检测任务", "SSE_UNSUPPORTED": "当前运行环境不支持实时事件流", "SOURCES_FAILED": "接口源数据加载失败", "ENDPOINTS_FAILED": "客户端接口数据加载失败", "DASHBOARD_FAILED": "仪表盘数据加载失败", "AUDIT_FAILED": "审计日志加载失败", "FEEDBACK_LIST_FAILED": "反馈列表加载失败", "FEEDBACK_UPDATE_FAILED": "反馈工单更新失败", "NOTICE_NOT_FOUND": "未找到版本日志", "NOTICES_FAILED": "版本日志加载失败", "MEDIA_TYPES_FAILED": "媒体源 JSON 加载失败", "SOURCE_CALL_FAILED": "接口调用状态上报失败", "IMPORT_FAILED": "导入失败", "PATH_FAILED": "路径解析失败", "INVALID_UPLOAD": "上传内容不正确", "BOOTSTRAP_FAILED": "后台初始化信息加载失败", "CAPTCHA_FAILED": "验证码加载失败", "TOO_LARGE": "反馈包过大", "MISSING_FIELD": "缺少旧反馈提交字段", "INVALID_TIMESTAMP": "反馈提交时间已过期", "INVALID_SIGNATURE": "反馈签名校验失败", "INVALID_PACKAGE": "反馈包格式不正确", "INVALID_ENCRYPTED_PACKAGE": "反馈加密包格式不正确", "DECRYPT_FAILED": "反馈包解密失败", "HASH_MISMATCH": "反馈包哈希校验失败", "SERVER_CONFIG": "反馈服务配置异常", } if translated, ok := byCode[code]; ok { if raw == "" || strings.EqualFold(raw, code) { return translated } return translated + ":" + raw } if raw == "" { return "操作失败" } return raw } func cleanPath(path string) string { if path == "" { return "/" } if path != "/" { path = strings.TrimRight(path, "/") } if path == "" { return "/" } return path } func requestBaseURL(r *http.Request, fallback string) string { scheme := r.Header.Get("X-Forwarded-Proto") if scheme == "" { if r.TLS != nil { scheme = "https" } else { scheme = "http" } } if r.Host != "" { return scheme + "://" + r.Host } return strings.TrimRight(fallback, "/") } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" }