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") }