Files
Reduce/backend/handlers.go
Arkaprabha Chakraborty 7c210d9581 fix: link authentication
2026-02-13 00:50:24 +05:30

302 lines
8.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"math/rand"
"net/http"
"os"
"strconv"
"time"
"github.com/jinzhu/gorm"
"github.com/labstack/echo"
"golang.org/x/crypto/bcrypt"
)
func codegen(length int) string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
func codeLength() int {
if v := os.Getenv("CODE_LENGTH"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
}
return 6
}
func uniqueCode() string {
for {
code := codegen(codeLength())
var count int
db.Model(&Link{}).Where("code = ?", code).Count(&count)
if count == 0 {
return code
}
}
}
// shortenURL creates a new shortened link
func shortenURL(c echo.Context) error {
type Req struct {
LongURL string `json:"lurl"`
BaseURL string `json:"base_url"`
Code string `json:"code"`
RequiresAuth bool `json:"requires_auth"`
}
r := new(Req)
if err := c.Bind(r); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
if r.LongURL == "" {
return echo.NewHTTPError(http.StatusBadRequest, "URL is required")
}
if r.BaseURL == "" {
r.BaseURL = os.Getenv("BASE_URL")
if r.BaseURL == "" {
return echo.NewHTTPError(http.StatusInternalServerError, "Base URL not configured")
}
}
// Authenticated user
var userID *uint
if uid, ok := c.Get("user_id").(uint); ok {
userID = &uid
}
if r.RequiresAuth && userID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Login required to create protected links")
}
// Determine short code
isCustom := false
code := r.Code
if code != "" {
isCustom = true
if len(code) < 2 || len(code) > 32 {
return echo.NewHTTPError(http.StatusBadRequest, "Custom code must be 232 characters")
}
var existing Link
if db.Where("code = ?", code).First(&existing).Error == nil {
return echo.NewHTTPError(http.StatusConflict, "Short code already taken")
}
} else {
code = uniqueCode()
}
link := Link{
UserID: userID,
Code: code,
LongURL: r.LongURL,
IsCustom: isCustom,
RequiresAuth: r.RequiresAuth,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Protected links use the creator's account credentials
if r.RequiresAuth {
var user User
if err := db.First(&user, *userID).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load user")
}
link.AccessUsername = user.Username
link.AccessPassword = user.Password // Already hashed
}
if err := db.Create(&link).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create link")
}
return c.JSON(http.StatusCreated, map[string]interface{}{
"surl": r.BaseURL + "/" + code,
"id": link.ID,
"code": code,
"long_url": link.LongURL,
"is_custom": link.IsCustom,
"requires_auth": link.RequiresAuth,
})
}
// fetchLURL resolves a short code to a long URL
func fetchLURL(c echo.Context) error {
code := c.Param("code")
var link Link
if err := db.Where("code = ?", code).First(&link).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "Link not found")
}
if link.RequiresAuth {
// Check if user is authenticated and is the link owner
if userID, ok := c.Get("user_id").(uint); ok {
if link.UserID != nil && *link.UserID == userID {
// Auto-authorize link owner
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
return c.JSON(http.StatusOK, map[string]interface{}{
"lurl": link.LongURL,
"requires_auth": false,
"auto_authorized": true,
})
}
}
// User not logged in or not the owner - require manual auth
return c.JSON(http.StatusOK, map[string]interface{}{
"requires_auth": true,
"code": link.Code,
})
}
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
return c.JSON(http.StatusOK, map[string]interface{}{
"lurl": link.LongURL,
"requires_auth": false,
})
}
// verifyAndRedirect checks credentials for auth-protected links
func verifyAndRedirect(c echo.Context) error {
code := c.Param("code")
type Req struct {
Username string `json:"username"`
Password string `json:"password"`
}
r := new(Req)
if err := c.Bind(r); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
var link Link
if err := db.Where("code = ?", code).First(&link).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "Link not found")
}
if !link.RequiresAuth {
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
return c.JSON(http.StatusOK, map[string]string{"lurl": link.LongURL})
}
if r.Username != link.AccessUsername {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
// Verify against the link owner's account password
var user User
if db.Where("username = ?", link.AccessUsername).First(&user).Error != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to verify credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.Password)); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
return c.JSON(http.StatusOK, map[string]string{"lurl": link.LongURL})
}
// --- Dashboard handlers (authenticated) ---
func listLinks(c echo.Context) error {
uid := c.Get("user_id").(uint)
var links []Link
if err := db.Where("user_id = ?", uid).Order("created_at desc").Find(&links).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch links")
}
return c.JSON(http.StatusOK, links)
}
func updateLink(c echo.Context) error {
uid := c.Get("user_id").(uint)
id := c.Param("id")
var link Link
if err := db.First(&link, id).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "Link not found")
}
if link.UserID == nil || *link.UserID != uid {
return echo.NewHTTPError(http.StatusForbidden, "Access denied")
}
type Req struct {
Code *string `json:"code"`
LongURL *string `json:"long_url"`
RequiresAuth *bool `json:"requires_auth"`
}
r := new(Req)
if err := c.Bind(r); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
if r.Code != nil {
if len(*r.Code) < 2 || len(*r.Code) > 32 {
return echo.NewHTTPError(http.StatusBadRequest, "Code must be 232 characters")
}
var dup Link
if db.Where("code = ? AND id != ?", *r.Code, link.ID).First(&dup).Error == nil {
return echo.NewHTTPError(http.StatusConflict, "Short code already taken")
}
link.Code = *r.Code
link.IsCustom = true
}
if r.LongURL != nil {
if *r.LongURL == "" {
return echo.NewHTTPError(http.StatusBadRequest, "URL cannot be empty")
}
link.LongURL = *r.LongURL
}
if r.RequiresAuth != nil {
if *r.RequiresAuth {
// Enable protection with owner's credentials
var user User
if err := db.First(&user, uid).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load user")
}
link.RequiresAuth = true
link.AccessUsername = user.Username
link.AccessPassword = user.Password // Already hashed
} else {
// Disable protection
link.RequiresAuth = false
link.AccessUsername = ""
link.AccessPassword = ""
}
}
link.UpdatedAt = time.Now()
if err := db.Save(&link).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update link")
}
return c.JSON(http.StatusOK, link)
}
func deleteLink(c echo.Context) error {
uid := c.Get("user_id").(uint)
id := c.Param("id")
var link Link
if err := db.First(&link, id).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "Link not found")
}
if link.UserID == nil || *link.UserID != uid {
return echo.NewHTTPError(http.StatusForbidden, "Access denied")
}
if err := db.Delete(&link).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete link")
}
return c.JSON(http.StatusOK, map[string]string{"message": "Link deleted"})
}