179 lines
8.1 KiB
Go
179 lines
8.1 KiB
Go
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 连接信息",
|
||
"mysql database is required": "请填写 MySQL 数据库名",
|
||
"mysql username is required": "请填写 MySQL 数据库用户",
|
||
"sqlite path is required": "请填写 SQLite 路径",
|
||
"mysql_dsn is required": "请填写 MySQL DSN",
|
||
"remote database is not configured": "远端 MySQL 未配置",
|
||
"database sync is already running": "数据库同步正在执行,请稍后再试",
|
||
"mail is not configured": "邮件通知尚未配置完整",
|
||
"release notices are not configured": "版本日志功能尚未配置",
|
||
"legacy sync service is not configured": "旧项目同步服务尚未配置",
|
||
"update-info requires app_version or title": "更新 JSON 需要填写 app_version 或 title",
|
||
"media-types requires categories array": "媒体源 JSON 需要包含 categories 数组",
|
||
"version or app_version is required": "版本日志需要填写 version 或 app_version",
|
||
}
|
||
if translated, ok := exact[lower]; ok {
|
||
return translated
|
||
}
|
||
byCode := map[string]string{
|
||
"UNAUTHORIZED": "需要登录后继续操作",
|
||
"LOGIN_FAILED": "登录失败,请检查密码和验证码",
|
||
"PASSWORD_CHANGE_FAILED": "密码修改失败",
|
||
"INVALID_PAYLOAD": "提交内容格式不正确",
|
||
"DATABASE_TEST_FAILED": "数据库连接测试失败",
|
||
"DATABASE_SAVE_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": "反馈工单更新失败",
|
||
"MAIL_CONFIG_FAILED": "邮件配置保存失败",
|
||
"MAIL_TEST_FAILED": "测试邮件发送失败",
|
||
"MAIL_RETRY_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 ""
|
||
}
|