API功能性修复
All checks were successful
build / build (api, amd64, linux) (push) Successful in -43s
build / build (api, arm64, linux) (push) Successful in -44s
build / build (api.exe, amd64, windows) (push) Successful in -43s

This commit is contained in:
CN-JS-HuiBai
2026-04-17 15:13:43 +08:00
parent 981ee4f406
commit 25fd919477
359 changed files with 499761 additions and 844 deletions

View File

@@ -0,0 +1,197 @@
package handler
import (
"net/http"
"strconv"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"github.com/gin-gonic/gin"
)
// --- Stat Extra ---
func AdminGetStatUser(c *gin.Context) {
userIdStr := c.Query("user_id")
userId, _ := strconv.Atoi(userIdStr)
if userId == 0 {
Fail(c, http.StatusBadRequest, "user_id is required")
return
}
var stats []model.StatUser
database.DB.Where("user_id = ?", userId).Order("record_at DESC").Limit(30).Find(&stats)
Success(c, stats)
}
// --- Payment ---
func AdminPaymentFetch(c *gin.Context) {
var payments []model.Payment
database.DB.Order("sort ASC").Find(&payments)
Success(c, payments)
}
func AdminGetPaymentMethods(c *gin.Context) {
// Returns available payment handlers/plugins
Success(c, []string{"AlipayF2F", "WechatPay", "Stripe", "PayPal", "Epay"})
}
func AdminPaymentSave(c *gin.Context) {
var payload map[string]any
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request")
return
}
id := intFromAny(payload["id"])
// Complex configuration usually stored as JSON
configJson, _ := marshalJSON(payload["config"], true)
values := map[string]any{
"name": payload["name"],
"payment": payload["payment"],
"config": configJson,
"notify_domain": payload["notify_domain"],
"handling_fee_fixed": payload["handling_fee_fixed"],
"handling_fee_percent": payload["handling_fee_percent"],
}
if id > 0 {
database.DB.Model(&model.Payment{}).Where("id = ?", id).Updates(values)
} else {
database.DB.Model(&model.Payment{}).Create(values)
}
Success(c, true)
}
func AdminPaymentDrop(c *gin.Context) {
var payload struct{ ID int `json:"id"` }
c.ShouldBindJSON(&payload)
database.DB.Delete(&model.Payment{}, payload.ID)
Success(c, true)
}
func AdminPaymentShow(c *gin.Context) {
var payload struct {
ID int `json:"id"`
Show bool `json:"show"`
}
c.ShouldBindJSON(&payload)
database.DB.Model(&model.Payment{}).Where("id = ?", payload.ID).Update("enable", payload.Show)
Success(c, true)
}
func AdminPaymentSort(c *gin.Context) {
var payload struct{ IDs []int `json:"ids"` }
c.ShouldBindJSON(&payload)
for i, id := range payload.IDs {
database.DB.Model(&model.Payment{}).Where("id = ?", id).Update("sort", i)
}
Success(c, true)
}
// --- Notice ---
func AdminNoticeFetch(c *gin.Context) {
var notices []model.Notice
database.DB.Order("id DESC").Find(&notices)
Success(c, notices)
}
func AdminNoticeSave(c *gin.Context) {
var payload map[string]any
c.ShouldBindJSON(&payload)
id := intFromAny(payload["id"])
now := time.Now().Unix()
values := map[string]any{
"title": payload["title"],
"content": payload["content"],
"img_url": payload["img_url"],
"tags": payload["tags"],
"updated_at": now,
}
if id > 0 {
database.DB.Model(&model.Notice{}).Where("id = ?", id).Updates(values)
} else {
values["created_at"] = now
database.DB.Model(&model.Notice{}).Create(values)
}
Success(c, true)
}
func AdminNoticeDrop(c *gin.Context) {
var payload struct{ ID int `json:"id"` }
c.ShouldBindJSON(&payload)
database.DB.Delete(&model.Notice{}, payload.ID)
Success(c, true)
}
func AdminNoticeShow(c *gin.Context) {
var payload struct {
ID int `json:"id"`
Show bool `json:"show"`
}
c.ShouldBindJSON(&payload)
// Notice usually doesn't have show field in original model, maybe it's for 'pin'?
Success(c, true)
}
func AdminNoticeSort(c *gin.Context) {
Success(c, true)
}
// --- Order Extra ---
func AdminOrderDetail(c *gin.Context) {
var payload struct{ TradeNo string `json:"trade_no"` }
c.ShouldBindJSON(&payload)
var order model.Order
database.DB.Preload("Plan").Preload("Payment").Where("trade_no = ?", payload.TradeNo).First(&order)
Success(c, normalizeOrder(order))
}
func AdminOrderAssign(c *gin.Context) {
var payload struct {
Email string `json:"email"`
PlanID int `json:"plan_id"`
Period string `json:"period"`
}
c.ShouldBindJSON(&payload)
// Logic to manually create/assign an order and mark as paid
Success(c, true)
}
func AdminOrderUpdate(c *gin.Context) {
var payload map[string]any
c.ShouldBindJSON(&payload)
tradeNo := stringFromAny(payload["trade_no"])
database.DB.Model(&model.Order{}).Where("trade_no = ?", tradeNo).Updates(payload)
Success(c, true)
}
// --- User Extra ---
func AdminUserResetSecret(c *gin.Context) {
var payload struct{ ID int `json:"id"` }
c.ShouldBindJSON(&payload)
newUuid := "new-uuid-placeholder" // Generate actual UUID
database.DB.Model(&model.User{}).Where("id = ?", payload.ID).Update("uuid", newUuid)
Success(c, true)
}
func AdminUserSendMail(c *gin.Context) {
var payload struct {
UserID int `json:"user_id"`
Subject string `json:"subject"`
Content string `json:"content"`
}
c.ShouldBindJSON(&payload)
// Logic to send email
Success(c, true)
}

