package handler import ( "strconv" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" "xboard-go/internal/service" "github.com/gin-gonic/gin" ) const unverifiedExpiration = int64(946684800) func PluginRealNameStatus(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, 401, "unauthorized") return } auth := realNameRecordForUser(user) status := "unverified" var realName, masked, rejectReason string var submittedAt, reviewedAt int64 if auth != nil { status = auth.Status realName = auth.RealName masked = auth.IdentityMasked rejectReason = auth.RejectReason submittedAt = auth.SubmittedAt reviewedAt = auth.ReviewedAt } canSubmit := status == "unverified" || (status == "rejected" && service.GetPluginConfigBool(service.PluginRealNameVerification, "allow_resubmit_after_reject", true)) Success(c, gin.H{ "status": status, "status_label": realNameStatusLabel(status), "is_inherited": strings.Contains(user.Email, "-ipv6@"), "can_submit": canSubmit, "real_name": realName, "identity_no_masked": masked, "notice": service.GetPluginConfigString(service.PluginRealNameVerification, "verification_notice", "Please submit real-name information."), "reject_reason": rejectReason, "submitted_at": submittedAt, "reviewed_at": reviewedAt, }) } func PluginRealNameSubmit(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, 401, "unauthorized") return } var req struct { RealName string `json:"real_name" binding:"required"` IdentityNo string `json:"identity_no" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, 422, "missing identity information") return } now := time.Now().Unix() record := model.RealNameAuth{ UserID: uint64(user.ID), RealName: req.RealName, IdentityMasked: maskIdentity(req.IdentityNo), IdentityEncrypted: req.IdentityNo, Status: "pending", SubmittedAt: now, ReviewedAt: 0, CreatedAt: now, UpdatedAt: now, } var existing model.RealNameAuth status := "pending" reviewedAt := int64(0) if service.GetPluginConfigBool(service.PluginRealNameVerification, "auto_approve", false) { status = "approved" reviewedAt = now } if err := database.DB.Where("user_id = ?", user.ID).First(&existing).Error; err == nil { if existing.Status == "approved" { Fail(c, 400, "verification already approved") return } if existing.Status == "pending" { Fail(c, 400, "verification is pending review") return } if existing.Status == "rejected" && !service.GetPluginConfigBool(service.PluginRealNameVerification, "allow_resubmit_after_reject", true) { Fail(c, 400, "rejected records cannot be resubmitted") return } record.ID = existing.ID record.CreatedAt = existing.CreatedAt record.Status = status record.ReviewedAt = reviewedAt record.RejectReason = "" database.DB.Model(&existing).Updates(record) } else { record.Status = status record.ReviewedAt = reviewedAt database.DB.Create(&record) } syncRealNameExpiration(user.ID, status) SuccessMessage(c, "verification submitted", gin.H{"status": status}) } func PluginRealNameRecords(c *gin.Context) { page := parsePositiveInt(c.DefaultQuery("page", "1"), 1) perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20) keyword := strings.TrimSpace(c.Query("keyword")) query := database.DB.Table("v2_user AS u"). Select("u.id, u.email, r.status, r.real_name, r.identity_masked, r.submitted_at, r.reviewed_at"). Joins("LEFT JOIN v2_realname_auth AS r ON u.id = r.user_id"). Where("u.email NOT LIKE ?", "%-ipv6@%") if keyword != "" { query = query.Where("u.email LIKE ?", "%"+keyword+"%") } var total int64 query.Count(&total) type recordRow struct { ID int Email string Status *string RealName *string IdentityMasked *string SubmittedAt *int64 ReviewedAt *int64 } var rows []recordRow if err := query.Offset((page - 1) * perPage).Limit(perPage).Scan(&rows).Error; err != nil { Fail(c, 500, "failed to fetch records") return } items := make([]gin.H, 0, len(rows)) for _, row := range rows { status := "unverified" if row.Status != nil && *row.Status != "" { status = *row.Status } items = append(items, gin.H{ "id": row.ID, "email": row.Email, "status": status, "status_label": realNameStatusLabel(status), "real_name": stringPointerValue(row.RealName), "identity_no_masked": stringPointerValue(row.IdentityMasked), "submitted_at": int64PointerValue(row.SubmittedAt), "reviewed_at": int64PointerValue(row.ReviewedAt), }) } Success(c, gin.H{ "status": "success", "data": items, "pagination": gin.H{ "total": total, "current": page, "last_page": calculateLastPage(total, perPage), }, }) } func PluginRealNameReview(c *gin.Context) { userID := parsePositiveInt(c.Param("userId"), 0) if userID == 0 { Fail(c, 400, "invalid user id") return } var req struct { Status string `json:"status"` Reason string `json:"reason"` } _ = c.ShouldBindJSON(&req) if req.Status == "" { req.Status = "approved" } now := time.Now().Unix() var record model.RealNameAuth if err := database.DB.Where("user_id = ?", userID).First(&record).Error; err == nil { record.Status = req.Status record.RejectReason = req.Reason record.ReviewedAt = now record.UpdatedAt = now database.DB.Save(&record) } else { database.DB.Create(&model.RealNameAuth{ UserID: uint64(userID), Status: req.Status, RealName: "admin approved", IdentityMasked: "admin approved", SubmittedAt: now, ReviewedAt: now, RejectReason: req.Reason, CreatedAt: now, UpdatedAt: now, }) } syncRealNameExpiration(userID, req.Status) SuccessMessage(c, "review updated", true) } func PluginRealNameReset(c *gin.Context) { userID := parsePositiveInt(c.Param("userId"), 0) if userID == 0 { Fail(c, 400, "invalid user id") return } database.DB.Where("user_id = ?", userID).Delete(&model.RealNameAuth{}) syncRealNameExpiration(userID, "unverified") SuccessMessage(c, "record reset", true) } func PluginRealNameSyncAll(c *gin.Context) { SuccessMessage(c, "sync completed", performGlobalRealNameSync()) } func PluginRealNameApproveAll(c *gin.Context) { var users []model.User database.DB.Where("email NOT LIKE ?", "%-ipv6@%").Find(&users) now := time.Now().Unix() for _, user := range users { var record model.RealNameAuth if err := database.DB.Where("user_id = ?", user.ID).First(&record).Error; err == nil { record.Status = "approved" record.RealName = "admin approved" record.IdentityMasked = "admin approved" record.SubmittedAt = now record.ReviewedAt = now record.UpdatedAt = now database.DB.Save(&record) } else { database.DB.Create(&model.RealNameAuth{ UserID: uint64(user.ID), Status: "approved", RealName: "admin approved", IdentityMasked: "admin approved", SubmittedAt: now, ReviewedAt: now, CreatedAt: now, UpdatedAt: now, }) } } SuccessMessage(c, "all users approved", performGlobalRealNameSync()) } func PluginRealNameClearCache(c *gin.Context) { _ = database.CacheDelete("realname:sync") SuccessMessage(c, "cache cleared", true) } func realNameRecordForUser(user *model.User) *model.RealNameAuth { if user == nil { return nil } var record model.RealNameAuth if err := database.DB.Where("user_id = ?", user.ID).First(&record).Error; err == nil { return &record } if strings.Contains(user.Email, "-ipv6@") { mainEmail := strings.Replace(user.Email, "-ipv6@", "@", 1) var mainUser model.User if err := database.DB.Where("email = ?", mainEmail).First(&mainUser).Error; err == nil { if err := database.DB.Where("user_id = ?", mainUser.ID).First(&record).Error; err == nil { return &record } } } return nil } func syncRealNameExpiration(userID int, status string) { if status == "approved" { database.DB.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]any{ "expired_at": parseExpirationSetting(), "updated_at": time.Now().Unix(), }) return } if !service.GetPluginConfigBool(service.PluginRealNameVerification, "enforce_real_name", false) { database.DB.Model(&model.User{}).Where("id = ?", userID).Update("updated_at", time.Now().Unix()) return } database.DB.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]any{ "expired_at": unverifiedExpiration, "updated_at": time.Now().Unix(), }) } func parseExpirationSetting() int64 { raw := service.GetPluginConfigString(service.PluginRealNameVerification, "verified_expiration_date", "2099-12-31") if value, err := strconv.ParseInt(raw, 10, 64); err == nil { return value } if parsed, err := time.Parse("2006-01-02", raw); err == nil { return parsed.Unix() } return time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC).Unix() } func performGlobalRealNameSync() gin.H { expireApproved := parseExpirationSetting() now := time.Now().Unix() enforce := service.GetPluginConfigBool(service.PluginRealNameVerification, "enforce_real_name", false) type approvedRow struct { UserID int } var approvedRows []approvedRow database.DB.Table("v2_realname_auth").Select("user_id").Where("status = ?", "approved").Scan(&approvedRows) approvedUsers := make([]int, 0, len(approvedRows)) for _, row := range approvedRows { approvedUsers = append(approvedUsers, row.UserID) } if len(approvedUsers) > 0 { database.DB.Model(&model.User{}).Where("id IN ?", approvedUsers). Updates(map[string]any{"expired_at": expireApproved, "updated_at": now}) if enforce { database.DB.Model(&model.User{}).Where("id NOT IN ?", approvedUsers). Updates(map[string]any{"expired_at": unverifiedExpiration, "updated_at": now}) } } else { if enforce { database.DB.Model(&model.User{}). Updates(map[string]any{"expired_at": unverifiedExpiration, "updated_at": now}) } } return gin.H{ "enforce_real_name": enforce, "total_verified": len(approvedUsers), "actual_synced": len(approvedUsers), } } func realNameStatusLabel(status string) string { switch status { case "approved": return "approved" case "pending": return "pending" case "rejected": return "rejected" default: return "unverified" } } func maskIdentity(identity string) string { if len(identity) <= 8 { return identity } return identity[:4] + "**********" + identity[len(identity)-4:] } func stringPointerValue(value *string) string { if value == nil { return "" } return *value } func int64PointerValue(value *int64) int64 { if value == nil { return 0 } return *value }