package handlers import ( "fmt" "log" "net/http" "time" "gitea.tecamino.com/paadi/access-handler/internal/utils" "gitea.tecamino.com/paadi/access-handler/models" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // ----------------------------- // 🔐 AUTHENTICATION CONSTANTS // ----------------------------- // JWT secrets (replace "*" with strong random values in production!) var ACCESS_SECRET = []byte("ShFRprALcXjlosJ2hFCnGYGG3Ce2uRx6") var REFRESH_SECRET = []byte("pQIjuX6g6Tzf0FEfdScxttT3hlL9NFaa") // DOMAIN defines where cookies are valid. Change this in production. var DOMAIN = "localhost" // ACCESS_TOKEN_TIME defines how long access tokens are valid. var ACCESS_TOKEN_TIME = 15 * time.Minute // REFRESH_TOKEN_TIME defines how long refresh tokens are valid. var REFRESH_TOKEN_TIME = 72 * time.Hour // ----------------------------- // 🧠 HANDLERS // ----------------------------- // Login authenticates a user and returns JWT tokens (access + refresh). func (aH *AccessHandler) Login(c *gin.Context) { var user models.User aH.logger.Debug("Login", "bind JSON request") if err := c.BindJSON(&user); err != nil { aH.logger.Error("Login", err) c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) return } // Validate input if !user.IsValid() { aH.logger.Error("Login", "user empty") c.JSON(http.StatusBadRequest, models.NewJsonMessageResponse("user empty")) return } // Fetch user record from DB var dbRecord []models.User err := aH.dbHandler.GetByKey(&dbRecord, "user_name", user.Name, false) if err != nil { aH.logger.Error("Login", err) c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) return } if len(dbRecord) > 1 { log.Println("multiple users found") aH.logger.Error("Login", "more than one record found") c.JSON(http.StatusInternalServerError, models.NewJsonMessageResponse("internal error")) return } // Check password if !utils.CheckPassword(user.Password, dbRecord[0].Password) { aH.logger.Error("Login", "invalid password") c.JSON(http.StatusUnauthorized, models.NewJsonMessageResponse("invalid credentials")) return } user = dbRecord[0] // ----------------------------- // 🔑 TOKEN CREATION // ----------------------------- aH.logger.Debug("Login", "create tokens") accessTokenExp := time.Now().Add(ACCESS_TOKEN_TIME) refreshTokenExp := time.Now().Add(REFRESH_TOKEN_TIME) // Create access token accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "id": user.Id, "username": user.Name, "role": user.Role, "type": "access", "exp": accessTokenExp.Unix(), }) // Create refresh token refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "id": user.Id, "username": user.Name, "role": user.Role, "type": "refresh", "exp": refreshTokenExp.Unix(), }) // Sign tokens accessString, err := accessToken.SignedString(ACCESS_SECRET) if err != nil { aH.logger.Error("Login", "failed to sign access token") c.JSON(http.StatusInternalServerError, models.NewJsonMessageResponse("could not create access token")) return } refreshString, err := refreshToken.SignedString(REFRESH_SECRET) if err != nil { aH.logger.Error("Login", "failed to sign refresh token") c.JSON(http.StatusInternalServerError, models.NewJsonMessageResponse("could not create refresh token")) return } // ----------------------------- // 🍪 SET COOKIES // ----------------------------- aH.logger.Debug("Login", "set cookies") secure := gin.Mode() == gin.ReleaseMode c.SetCookie("access_token", accessString, int(time.Until(accessTokenExp).Seconds()), "/", "", secure, true) c.SetCookie("refresh_token", refreshString, int(time.Until(refreshTokenExp).Seconds()), "/", "", secure, true) aH.logger.Info("Login", "user "+user.Name+" logged in successfully") c.JSON(http.StatusOK, gin.H{ "message": "login successful", "id": user.Id, "user": user.Name, "role": user.Role, "settings": user.Settings, }) } // Refresh generates a new access token from a valid refresh token. func (aH *AccessHandler) Refresh(c *gin.Context) { aH.logger.Debug("Refresh", "get refresh cookie") refreshCookie, err := c.Cookie("refresh_token") if err != nil { aH.logger.Error("Refresh", "no refresh token") c.JSON(http.StatusUnauthorized, gin.H{"message": "no refresh token"}) return } // Validate token aH.logger.Debug("Refresh", "parse token") token, err := jwt.Parse(refreshCookie, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return REFRESH_SECRET, nil }) if err != nil || !token.Valid { aH.logger.Error("Refresh", err) c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid refresh token"}) return } // Extract claims claims, ok := token.Claims.(jwt.MapClaims) if !ok || claims["type"] != "refresh" { aH.logger.Error("Refresh", "invalid token type") c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid token type"}) return } username := claims["username"].(string) id := int(claims["id"].(float64)) role := claims["role"].(string) // Create new access token aH.logger.Debug("Refresh", "create new access token") accessExp := time.Now().Add(ACCESS_TOKEN_TIME) newAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "id": id, "username": username, "role": role, "exp": accessExp.Unix(), }) accessString, _ := newAccess.SignedString(ACCESS_SECRET) // Set new access cookie aH.logger.Debug("Refresh", "set new access cookie") c.SetCookie("access_token", accessString, int(time.Until(accessExp).Seconds()), "/", DOMAIN, gin.Mode() == gin.ReleaseMode, true) c.JSON(http.StatusOK, gin.H{"message": "token refreshed"}) } // Me returns information about the currently authenticated user. func (aH *AccessHandler) Me(c *gin.Context) { aH.logger.Debug("Me", "get access cookie") cookie, err := c.Cookie("access_token") if err != nil { aH.logger.Error("Me", err) c.JSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) return } // Parse and validate access token aH.logger.Debug("Me", "parse token") token, err := jwt.Parse(cookie, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return ACCESS_SECRET, nil }) if err != nil || !token.Valid { aH.logger.Error("Me", err) c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) return } claims := token.Claims.(jwt.MapClaims) c.JSON(http.StatusOK, gin.H{ "id": claims["id"], "user": claims["username"], "role": claims["role"], }) } // Logout clears authentication cookies and ends the session. func (aH *AccessHandler) Logout(c *gin.Context) { aH.logger.Info("Logout", "logout user") secure := gin.Mode() == gin.ReleaseMode aH.logger.Debug("Logout", fmt.Sprintf("domain=%s secure=%t", DOMAIN, secure)) // Clear cookies c.SetCookie("access_token", "", -1, "/", DOMAIN, secure, true) c.SetCookie("refresh_token", "", -1, "/", DOMAIN, secure, true) c.JSON(http.StatusOK, gin.H{"message": "logged out"}) }