mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
dev day #79
This commit is contained in:
@@ -18,12 +18,79 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type ShortLinkController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// PublicCreateShortLink creates (or upserts) a short link for a given target URL.
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &ShortLinkController{DB: db}
|
||||
}
|
||||
@@ -57,6 +124,18 @@ func hashIPShort(ip string) string {
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func codeFromHash(s string, n int) string {
|
||||
if n <= 0 { n = 7 }
|
||||
alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
out := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
b := sum[i%len(sum)]
|
||||
out[i] = alphabet[int(b)%len(alphabet)]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func getScheme(c *gin.Context) string {
|
||||
if p := c.GetHeader("X-Forwarded-Proto"); p != "" {
|
||||
return p
|
||||
@@ -204,14 +283,25 @@ func (s *ShortLinkController) CreateShortLink(c *gin.Context) {
|
||||
Active: active,
|
||||
ExpiresAt: body.ExpiresAt,
|
||||
}
|
||||
if err := s.DB.Create(&link).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot save"})
|
||||
// Upsert on code to avoid duplicate errors and keep link stable across regenerations
|
||||
if err := s.DB.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "code"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"target_url", "title", "source_type", "source_id", "active", "expires_at", "updated_at"}),
|
||||
}).Create(&link).Error; err != nil {
|
||||
// Return database error message for easier debugging (non-sensitive)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot save shortlink", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
// Ensure we return the saved record (ID will be empty on update path)
|
||||
var saved models.ShortLink
|
||||
if err := s.DB.Where("code = ?", code).First(&saved).Error; err != nil {
|
||||
// Fallback to in-memory link if fetch fails
|
||||
saved = link
|
||||
}
|
||||
scheme := getScheme(c)
|
||||
host := c.Request.Host
|
||||
shortURL := fmt.Sprintf("%s://%s/s/%s", scheme, host, link.Code)
|
||||
c.JSON(http.StatusOK, gin.H{"id": link.ID, "code": link.Code, "short_url": shortURL, "link": link})
|
||||
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 (s *ShortLinkController) ListShortLinks(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user