mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
dev day #92
This commit is contained in:
@@ -2261,10 +2261,14 @@ func (bc *BaseController) GetAdminMatches(c *gin.Context) {
|
||||
m["venue"] = *ov.VenueOverride
|
||||
}
|
||||
if ov.DateTimeOverride != nil {
|
||||
// Keep a consistent ISO string for machines and Czech human-readable for display
|
||||
m["date_time"] = ov.DateTimeOverride.Format(time.RFC3339)
|
||||
m["date"] = ov.DateTimeOverride.Format("2006-01-02 15:04")
|
||||
m["date"] = ov.DateTimeOverride.Format("02.01.2006")
|
||||
m["time"] = ov.DateTimeOverride.Format("15:04")
|
||||
}
|
||||
if ov.ScoreOverride != nil {
|
||||
m["score"] = strings.TrimSpace(*ov.ScoreOverride)
|
||||
}
|
||||
if ov.HomeLogoURL != nil {
|
||||
m["home_logo_url"] = *ov.HomeLogoURL
|
||||
}
|
||||
@@ -2323,6 +2327,7 @@ func (bc *BaseController) PutMatchOverride(c *gin.Context) {
|
||||
AwayNameOverride *string `json:"away_name_override"`
|
||||
VenueOverride *string `json:"venue_override"`
|
||||
DateTimeOverride *time.Time `json:"date_time_override"`
|
||||
ScoreOverride *string `json:"score_override"`
|
||||
HomeLogoURL *string `json:"home_logo_url"`
|
||||
AwayLogoURL *string `json:"away_logo_url"`
|
||||
Notes *string `json:"notes"`
|
||||
@@ -2345,6 +2350,7 @@ func (bc *BaseController) PutMatchOverride(c *gin.Context) {
|
||||
item.AwayNameOverride = body.AwayNameOverride
|
||||
item.VenueOverride = body.VenueOverride
|
||||
item.DateTimeOverride = body.DateTimeOverride
|
||||
item.ScoreOverride = body.ScoreOverride
|
||||
item.HomeLogoURL = body.HomeLogoURL
|
||||
item.AwayLogoURL = body.AwayLogoURL
|
||||
if body.Notes != nil {
|
||||
@@ -2387,12 +2393,29 @@ func (bc *BaseController) PatchMatchOverride(c *gin.Context) {
|
||||
}
|
||||
// Prevent changing the key
|
||||
delete(body, "external_match_id")
|
||||
// Normalize date_time_override to *time.Time if provided as string
|
||||
if v, ok := body["date_time_override"]; ok {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
s := strings.TrimSpace(vv)
|
||||
if s == "" {
|
||||
body["date_time_override"] = nil
|
||||
} else {
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
body["date_time_override"] = &t
|
||||
} else if t2, err2 := time.Parse("2006-01-02T15:04", s); err2 == nil {
|
||||
body["date_time_override"] = &t2
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Neplatný formát date_time_override"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := bc.DB.Model(&item).Updates(body).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze uložit změny"})
|
||||
return
|
||||
}
|
||||
// Best-effort: write JSON snapshot to cache
|
||||
go bc.writeTeamLogoOverridesCache()
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
@@ -2413,34 +2436,34 @@ func (bc *BaseController) GetTeamLogoOverrides(c *gin.Context) {
|
||||
// "by_id": { "<external_team_id>": { "name": "Team Name", "logo_url": "https://.../logo.png" } }
|
||||
// }
|
||||
func (bc *BaseController) GetPublicTeamLogoOverrides(c *gin.Context) {
|
||||
var items []models.TeamLogoOverride
|
||||
if err := bc.DB.Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Chyba databáze"})
|
||||
return
|
||||
}
|
||||
m := make(map[string]string, len(items))
|
||||
byID := make(map[string]any, len(items))
|
||||
for _, it := range items {
|
||||
if it.TeamName != "" && it.LogoURL != "" {
|
||||
// Primary exact key
|
||||
m[it.TeamName] = it.LogoURL
|
||||
// Add smart aliases so frontends can match sponsor-shortened variants
|
||||
for _, alias := range generateTeamNameAliases(it.TeamName) {
|
||||
if alias != "" {
|
||||
m[alias] = it.LogoURL
|
||||
}
|
||||
}
|
||||
}
|
||||
if it.ExternalTeamID != "" {
|
||||
byID[it.ExternalTeamID] = map[string]string{
|
||||
"name": it.TeamName,
|
||||
"logo_url": it.LogoURL,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Public cacheable response
|
||||
c.Header("Cache-Control", "public, max-age=120")
|
||||
c.JSON(http.StatusOK, gin.H{"by_name": m, "by_id": byID})
|
||||
var items []models.TeamLogoOverride
|
||||
if err := bc.DB.Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Chyba databáze"})
|
||||
return
|
||||
}
|
||||
m := make(map[string]string, len(items))
|
||||
byID := make(map[string]any, len(items))
|
||||
for _, it := range items {
|
||||
if it.TeamName != "" && it.LogoURL != "" {
|
||||
// Primary exact key
|
||||
m[it.TeamName] = it.LogoURL
|
||||
// Add smart aliases so frontends can match sponsor-shortened variants
|
||||
for _, alias := range generateTeamNameAliases(it.TeamName) {
|
||||
if alias != "" {
|
||||
m[alias] = it.LogoURL
|
||||
}
|
||||
}
|
||||
}
|
||||
if it.ExternalTeamID != "" {
|
||||
byID[it.ExternalTeamID] = map[string]string{
|
||||
"name": it.TeamName,
|
||||
"logo_url": it.LogoURL,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Public cacheable response
|
||||
c.Header("Cache-Control", "public, max-age=120")
|
||||
c.JSON(http.StatusOK, gin.H{"by_name": m, "by_id": byID})
|
||||
}
|
||||
|
||||
// writeTeamLogoOverridesCache writes a JSON snapshot of team-logo overrides to cache/prefetch/team_logo_overrides.json
|
||||
|
||||
@@ -386,6 +386,7 @@ type commentOutput struct {
|
||||
TargetLabel string `json:"target_label,omitempty"`
|
||||
ParentID *uint `json:"parent_id,omitempty"`
|
||||
Content string `json:"content"`
|
||||
ContentHTML string `json:"content_html,omitempty"`
|
||||
Status string `json:"status"`
|
||||
IsEdited bool `json:"is_edited"`
|
||||
EditedAt *time.Time `json:"edited_at"`
|
||||
@@ -542,6 +543,9 @@ func (cc *CommentController) GetComments(c *gin.Context) {
|
||||
|
||||
for _, r := range rows {
|
||||
co := toOutput(r)
|
||||
if role != "admin" {
|
||||
co.ContentHTML = services.MaskBadWordsHTML(co.Content)
|
||||
}
|
||||
if co.User.ID != 0 {
|
||||
if p, ok := profByUser[co.User.ID]; ok {
|
||||
if strings.TrimSpace(p.Username) != "" { co.User.Username = p.Username }
|
||||
@@ -620,12 +624,11 @@ func (cc *CommentController) CreateComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Spam evaluation and bad words filtering
|
||||
// Spam evaluation and moderation (do not alter stored content)
|
||||
score, rules := services.EvaluateSpamScore(content)
|
||||
filtered, _ := services.FilterBadWords(content)
|
||||
status := "visible"
|
||||
// Moderation only if sensitive terms detected
|
||||
if ok, _ := services.ContainsSensitiveWords(filtered); ok {
|
||||
if ok, _ := services.ContainsSensitiveWords(content); ok {
|
||||
status = "hidden"
|
||||
}
|
||||
rulesJSON, _ := json.Marshal(rules)
|
||||
@@ -635,7 +638,7 @@ func (cc *CommentController) CreateComment(c *gin.Context) {
|
||||
TargetID: in.TargetID,
|
||||
UserID: userID,
|
||||
ParentID: in.ParentID,
|
||||
Content: filtered,
|
||||
Content: content,
|
||||
Status: status,
|
||||
SpamScore: float32(score),
|
||||
SpamRules: string(rulesJSON),
|
||||
@@ -701,11 +704,10 @@ func (cc *CommentController) UpdateComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter & re-evaluate basic spam (do not auto-hide unless sensitive)
|
||||
// Re-evaluate basic spam, keep original content (masking is done on output)
|
||||
score, rules := services.EvaluateSpamScore(content)
|
||||
filtered, _ := services.FilterBadWords(content)
|
||||
now := time.Now()
|
||||
cm.Content = filtered
|
||||
cm.Content = content
|
||||
cm.IsEdited = true
|
||||
cm.EditedAt = &now
|
||||
cm.SpamScore = float32(score)
|
||||
|
||||
@@ -16,6 +16,7 @@ type MatchOverride struct {
|
||||
AwayNameOverride *string `json:"away_name_override"`
|
||||
VenueOverride *string `json:"venue_override"`
|
||||
DateTimeOverride *time.Time `json:"date_time_override"`
|
||||
ScoreOverride *string `json:"score_override"`
|
||||
HomeLogoURL *string `json:"home_logo_url"`
|
||||
AwayLogoURL *string `json:"away_logo_url"`
|
||||
Notes string `gorm:"type:text" json:"notes"`
|
||||
|
||||
@@ -560,6 +560,8 @@ func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
|
||||
|
||||
api.GET("/umami/config", umamiController.GetUmamiConfig)
|
||||
api.POST("/umami/initialize-setup", umamiController.InitializeUmamiSetup)
|
||||
// Adblock-safe public alias for config (avoids 'umami' keyword)
|
||||
api.GET("/insights/config", umamiController.GetUmamiConfig)
|
||||
|
||||
umami := api.Group("/admin/umami")
|
||||
umami.Use(middleware.JWTAuth(db))
|
||||
@@ -571,6 +573,17 @@ func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
|
||||
umami.GET("/pageviews", umamiController.GetPageviews)
|
||||
}
|
||||
|
||||
// Adblock-safe admin aliases (avoid 'umami'/'metrics' in path)
|
||||
insights := api.Group("/admin/insights")
|
||||
insights.Use(middleware.JWTAuth(db))
|
||||
insights.Use(middleware.RoleAuth("admin"))
|
||||
{
|
||||
insights.POST("/initialize", umamiController.InitializeUmami)
|
||||
insights.GET("/summary", umamiController.GetStats)
|
||||
insights.GET("/breakdown/:type", umamiController.GetMetrics)
|
||||
insights.GET("/pageviews", umamiController.GetPageviews)
|
||||
}
|
||||
|
||||
RegisterContactInfoRoutes(api, db)
|
||||
|
||||
api.POST("/upload", middleware.RateLimit(30, time.Minute), baseController.UploadImage)
|
||||
@@ -618,7 +631,7 @@ func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
|
||||
api.GET("/matches", baseController.GetMatches)
|
||||
api.GET("/matches/history", baseController.GetMatchesHistory)
|
||||
api.GET("/standings", baseController.GetStandings)
|
||||
|
||||
|
||||
api.GET("/gallery/albums", galleryController.GetGalleryAlbums)
|
||||
api.GET("/gallery/albums/:id", galleryController.GetGalleryAlbum)
|
||||
api.GET("/gallery/proxy-image", galleryController.ProxyImage)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"html"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A compact list of Czech and English bad words with family-friendly replacements.
|
||||
@@ -138,6 +140,56 @@ func FilterBadWords(s string) (string, bool) {
|
||||
return out, replaced
|
||||
}
|
||||
|
||||
func MaskBadWords(s string) (string, []string) {
|
||||
if strings.TrimSpace(s) == "" { return s, nil }
|
||||
out := s
|
||||
originals := []string{}
|
||||
for _, cr := range compiledRepls {
|
||||
out = cr.re.ReplaceAllStringFunc(out, func(m string) string {
|
||||
originals = append(originals, m)
|
||||
n := len([]rune(m))
|
||||
if n <= 0 { return m }
|
||||
return strings.Repeat("*", n)
|
||||
})
|
||||
}
|
||||
return out, originals
|
||||
}
|
||||
|
||||
func MaskBadWordsHTML(s string) string {
|
||||
if strings.TrimSpace(s) == "" { return html.EscapeString(s) }
|
||||
var b strings.Builder
|
||||
i := 0
|
||||
for i < len(s) {
|
||||
sub := s[i:]
|
||||
best := ""
|
||||
bestLen := 0
|
||||
for _, cr := range compiledRepls {
|
||||
if loc := cr.re.FindStringIndex(sub); loc != nil && loc[0] == 0 {
|
||||
m := sub[:loc[1]]
|
||||
if len(m) > bestLen {
|
||||
best = m
|
||||
bestLen = len(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestLen > 0 {
|
||||
n := utf8.RuneCountInString(best)
|
||||
b.WriteString(`<span class="cw" title="`)
|
||||
b.WriteString(html.EscapeString(best))
|
||||
b.WriteString(`">`)
|
||||
b.WriteString(strings.Repeat("*", n))
|
||||
b.WriteString("</span>")
|
||||
i += bestLen
|
||||
continue
|
||||
}
|
||||
r, size := utf8.DecodeRuneInString(sub)
|
||||
if r == utf8.RuneError && size == 0 { break }
|
||||
b.WriteString(html.EscapeString(sub[:size]))
|
||||
i += size
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ContainsSensitiveWords returns true and the matched words if content contains strong/explicit terms.
|
||||
func ContainsSensitiveWords(s string) (bool, []string) {
|
||||
if strings.TrimSpace(s) == "" { return false, nil }
|
||||
|
||||
Reference in New Issue
Block a user