305 lines
8.1 KiB
Go
305 lines
8.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"xboard-go/internal/database"
|
|
"xboard-go/internal/model"
|
|
"xboard-go/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func PluginUserOnlineDevicesUsers(c *gin.Context) {
|
|
if !service.IsPluginEnabled(service.PluginUserOnlineDevices) {
|
|
Fail(c, 400, "plugin is not enabled")
|
|
return
|
|
}
|
|
|
|
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
|
perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
|
|
keyword := strings.TrimSpace(c.Query("keyword"))
|
|
|
|
query := database.DB.Model(&model.User{}).Order("id DESC")
|
|
if keyword != "" {
|
|
query = query.Where("email LIKE ? OR id = ?", "%"+keyword+"%", keyword)
|
|
}
|
|
|
|
var total int64
|
|
query.Count(&total)
|
|
|
|
var users []model.User
|
|
if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&users).Error; err != nil {
|
|
Fail(c, 500, "failed to fetch users")
|
|
return
|
|
}
|
|
|
|
userIDs := make([]int, 0, len(users))
|
|
for _, user := range users {
|
|
userIDs = append(userIDs, user.ID)
|
|
}
|
|
devices := service.GetUsersDevices(userIDs)
|
|
|
|
list := make([]gin.H, 0, len(users))
|
|
usersWithOnlineIP := 0
|
|
totalOnlineIPs := 0
|
|
for _, user := range users {
|
|
subscriptionName := "No subscription"
|
|
if user.PlanID != nil {
|
|
var plan model.Plan
|
|
if database.DB.First(&plan, *user.PlanID).Error == nil {
|
|
subscriptionName = plan.Name
|
|
}
|
|
}
|
|
|
|
ips := devices[user.ID]
|
|
if len(ips) > 0 {
|
|
usersWithOnlineIP++
|
|
totalOnlineIPs += len(ips)
|
|
}
|
|
|
|
list = append(list, gin.H{
|
|
"id": user.ID,
|
|
"email": user.Email,
|
|
"subscription_name": subscriptionName,
|
|
"online_count": len(ips),
|
|
"online_devices": ips,
|
|
"last_online_text": formatTimeValue(user.LastOnlineAt),
|
|
"created_text": formatUnixValue(user.CreatedAt),
|
|
})
|
|
}
|
|
|
|
Success(c, gin.H{
|
|
"list": list,
|
|
"filters": gin.H{
|
|
"keyword": keyword,
|
|
"per_page": perPage,
|
|
},
|
|
"summary": gin.H{
|
|
"page_users": len(users),
|
|
"users_with_online_ip": usersWithOnlineIP,
|
|
"total_online_ips": totalOnlineIPs,
|
|
"current_page": page,
|
|
},
|
|
"pagination": gin.H{
|
|
"current": page,
|
|
"last_page": calculateLastPage(total, perPage),
|
|
"per_page": perPage,
|
|
"total": total,
|
|
},
|
|
})
|
|
}
|
|
|
|
func PluginUserOnlineDevicesGetIP(c *gin.Context) {
|
|
if !service.IsPluginEnabled(service.PluginUserOnlineDevices) {
|
|
Fail(c, 400, "plugin is not enabled")
|
|
return
|
|
}
|
|
|
|
user, ok := currentUser(c)
|
|
if !ok {
|
|
Fail(c, 401, "unauthorized")
|
|
return
|
|
}
|
|
|
|
devices := service.GetUsersDevices([]int{user.ID})
|
|
ips := devices[user.ID]
|
|
authToken, _ := c.Get("auth_token")
|
|
currentToken, _ := authToken.(string)
|
|
sessions := service.GetUserSessions(user.ID, currentToken)
|
|
currentID := currentSessionID(c)
|
|
|
|
sessionItems := make([]gin.H, 0, len(sessions))
|
|
for _, session := range sessions {
|
|
sessionItems = append(sessionItems, gin.H{
|
|
"id": session.ID,
|
|
"name": session.Name,
|
|
"user_agent": session.UserAgent,
|
|
"ip": firstString(session.IP, "-"),
|
|
"created_at": session.CreatedAt,
|
|
"last_used_at": session.LastUsedAt,
|
|
"expires_at": session.ExpiresAt,
|
|
"is_current": session.ID == currentID,
|
|
})
|
|
}
|
|
Success(c, gin.H{
|
|
"ips": ips,
|
|
"session_overview": gin.H{
|
|
"online_ip_count": len(ips),
|
|
"online_ips": ips,
|
|
"online_device_count": len(ips),
|
|
"device_limit": user.DeviceLimit,
|
|
"last_online_at": user.LastOnlineAt,
|
|
"active_session_count": len(sessionItems),
|
|
"sessions": sessionItems,
|
|
},
|
|
})
|
|
}
|
|
|
|
func PluginUserAddIPv6Check(c *gin.Context) {
|
|
if !service.IsPluginEnabled(service.PluginUserAddIPv6) {
|
|
Fail(c, 400, "plugin is not enabled")
|
|
return
|
|
}
|
|
|
|
user, ok := currentUser(c)
|
|
if !ok {
|
|
Fail(c, 401, "unauthorized")
|
|
return
|
|
}
|
|
|
|
if user.PlanID == nil {
|
|
Success(c, gin.H{"allowed": false, "reason": "No active plan"})
|
|
return
|
|
}
|
|
|
|
var plan model.Plan
|
|
if err := database.DB.First(&plan, *user.PlanID).Error; err != nil {
|
|
Success(c, gin.H{"allowed": false, "reason": "No active plan"})
|
|
return
|
|
}
|
|
|
|
ipv6Email := service.IPv6ShadowEmail(user.Email)
|
|
var count int64
|
|
database.DB.Model(&model.User{}).Where("email = ?", ipv6Email).Count(&count)
|
|
|
|
Success(c, gin.H{
|
|
"allowed": service.PluginPlanAllowed(&plan),
|
|
"is_active": count > 0,
|
|
})
|
|
}
|
|
|
|
func PluginUserAddIPv6Enable(c *gin.Context) {
|
|
if !service.IsPluginEnabled(service.PluginUserAddIPv6) {
|
|
Fail(c, 400, "plugin is not enabled")
|
|
return
|
|
}
|
|
|
|
user, ok := currentUser(c)
|
|
if !ok {
|
|
Fail(c, 401, "unauthorized")
|
|
return
|
|
}
|
|
|
|
if !service.SyncIPv6ShadowAccount(user) {
|
|
Fail(c, 403, "your plan does not support IPv6 subscription")
|
|
return
|
|
}
|
|
|
|
SuccessMessage(c, "IPv6 subscription enabled/synced", true)
|
|
}
|
|
|
|
func PluginUserAddIPv6SyncPassword(c *gin.Context) {
|
|
if !service.IsPluginEnabled(service.PluginUserAddIPv6) {
|
|
Fail(c, 400, "plugin is not enabled")
|
|
return
|
|
}
|
|
|
|
user, ok := currentUser(c)
|
|
if !ok {
|
|
Fail(c, 401, "unauthorized")
|
|
return
|
|
}
|
|
|
|
ipv6Email := service.IPv6ShadowEmail(user.Email)
|
|
result := database.DB.Model(&model.User{}).Where("email = ?", ipv6Email).Update("password", user.Password)
|
|
if result.Error != nil {
|
|
Fail(c, 404, "IPv6 user not found")
|
|
return
|
|
}
|
|
if result.RowsAffected == 0 {
|
|
Fail(c, 404, "IPv6 user not found")
|
|
return
|
|
}
|
|
|
|
SuccessMessage(c, "Password synced to IPv6 account", true)
|
|
}
|
|
|
|
func AdminConfigFetch(c *gin.Context) {
|
|
Success(c, gin.H{
|
|
"app_name": service.MustGetString("app_name", "XBoard"),
|
|
"app_url": service.GetAppURL(),
|
|
"secure_path": service.GetAdminSecurePath(),
|
|
"server_pull_interval": service.MustGetInt("server_pull_interval", 60),
|
|
"server_push_interval": service.MustGetInt("server_push_interval", 60),
|
|
})
|
|
}
|
|
|
|
func AdminSystemStatus(c *gin.Context) {
|
|
Success(c, gin.H{
|
|
"server_time": time.Now().Unix(),
|
|
"app_name": service.MustGetString("app_name", "XBoard"),
|
|
"app_url": service.GetAppURL(),
|
|
})
|
|
}
|
|
|
|
func AdminPluginsList(c *gin.Context) {
|
|
var plugins []model.Plugin
|
|
if err := database.DB.Order("id ASC").Find(&plugins).Error; err != nil {
|
|
Fail(c, 500, "failed to fetch plugins")
|
|
return
|
|
}
|
|
Success(c, plugins)
|
|
}
|
|
|
|
func AdminPluginTypes(c *gin.Context) {
|
|
Success(c, []string{"feature", "payment"})
|
|
}
|
|
|
|
func AdminPluginIntegrationStatus(c *gin.Context) {
|
|
Success(c, gin.H{
|
|
"user_online_devices": gin.H{
|
|
"enabled": service.IsPluginEnabled(service.PluginUserOnlineDevices),
|
|
"status": "complete",
|
|
"summary": "用户侧在线设备概览与后台设备监控已接入 Go 后端。",
|
|
"endpoints": []string{"/api/v1/user/user-online-devices/get-ip", "/api/v1/" + service.GetAdminSecurePath() + "/user-online-devices/users"},
|
|
},
|
|
"real_name_verification": gin.H{
|
|
"enabled": service.IsPluginEnabled(service.PluginRealNameVerification),
|
|
"status": "complete",
|
|
"summary": "实名状态查询、提交、后台审核与批量同步均已整合,并补齐 auto_approve/allow_resubmit_after_reject 行为。",
|
|
"endpoints": []string{"/api/v1/user/real-name-verification/status", "/api/v1/user/real-name-verification/submit", "/api/v1/" + service.GetAdminSecurePath() + "/realname/records"},
|
|
},
|
|
"user_add_ipv6_subscription": gin.H{
|
|
"enabled": service.IsPluginEnabled(service.PluginUserAddIPv6),
|
|
"status": "integrated_with_runtime_sync",
|
|
"summary": "用户启用、密码同步与运行时影子账号同步已接入;订单生命周期自动钩子仍依赖后续完整订单流重构。",
|
|
"endpoints": []string{"/api/v1/user/user-add-ipv6-subscription/check", "/api/v1/user/user-add-ipv6-subscription/enable", "/api/v1/user/user-add-ipv6-subscription/sync-password"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func parsePositiveInt(raw string, defaultValue int) int {
|
|
value, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
if err != nil || value <= 0 {
|
|
return defaultValue
|
|
}
|
|
return value
|
|
}
|
|
|
|
func calculateLastPage(total int64, perPage int) int {
|
|
if perPage <= 0 {
|
|
return 1
|
|
}
|
|
last := int((total + int64(perPage) - 1) / int64(perPage))
|
|
if last == 0 {
|
|
return 1
|
|
}
|
|
return last
|
|
}
|
|
|
|
func formatUnixValue(value int64) string {
|
|
if value <= 0 {
|
|
return "-"
|
|
}
|
|
return time.Unix(value, 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
func formatTimeValue(value *time.Time) string {
|
|
if value == nil {
|
|
return "-"
|
|
}
|
|
return value.Format("2006-01-02 15:04:05")
|
|
}
|