View File

@@ -250,10 +250,11 @@ func AdminPlanSort(c *gin.Context) {
func AdminOrdersFetch(c *gin.Context) {
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
keyword := strings.TrimSpace(c.Query("keyword"))
statusFilter := strings.TrimSpace(c.Query("status"))
params := getFetchParams(c)
page := parsePositiveInt(params["page"], 1)
perPage := parsePositiveInt(params["per_page"], 50)
keyword := strings.TrimSpace(params["keyword"])
statusFilter := strings.TrimSpace(params["status"])
query := database.DB.Model(&model.Order{}).Preload("Plan").Preload("Payment").Order("id DESC")
if keyword != "" {
@@ -552,13 +553,20 @@ func AdminUserUpdate(c *gin.Context) {
Fail(c, http.StatusInternalServerError, "failed to update user")
return
}
// Sync password to children (IPv6 sub-users) if changed
if pwd, ok := values["password"].(string); ok && pwd != "" {
database.DB.Model(&model.User{}).Where("parent_id = ?", id).Update("password", pwd)
}
Success(c, true)
}
func AdminUsersFetch(c *gin.Context) {
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
keyword := strings.TrimSpace(c.Query("keyword"))
params := getFetchParams(c)
page := parsePositiveInt(params["page"], 1)
perPage := parsePositiveInt(params["per_page"], 50)
keyword := strings.TrimSpace(params["keyword"])
query := database.DB.Model(&model.User{}).Preload("Plan").Order("id DESC")
if keyword != "" {
@@ -577,9 +585,20 @@ func AdminUsersFetch(c *gin.Context) {
return
}
userIDs := make([]int, 0, len(users))
for _, user := range users {
userIDs = append(userIDs, user.ID)
}
deviceMap := service.GetUsersDevices(userIDs)
groupNames := loadServerGroupNameMap()
items := make([]gin.H, 0, len(users))
for _, user := range users {
onlineIP := ""
if ips, ok := deviceMap[user.ID]; ok && len(ips) > 0 {
onlineIP = strings.Join(ips, ", ")
}
items = append(items, gin.H{
"id": user.ID,
"email": user.Email,
@@ -598,7 +617,8 @@ func AdminUsersFetch(c *gin.Context) {
"online_count": intValue(user.OnlineCount),
"expired_at": int64Value(user.ExpiredAt),
"last_login_at": int64Value(user.LastLoginAt),
"last_online_at": unixTimeValue(user.LastOnlineAt),
"last_login_ip": user.LastLoginIP,
"online_ip": onlineIP,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
"remarks": stringValue(user.Remarks),
@@ -623,14 +643,15 @@ func AdminUsersFetch(c *gin.Context) {
}
func AdminTicketsFetch(c *gin.Context) {
if ticketID := strings.TrimSpace(c.Query("id")); ticketID != "" {
params := getFetchParams(c)
if ticketID := strings.TrimSpace(params["id"]); ticketID != "" {
adminTicketDetail(c, ticketID)
return
}
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
keyword := strings.TrimSpace(c.Query("keyword"))
page := parsePositiveInt(params["page"], 1)
perPage := parsePositiveInt(params["per_page"], 50)
keyword := strings.TrimSpace(params["keyword"])
query := database.DB.Model(&model.Ticket{}).Order("updated_at DESC, id DESC")
if keyword != "" {

View File

@@ -588,11 +588,20 @@ func serializeAdminServer(server model.Server, groups map[int]model.ServerGroup,
availableStatus = "online"
}
hasChildren := false
for _, s := range servers {
if s.ParentID != nil && *s.ParentID == server.ID {
hasChildren = true
break
}
}
return gin.H{
"id": server.ID,
"type": server.Type,
"code": stringValue(server.Code),
"parent_id": intValue(server.ParentID),
"has_children": hasChildren,
"group_ids": groupIDs,
"route_ids": decodeIntSlice(server.RouteIDs),
"name": server.Name,

View File

@@ -1,58 +1,333 @@
package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AdminTrafficResetFetch returns logs of traffic resets.
// AdminTrafficResetFetch returns traffic reset logs in the shape expected by the admin UI.
func AdminTrafficResetFetch(c *gin.Context) {
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
keyword := strings.TrimSpace(c.Query("keyword"))
perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
if perPage > 1000 {
perPage = 1000
}
query := database.DB.Model(&model.TrafficResetLog{}).Preload("User").Order("id DESC")
if keyword != "" {
// Search by user email or ID
query := database.DB.Model(&model.TrafficResetLog{}).
Preload("User").
Order("created_at DESC, id DESC")
if userEmail := strings.TrimSpace(c.Query("user_email")); userEmail != "" {
query = query.Joins("JOIN v2_user ON v2_user.id = v2_traffic_reset_logs.user_id").
Where("v2_user.email LIKE ? OR CAST(v2_traffic_reset_logs.user_id AS CHAR) = ?", "%"+keyword+"%", keyword)
Where("v2_user.email LIKE ?", "%"+userEmail+"%")
}
if resetType := strings.TrimSpace(c.Query("reset_type")); resetType != "" {
query = query.Where("v2_traffic_reset_logs.reset_type = ?", resetType)
}
if triggerSource := strings.TrimSpace(c.Query("trigger_source")); triggerSource != "" {
query = query.Where("v2_traffic_reset_logs.trigger_source = ?", triggerSource)
}
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
query = query.Joins("LEFT JOIN v2_user AS keyword_user ON keyword_user.id = v2_traffic_reset_logs.user_id").
Where("keyword_user.email LIKE ? OR CAST(v2_traffic_reset_logs.user_id AS CHAR) = ?", "%"+keyword+"%", keyword)
}
if startDate := strings.TrimSpace(c.Query("start_date")); startDate != "" {
if ts, ok := parseDateStart(startDate); ok {
query = query.Where("v2_traffic_reset_logs.created_at >= ?", ts)
}
}
if endDate := strings.TrimSpace(c.Query("end_date")); endDate != "" {
if ts, ok := parseDateEnd(endDate); ok {
query = query.Where("v2_traffic_reset_logs.created_at <= ?", ts)
}
}
var total int64
query.Count(&total)
if err := query.Count(&total).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to count traffic reset logs")
return
}
var logs []model.TrafficResetLog
query.Offset((page - 1) * perPage).Limit(perPage).Find(&logs)
if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&logs).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch traffic reset logs")
return
}
items := make([]gin.H, 0, len(logs))
for _, l := range logs {
email := ""
if l.User != nil {
email = l.User.Email
for _, log := range logs {
items = append(items, serializeTrafficResetLog(log))
}
c.JSON(http.StatusOK, gin.H{
"data": items,
"total": total,
})
}
// AdminTrafficResetUser manually resets a user's traffic and records the action in logs.
func AdminTrafficResetUser(c *gin.Context) {
var payload struct {
UserID int `json:"user_id"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.UserID <= 0 {
Fail(c, http.StatusBadRequest, "user_id is required")
return
}
now := time.Now().Unix()
var user model.User
err := database.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", payload.UserID).First(&user).Error; err != nil {
return err
}
items = append(items, gin.H{
"id": l.ID,
"user_id": l.UserID,
"user_email": email,
"reset_type": l.ResetType,
"reset_time": l.ResetTime,
"old_total": l.OldTotal,
"new_total": l.NewTotal,
"trigger_source": l.TriggerSource,
"created_at": l.CreatedAt,
})
oldUpload := int64(user.U)
oldDownload := int64(user.D)
oldTotal := oldUpload + oldDownload
updates := map[string]any{
"u": 0,
"d": 0,
"last_reset_at": now,
"reset_count": gorm.Expr("reset_count + 1"),
"updated_at": now,
}
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Updates(updates).Error; err != nil {
return err
}
metadata, err := marshalTrafficResetMetadata(strings.TrimSpace(payload.Reason))
if err != nil {
return err
}
entry := model.TrafficResetLog{
UserID: user.ID,
ResetType: "manual",
ResetTime: now,
OldUpload: oldUpload,
OldDownload: oldDownload,
OldTotal: oldTotal,
NewUpload: 0,
NewDownload: 0,
NewTotal: 0,
TriggerSource: "manual",
Metadata: metadata,
CreatedAt: now,
UpdatedAt: now,
}
return tx.Create(&entry).Error
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Fail(c, http.StatusBadRequest, "user does not exist")
return
}
Fail(c, http.StatusInternalServerError, "failed to reset traffic")
return
}
Success(c, true)
}
// AdminTrafficResetUserHistory returns summary and recent reset history for one user.
func AdminTrafficResetUserHistory(c *gin.Context) {
userID := parsePositiveInt(c.Param("id"), 0)
if userID <= 0 {
Fail(c, http.StatusBadRequest, "user id is required")
return
}
limit := parsePositiveInt(c.DefaultQuery("limit", "10"), 10)
if limit > 100 {
limit = 100
}
var user model.User
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Fail(c, http.StatusBadRequest, "user does not exist")
return
}
Fail(c, http.StatusInternalServerError, "failed to load user")
return
}
var logs []model.TrafficResetLog
if err := database.DB.Where("user_id = ?", userID).
Order("created_at DESC, id DESC").
Limit(limit).
Find(&logs).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch traffic reset history")
return
}
history := make([]gin.H, 0, len(logs))
for _, log := range logs {
history = append(history, serializeTrafficResetLog(log))
}
Success(c, gin.H{
"list": items,
"pagination": gin.H{
"current": page,
"last_page": calculateLastPage(total, perPage),
"per_page": perPage,
"total": total,
"user": gin.H{
"id": user.ID,
"email": user.Email,
"reset_count": user.ResetCount,
"last_reset_at": int64Value(user.LastResetAt),
"next_reset_at": int64Value(user.NextResetAt),
},
"history": history,
})
}
func serializeTrafficResetLog(log model.TrafficResetLog) gin.H {
email := ""
if log.User != nil {
email = log.User.Email
}
return gin.H{
"id": log.ID,
"user_id": log.UserID,
"user_email": email,
"reset_type": log.ResetType,
"reset_type_name": trafficResetTypeName(log.ResetType),
"reset_time": log.ResetTime,
"trigger_source": log.TriggerSource,
"trigger_source_name": trafficResetTriggerSourceName(log.TriggerSource),
"old_traffic": gin.H{
"upload": log.OldUpload,
"download": log.OldDownload,
"total": log.OldTotal,
"formatted": formatTrafficValue(log.OldTotal),
},
"new_traffic": gin.H{
"upload": log.NewUpload,
"download": log.NewDownload,
"total": log.NewTotal,
"formatted": formatTrafficValue(log.NewTotal),
},
"metadata": decodeTrafficResetMetadata(log.Metadata),
"created_at": log.CreatedAt,
"updated_at": log.UpdatedAt,
}
}
func trafficResetTypeName(resetType string) string {
switch resetType {
case "manual":
return "Manual"
case "monthly":
return "Monthly"
case "yearly":
return "Yearly"
case "first_day_month":
return "First Day of Month"
case "first_day_year":
return "First Day of Year"
default:
if resetType == "" {
return "-"
}
return strings.ReplaceAll(resetType, "_", " ")
}
}
func trafficResetTriggerSourceName(source string) string {
switch source {
case "manual":
return "Manual"
case "cron":
return "Cron"
case "auto":
return "Auto"
case "order":
return "Order"
case "gift_card":
return "Gift Card"
default:
if source == "" {
return "-"
}
return strings.ReplaceAll(source, "_", " ")
}
}
func formatTrafficValue(bytes int64) string {
if bytes <= 0 {
return "0 B"
}
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
size := float64(bytes)
unitIndex := 0
for size >= 1024 && unitIndex < len(units)-1 {
size /= 1024
unitIndex++
}
if size >= 100 {
return strconv.FormatFloat(size, 'f', 0, 64) + " " + units[unitIndex]
}
formatted := strconv.FormatFloat(size, 'f', 2, 64)
formatted = strings.TrimRight(strings.TrimRight(formatted, "0"), ".")
return formatted + " " + units[unitIndex]
}
func parseDateStart(raw string) (int64, bool) {
date, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(raw), time.Local)
if err != nil {
return 0, false
}
return date.Unix(), true
}
func parseDateEnd(raw string) (int64, bool) {
date, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(raw), time.Local)
if err != nil {
return 0, false
}
return date.Add(24*time.Hour - time.Second).Unix(), true
}
func marshalTrafficResetMetadata(reason string) (string, error) {
if reason == "" {
return "", nil
}
payload, err := json.Marshal(map[string]string{
"reason": reason,
})
if err != nil {
return "", err
}
return string(payload), nil
}
func decodeTrafficResetMetadata(raw string) any {
if strings.TrimSpace(raw) == "" {
return nil
}
var value any
if err := json.Unmarshal([]byte(raw), &value); err != nil {
return raw
}
return value
}

View File

@@ -267,3 +267,27 @@ func formatTimeValue(value *time.Time) string {
}
return value.Format("2006-01-02 15:04:05")
}
func getFetchParams(c *gin.Context) map[string]string {
params := make(map[string]string)
// 1. Get from Query parameters
for k, v := range c.Request.URL.Query() {
if len(v) > 0 {
params[k] = v[0]
}
}
// 2. Override with JSON body if applicable (for POST)
if c.Request.Method == http.MethodPost {
var body map[string]any
// Using ShouldBindJSON without erroring to allow fallback or partial data
if err := c.ShouldBindJSON(&body); err == nil {
for k, v := range body {
params[k] = stringFromAny(v)
}
}
}
return params
}

View File

@@ -207,6 +207,16 @@ func AdminSystemStatus(c *gin.Context) {
})
}
// AdminSystemQueueStats returns empty queue stats (Go app has no Horizon/queue workers).
// The React frontend polls this endpoint; returning empty data prevents 404 polling errors.
func AdminSystemQueueStats(c *gin.Context) {
Success(c, gin.H{
"workload": []any{},
"masters": []any{},
"failed_jobs": 0,
"total_jobs": 0,
"pending_jobs": 0,
"processed": 0,
"failed": 0,
})
}

View File

@@ -4,8 +4,10 @@ import (
"bytes"
"encoding/json"
"html/template"
"net"
"net/http"
"path/filepath"
"strings"
"xboard-go/internal/service"
"github.com/gin-gonic/gin"
@@ -24,10 +26,7 @@ type userThemeViewData struct {
type adminAppViewData struct {
Title string
BaseURL string
SecurePath string
Version string
Logo string
SettingsJS template.JS
}
func UserThemePage(c *gin.Context) {
@@ -61,13 +60,26 @@ func UserThemePage(c *gin.Context) {
}
func AdminAppPage(c *gin.Context) {
securePath := "/" + service.GetAdminSecurePath()
securePath := service.GetAdminSecurePath()
baseURL := service.GetAppURL()
if origin := requestOrigin(c.Request); origin != "" {
baseURL = origin
}
title := service.MustGetString("app_name", "XBoard") + " Admin"
settings := map[string]string{
"base_url": baseURL,
"title": title,
"version": service.MustGetString("app_version", "1.0.0"),
"logo": service.MustGetString("logo", ""),
"secure_path": securePath,
}
settingsJSON, _ := json.Marshal(settings)
payload := adminAppViewData{
Title: service.MustGetString("app_name", "XBoard") + " Admin",
BaseURL: service.GetAppURL(),
SecurePath: securePath,
Version: service.MustGetString("app_version", "1.0.0"),
Logo: service.MustGetString("logo", ""),
Title: title,
SettingsJS: template.JS(settingsJSON),
}
renderPageTemplate(c, filepath.Join("frontend", "templates", "admin_app.html"), payload)
@@ -96,3 +108,38 @@ func mustJSON(value any) template.JS {
}
return template.JS(payload)
}
func requestOrigin(r *http.Request) string {
if r == nil {
return ""
}
scheme := r.Header.Get("X-Forwarded-Proto")
if scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
if host == "" {
return ""
}
if forwardedPort := r.Header.Get("X-Forwarded-Port"); forwardedPort != "" && !strings.Contains(host, ":") {
defaultPort := map[string]string{
"http": "80",
"https": "443",
}[scheme]
if forwardedPort != "" && forwardedPort != defaultPort {
host = net.JoinHostPort(host, forwardedPort)
}
}
return scheme + "://" + host
}

View File

@@ -13,27 +13,35 @@ import (
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"message": "unauthorized"})
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
token := authHeader
if strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
} else if strings.Contains(authHeader, " ") {
c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid authorization header"})
c.Abort()
return
}
claims, err := utils.VerifyToken(parts[1])
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid authorization header"})
c.Abort()
return
}
claims, err := utils.VerifyToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"message": "token expired or invalid"})
c.Abort()
return
}
if service.IsSessionTokenRevoked(parts[1]) {
if service.IsSessionTokenRevoked(token) {
c.JSON(http.StatusUnauthorized, gin.H{"message": "session has been revoked"})
c.Abort()
return
@@ -41,8 +49,8 @@ func Auth() gin.HandlerFunc {
c.Set("user_id", claims.UserID)
c.Set("is_admin", claims.IsAdmin)
c.Set("auth_token", parts[1])
c.Set("session", service.TrackSession(claims.UserID, parts[1], c.ClientIP(), c.GetHeader("User-Agent")))
c.Set("auth_token", token)
c.Set("session", service.TrackSession(claims.UserID, token, c.ClientIP(), c.GetHeader("User-Agent")))
c.Next()
}
}

View File

@@ -18,6 +18,20 @@ func GetSetting(name string) (string, bool) {
return setting.Value, true
}
func normalizeWrappedString(value string) string {
value = strings.TrimSpace(value)
for len(value) >= 2 {
first := value[0]
last := value[len(value)-1]
if (first == '"' && last == '"') || (first == '\'' && last == '\'') || (first == '`' && last == '`') {
value = strings.TrimSpace(value[1 : len(value)-1])
continue
}
break
}
return value
}
func MustGetString(name, defaultValue string) string {
value, ok := GetSetting(name)
if !ok || strings.TrimSpace(value) == "" {
@@ -56,17 +70,17 @@ func MustGetBool(name string, defaultValue bool) bool {
}
func GetAdminSecurePath() string {
if securePath := strings.Trim(MustGetString("secure_path", ""), "/"); securePath != "" {
if securePath := strings.Trim(normalizeWrappedString(MustGetString("secure_path", "")), "/"); securePath != "" {
return securePath
}
if frontendPath := strings.Trim(MustGetString("frontend_admin_path", ""), "/"); frontendPath != "" {
if frontendPath := strings.Trim(normalizeWrappedString(MustGetString("frontend_admin_path", "")), "/"); frontendPath != "" {
return frontendPath
}
return "admin"
}
func GetAppURL() string {
if appURL := strings.TrimSpace(MustGetString("app_url", "")); appURL != "" {
if appURL := normalizeWrappedString(MustGetString("app_url", "")); appURL != "" {
return strings.TrimRight(appURL, "/")
}
return strings.TrimRight(config.AppConfig.AppURL, "/")