This commit is contained in:
2026-03-20 12:33:37 +01:00
commit ce3925423f
44 changed files with 3143 additions and 0 deletions

5
internal/auth/errors.go Normal file
View File

@@ -0,0 +1,5 @@
package auth
import "errors"
var ErrInvalidCredentials = errors.New("invalid credentials")

63
internal/auth/handler.go Normal file
View File

@@ -0,0 +1,63 @@
package auth
import (
"os"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(s *Service) *Handler {
return &Handler{service: s}
}
func (h *Handler) Me(c *gin.Context) {
userID, _ := c.Get("user_id")
role, _ := c.Get("role")
c.JSON(200, gin.H{
"user_id": userID,
"role": role,
})
}
func (h *Handler) AdminCheck(c *gin.Context) {
c.JSON(200, gin.H{
"message": "you are an admin",
})
}
func (h *Handler) Login(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid request body"})
return
}
token, err := h.service.Login(req.Username, req.Password)
if err != nil {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
isSecure := os.Getenv("USE_HTTPS") == "true"
c.SetCookie(
"auth_token",
token,
3600*24,
"/",
os.Getenv("DOMAIN"),
isSecure,
true, // httpOnly (IMPORTANT)
)
c.JSON(200, gin.H{"token": token})
}

31
internal/auth/jwt.go Normal file
View File

@@ -0,0 +1,31 @@
package auth
import (
"os"
"time"
"github.com/golang-jwt/jwt/v4"
)
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
type Claims struct {
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateJWT(username string, role string) (string, error) {
claims := Claims{
Username: username,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // 24h expiration
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}

View File

@@ -0,0 +1,27 @@
package auth
import (
"ResendIt/internal/user"
"errors"
"gorm.io/gorm"
)
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db}
}
func (r *Repository) FindByUsername(username string) (*user.User, error) {
var u user.User
if err := r.db.Where("username = ?", username).First(&u).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, user.ErrUserNotFound
}
return nil, err
}
return &u, nil
}

23
internal/auth/routes.go Normal file
View File

@@ -0,0 +1,23 @@
package auth
import (
"ResendIt/internal/api/middleware"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(r *gin.RouterGroup, h *Handler) {
auth := r.Group("/auth")
auth.POST("/login", h.Login)
protected := auth.Group("/")
protected.Use(middleware.AuthMiddleware())
protected.GET("/me", h.Me)
admin := protected.Group("/")
admin.Use(middleware.RequireRole("admin"))
admin.GET("/admin-check", h.AdminCheck)
}

32
internal/auth/service.go Normal file
View File

@@ -0,0 +1,32 @@
package auth
import (
"ResendIt/internal/security"
"ResendIt/internal/user"
"errors"
)
type Service struct {
repo *Repository
}
func NewService(r *Repository) *Service {
return &Service{repo: r}
}
func (s *Service) Login(username, password string) (string, error) {
u, err := s.repo.FindByUsername(username)
if errors.Is(err, user.ErrUserNotFound) {
// Prevent user enumeration by returning a generic error message
return "", ErrInvalidCredentials
} else if err != nil {
return "", err
}
if !security.CheckPassword(password, u.PasswordHash) {
return "", ErrInvalidCredentials
}
return GenerateJWT(u.Username, u.Role)
}