@@ -0,0 +1,166 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user