mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
dev day #100 - WE ARE FUCKING DONE, hotfixes incoming but we did it in 100 days, lets fucking go guys, anyone reading this...i love you
This commit is contained in:
@@ -29,66 +29,68 @@ type ShortLinkController struct {
|
||||
// Restrictions: only allows shortening links pointing to this site (request host)
|
||||
// or to the configured FrontendBaseURL. Intended for visitor share/copy flows.
|
||||
func (s *ShortLinkController) PublicCreateShortLink(c *gin.Context) {
|
||||
var body struct {
|
||||
TargetURL string `json:"target_url"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
target, err := parseTarget(body.TargetURL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target_url"})
|
||||
return
|
||||
}
|
||||
tu, _ := url.Parse(target)
|
||||
if tu == nil || tu.Host == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target_url"})
|
||||
return
|
||||
}
|
||||
// Allow only same-site or configured frontend host
|
||||
reqHost := c.Request.Host
|
||||
stripPort := func(h string) string {
|
||||
if i := strings.IndexByte(h, ':'); i >= 0 { return h[:i] }
|
||||
return h
|
||||
}
|
||||
allowed := stripPort(tu.Host) == stripPort(reqHost)
|
||||
if !allowed && config.AppConfig != nil && strings.TrimSpace(config.AppConfig.FrontendBaseURL) != "" {
|
||||
if fu, err := url.Parse(config.AppConfig.FrontendBaseURL); err == nil && fu.Host != "" {
|
||||
if stripPort(fu.Host) == stripPort(tu.Host) {
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "target host not allowed"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
TargetURL string `json:"target_url"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
target, err := parseTarget(body.TargetURL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target_url"})
|
||||
return
|
||||
}
|
||||
tu, _ := url.Parse(target)
|
||||
if tu == nil || tu.Host == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target_url"})
|
||||
return
|
||||
}
|
||||
// Allow only same-site or configured frontend host
|
||||
reqHost := c.Request.Host
|
||||
stripPort := func(h string) string {
|
||||
if i := strings.IndexByte(h, ':'); i >= 0 {
|
||||
return h[:i]
|
||||
}
|
||||
return h
|
||||
}
|
||||
allowed := stripPort(tu.Host) == stripPort(reqHost)
|
||||
if !allowed && config.AppConfig != nil && strings.TrimSpace(config.AppConfig.FrontendBaseURL) != "" {
|
||||
if fu, err := url.Parse(config.AppConfig.FrontendBaseURL); err == nil && fu.Host != "" {
|
||||
if stripPort(fu.Host) == stripPort(tu.Host) {
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "target host not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Deterministic code from URL so repeated calls return same shortlink
|
||||
code := "p-" + codeFromHash(target, 7)
|
||||
link := models.ShortLink{
|
||||
Code: code,
|
||||
TargetURL: target,
|
||||
Title: strings.TrimSpace(body.Title),
|
||||
Active: true,
|
||||
}
|
||||
if err := s.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "code"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"target_url", "title", "active", "updated_at"}),
|
||||
}).Create(&link).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot save shortlink", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
var saved models.ShortLink
|
||||
if err := s.DB.Where("code = ?", code).First(&saved).Error; err != nil {
|
||||
saved = link
|
||||
}
|
||||
scheme := getScheme(c)
|
||||
host := c.Request.Host
|
||||
shortURL := fmt.Sprintf("%s://%s/s/%s", scheme, host, code)
|
||||
c.JSON(http.StatusOK, gin.H{"id": saved.ID, "code": code, "short_url": shortURL, "link": saved})
|
||||
// Deterministic code from URL so repeated calls return same shortlink
|
||||
code := "p-" + codeFromHash(target, 7)
|
||||
link := models.ShortLink{
|
||||
Code: code,
|
||||
TargetURL: target,
|
||||
Title: strings.TrimSpace(body.Title),
|
||||
Active: true,
|
||||
}
|
||||
if err := s.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "code"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"target_url", "title", "active", "updated_at"}),
|
||||
}).Create(&link).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot save shortlink", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
var saved models.ShortLink
|
||||
if err := s.DB.Where("code = ?", code).First(&saved).Error; err != nil {
|
||||
saved = link
|
||||
}
|
||||
scheme := getScheme(c)
|
||||
host := c.Request.Host
|
||||
shortURL := fmt.Sprintf("%s://%s/s/%s", scheme, host, code)
|
||||
c.JSON(http.StatusOK, gin.H{"id": saved.ID, "code": code, "short_url": shortURL, "link": saved})
|
||||
}
|
||||
|
||||
func NewShortLinkController(db *gorm.DB) *ShortLinkController {
|
||||
@@ -125,7 +127,9 @@ func hashIPShort(ip string) string {
|
||||
}
|
||||
|
||||
func codeFromHash(s string, n int) string {
|
||||
if n <= 0 { n = 7 }
|
||||
if n <= 0 {
|
||||
n = 7
|
||||
}
|
||||
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
out := make([]byte, n)
|
||||
@@ -137,20 +141,24 @@ func codeFromHash(s string, n int) string {
|
||||
}
|
||||
|
||||
func sanitizeCode(in string) string {
|
||||
s := strings.TrimSpace(in)
|
||||
if s == "" { return "" }
|
||||
// filter allowed runes
|
||||
rb := make([]rune, 0, len(s))
|
||||
for _, ch := range s {
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' {
|
||||
rb = append(rb, ch)
|
||||
}
|
||||
}
|
||||
if len(rb) == 0 { return "" }
|
||||
if len(rb) > 16 {
|
||||
rb = rb[:16]
|
||||
}
|
||||
return string(rb)
|
||||
s := strings.TrimSpace(in)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
// filter allowed runes
|
||||
rb := make([]rune, 0, len(s))
|
||||
for _, ch := range s {
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' {
|
||||
rb = append(rb, ch)
|
||||
}
|
||||
}
|
||||
if len(rb) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(rb) > 16 {
|
||||
rb = rb[:16]
|
||||
}
|
||||
return string(rb)
|
||||
}
|
||||
|
||||
func getScheme(c *gin.Context) string {
|
||||
@@ -174,11 +182,20 @@ func parseTarget(raw string) (string, error) {
|
||||
raw = string(dec)
|
||||
}
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
return "", errors.New("invalid url")
|
||||
// Try as-is first
|
||||
if u, err := url.Parse(raw); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" {
|
||||
return u.String(), nil
|
||||
}
|
||||
return u.String(), nil
|
||||
// If scheme is missing, try https:// fallback, then http://
|
||||
if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
|
||||
if u, err := url.Parse("https://" + raw); err == nil && u.Host != "" {
|
||||
return u.String(), nil
|
||||
}
|
||||
if u, err := url.Parse("http://" + raw); err == nil && u.Host != "" {
|
||||
return u.String(), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("invalid url")
|
||||
}
|
||||
|
||||
func (s *ShortLinkController) RedirectShort(c *gin.Context) {
|
||||
@@ -274,23 +291,25 @@ func (s *ShortLinkController) CreateShortLink(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
code := sanitizeCode(strings.TrimSpace(body.Code))
|
||||
if code == "" {
|
||||
for i := 0; i < 5; i++ {
|
||||
cnd, _ := randCode(7)
|
||||
var cnt int64
|
||||
s.DB.Model(&models.ShortLink{}).Where("code = ?", cnd).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
code = cnd
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if code == "" {
|
||||
for i := 0; i < 5; i++ {
|
||||
cnd, _ := randCode(7)
|
||||
var cnt int64
|
||||
s.DB.Model(&models.ShortLink{}).Where("code = ?", cnd).Count(&cnt)
|
||||
if cnt == 0 {
|
||||
code = cnd
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if code == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot generate code"})
|
||||
return
|
||||
}
|
||||
active := true
|
||||
if body.Active != nil { active = *body.Active }
|
||||
if body.Active != nil {
|
||||
active = *body.Active
|
||||
}
|
||||
link := models.ShortLink{
|
||||
Code: code,
|
||||
TargetURL: target,
|
||||
@@ -329,22 +348,37 @@ func (s *ShortLinkController) ListShortLinks(c *gin.Context) {
|
||||
|
||||
func (s *ShortLinkController) GetShortLinkStats(c *gin.Context) {
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing id"}); return }
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing id"})
|
||||
return
|
||||
}
|
||||
var link models.ShortLink
|
||||
if err := s.DB.First(&link, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}); return }
|
||||
start := time.Now().AddDate(0,0,-30)
|
||||
type Row struct{ Date string `json:"date"`; Count int64 `json:"count"` }
|
||||
if err := s.DB.First(&link, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
start := time.Now().AddDate(0, 0, -30)
|
||||
type Row struct {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var rows []Row
|
||||
s.DB.Model(&models.LinkClick{}).
|
||||
Select("DATE(created_at) as date, COUNT(*) as count").
|
||||
Where("short_link_id = ? AND created_at >= ?", link.ID, start).
|
||||
Group("DATE(created_at)").Order("date ASC").Scan(&rows)
|
||||
var refRows []struct{ Referrer string; Count int64 }
|
||||
var refRows []struct {
|
||||
Referrer string
|
||||
Count int64
|
||||
}
|
||||
s.DB.Model(&models.LinkClick{}).
|
||||
Select("referrer, COUNT(*) as count").
|
||||
Where("short_link_id = ? AND created_at >= ?", link.ID, start).
|
||||
Group("referrer").Order("count DESC").Limit(20).Scan(&refRows)
|
||||
var utmRows []struct{ Source, Medium, Campaign string; Count int64 }
|
||||
var utmRows []struct {
|
||||
Source, Medium, Campaign string
|
||||
Count int64
|
||||
}
|
||||
s.DB.Model(&models.LinkClick{}).
|
||||
Select("utm_source as source, utm_medium as medium, utm_campaign as campaign, COUNT(*) as count").
|
||||
Where("short_link_id = ? AND created_at >= ?", link.ID, start).
|
||||
|
||||
Reference in New Issue
Block a user