feat: version 2.1

This commit is contained in:
Arkaprabha Chakraborty
2026-02-13 00:01:00 +05:30
parent 005838045a
commit 2a46aa7d79
13 changed files with 548 additions and 63 deletions

View File

@@ -56,11 +56,21 @@ func parseToken(tokenStr string) (*JWTClaims, error) {
// JWTMiddleware requires a valid JWT token
func JWTMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
auth := c.Request().Header.Get("Authorization")
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing or invalid token")
// Try to get token from cookie first
cookie, err := c.Cookie("token")
var tokenStr string
if err == nil && cookie != nil {
tokenStr = cookie.Value
} else {
// Fallback to Authorization header for backward compatibility
auth := c.Request().Header.Get("Authorization")
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing or invalid token")
}
tokenStr = strings.TrimPrefix(auth, "Bearer ")
}
claims, err := parseToken(strings.TrimPrefix(auth, "Bearer "))
claims, err := parseToken(tokenStr)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired token")
}
@@ -73,9 +83,21 @@ func JWTMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
// OptionalJWTMiddleware extracts user if token present, doesn't require it
func OptionalJWTMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
auth := c.Request().Header.Get("Authorization")
if auth != "" && strings.HasPrefix(auth, "Bearer ") {
if claims, err := parseToken(strings.TrimPrefix(auth, "Bearer ")); err == nil {
// Try to get token from cookie first
cookie, err := c.Cookie("token")
var tokenStr string
if err == nil && cookie != nil {
tokenStr = cookie.Value
} else {
// Fallback to Authorization header
auth := c.Request().Header.Get("Authorization")
if auth != "" && strings.HasPrefix(auth, "Bearer ") {
tokenStr = strings.TrimPrefix(auth, "Bearer ")
}
}
if tokenStr != "" {
if claims, err := parseToken(tokenStr); err == nil {
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
}
@@ -122,9 +144,20 @@ func register(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}
// Set httpOnly cookie
cookie := &http.Cookie{
Name: "token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: os.Getenv("ENV") == "production", // Only secure in production
SameSite: http.SameSiteLaxMode,
MaxAge: 72 * 3600, // 72 hours
}
c.SetCookie(cookie)
return c.JSON(http.StatusCreated, map[string]interface{}{
"token": token,
"user": map[string]interface{}{"id": user.ID, "username": user.Username},
"user": map[string]interface{}{"id": user.ID, "username": user.Username},
})
}
@@ -152,9 +185,36 @@ func login(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}
// Set httpOnly cookie
cookie := &http.Cookie{
Name: "token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: os.Getenv("ENV") == "production", // Only secure in production
SameSite: http.SameSiteLaxMode,
MaxAge: 72 * 3600, // 72 hours
}
c.SetCookie(cookie)
return c.JSON(http.StatusOK, map[string]interface{}{
"token": token,
"user": map[string]interface{}{"id": user.ID, "username": user.Username},
"user": map[string]interface{}{"id": user.ID, "username": user.Username},
})
}
func logout(c echo.Context) error {
// Clear the cookie
cookie := &http.Cookie{
Name: "token",
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1, // Delete cookie
}
c.SetCookie(cookie)
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Logged out successfully",
})
}
@@ -169,3 +229,117 @@ func getMe(c echo.Context) error {
"username": user.Username,
})
}
func updateUsername(c echo.Context) error {
uid := c.Get("user_id").(uint)
type Req struct {
Username string `json:"username"`
}
r := new(Req)
if err := c.Bind(r); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
r.Username = strings.TrimSpace(r.Username)
if len(r.Username) < 3 || len(r.Username) > 32 {
return echo.NewHTTPError(http.StatusBadRequest, "Username must be 332 characters")
}
// Check if username is already taken by another user
var existing User
if db.Where("username = ? AND id != ?", r.Username, uid).First(&existing).Error == nil {
return echo.NewHTTPError(http.StatusConflict, "Username already taken")
}
var user User
if err := db.First(&user, uid).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
user.Username = r.Username
if err := db.Save(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update username")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"id": user.ID,
"username": user.Username,
})
}
func updatePassword(c echo.Context) error {
uid := c.Get("user_id").(uint)
type Req struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
r := new(Req)
if err := c.Bind(r); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
if len(r.NewPassword) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Password must be at least 6 characters")
}
var user User
if err := db.First(&user, uid).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Verify current password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.CurrentPassword)); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Current password is incorrect")
}
// Hash new password
hash, err := bcrypt.GenerateFromPassword([]byte(r.NewPassword), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to process password")
}
user.Password = string(hash)
if err := db.Save(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update password")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Password updated successfully",
})
}
func deleteAccount(c echo.Context) error {
uid := c.Get("user_id").(uint)
var user User
if err := db.First(&user, uid).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Delete all links owned by this user
db.Where("user_id = ?", uid).Delete(&Link{})
// Delete user
if err := db.Delete(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete account")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Account deleted successfully",
})
}
func getUserStats(c echo.Context) error {
uid := c.Get("user_id").(uint)
var linkCount int
var totalClicks int
db.Model(&Link{}).Where("user_id = ?", uid).Count(&linkCount)
db.Model(&Link{}).Where("user_id = ?", uid).Select("COALESCE(SUM(click_count), 0)").Row().Scan(&totalClicks)
return c.JSON(http.StatusOK, map[string]interface{}{
"link_count": linkCount,
"total_clicks": totalClicks,
})
}