I am batman

This commit is contained in:
2026-07-12 20:26:16 -04:00
commit 5a3a6357cf
37 changed files with 2938 additions and 0 deletions

View File

@@ -0,0 +1,396 @@
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"OpsMastery.v5/internal/database"
"OpsMastery.v5/internal/models"
"OpsMastery.v5/internal/utils"
"github.com/gofiber/adaptor/v2"
"github.com/gofiber/fiber/v2"
"github.com/markbates/goth/gothic"
"golang.org/x/crypto/bcrypt"
)
func SignUp(c *fiber.Ctx) error {
var user models.User
var input struct {
Email string `json:"email"`
Password string `json:"password"`
Name string `json:"name"`
Role string `json:"role"`
Username string `json:"username"`
}
if err := c.BodyParser(&input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
user.Email = strings.ToLower(input.Email)
user.Password = input.Password
user.Name = strings.ToLower(input.Name)
user.Role = input.Role
user.Username = strings.ToLower(input.Username)
user.Active = false
user.VerificationToken = utils.GenerateRandomToken()
storedHash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to hash password"})
}
user.Password = string(storedHash)
log.Printf("Hashed password for user %s: %s\n", user.Email, user.Password)
var existingUser models.User
if err := database.DB().Where("email = ? OR username = ?", user.Email, user.Username).First(&existingUser).Error; err == nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Email or username already exists"})
}
if err := database.DB().Create(&user).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if err := utils.SendVerificationEmail(user.Email, user.VerificationToken); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send verification email"})
}
return c.Status(http.StatusCreated).JSON(fiber.Map{
"message": "Registration successful. Please check your email to verify your account.",
})
}
func SignIn(c *fiber.Ctx) error {
var userInput struct {
EmailOrUsername string `json:"emailOrUsername"`
Password string `json:"password"`
}
if err := c.BodyParser(&userInput); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
}
// Force to lowercase for case-insensitive match
normalizedInput := strings.ToLower(userInput.EmailOrUsername)
var user models.User
if err := database.DB().Where("LOWER(email) = ? OR LOWER(username) = ?", normalizedInput, normalizedInput).First(&user).Error; err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid email/username or password"})
}
if !user.Active {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Please verify your email before signing in",
})
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userInput.Password)); err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid email/username or password"})
}
// Generate JWTs
accessToken, refreshToken, err := utils.GenerateJWT(user)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "Could not generate tokens"})
}
// Set Refresh Token in HttpOnly Cookie
c.Cookie(&fiber.Cookie{
Name: "refresh_token",
Value: refreshToken,
Expires: time.Now().Add(7 * 24 * time.Hour),
HTTPOnly: true,
Secure: false,
SameSite: "Strict",
Path: "/api/v1/auth/refresh", // restrict usage
})
// Return Access Token in response (frontend stores in memory)
return c.Status(http.StatusOK).JSON(fiber.Map{
"message": "Sign in successful",
"access_token": accessToken,
"user": fiber.Map{
"id": user.ID,
"email": user.Email,
"name": user.Name,
"role": user.Role,
"username": user.Username,
"address": user.Address,
"phoneNumber": user.PhoneNumber,
"active": user.Active,
},
})
}
func RefreshToken(c *fiber.Ctx) error {
refreshToken := c.Cookies("refresh_token")
if refreshToken == "" {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "Refresh token not found"})
}
claims, err := utils.ValidateJWT(refreshToken, true)
if err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid refresh token"})
}
// Get user from claims
var user models.User
if err := database.DB().First(&user, claims["sub"]).Error; err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "User not found"})
}
// Generate new tokens
accessToken, newRefreshToken, err := utils.GenerateJWT(user)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "Could not generate tokens"})
}
// Rotate refresh token
c.Cookie(&fiber.Cookie{
Name: "refresh_token",
Value: newRefreshToken,
Expires: time.Now().Add(7 * 24 * time.Hour),
HTTPOnly: true,
Secure: false,
SameSite: "Strict",
Path: "/api/auth/refresh",
})
// Return new Access Token
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"access_token": accessToken,
})
}
func SignOut(c *fiber.Ctx) error {
// Invalidate the refresh token cookie
c.Cookie(&fiber.Cookie{
Name: "refresh_token",
Value: "",
Expires: time.Now().Add(-1 * time.Hour),
HTTPOnly: true,
Secure: false,
SameSite: "Strict",
Path: "/api/auth/refresh",
})
return c.Status(http.StatusOK).JSON(fiber.Map{"message": "Successfully signed out"})
}
// func VerifyEmail(c *fiber.Ctx) error {
// token := c.Params("token")
// var user models.User
// if err := database.DB().Where("verification_token = ?", token).First(&user).Error; err != nil {
// return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
// "error": "Invalid verification token",
// })
// }
// user.Active = true
// user.VerificationToken = "" // Clear the token after verification
// if err := database.DB().Save(&user).Error; err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to verify email",
// })
// }
// return c.Status(fiber.StatusOK).JSON(fiber.Map{
// "message": "Email verified successfully",
// })
// }
func VerifyEmail(c *fiber.Ctx) error {
var body struct {
Token string `json:"token"`
}
if err := c.BodyParser(&body); err != nil || body.Token == "" {
return c.Redirect("http://localhost:3000/auth/verification-failed", fiber.StatusSeeOther)
}
var user models.User
if err := database.DB().Where("verification_token = ?", body.Token).First(&user).Error; err != nil {
return c.Redirect("http://localhost:3000/auth/verification-failed", fiber.StatusSeeOther)
}
user.Active = true
user.VerificationToken = ""
if err := database.DB().Save(&user).Error; err != nil {
return c.Redirect("http://localhost:3000/auth/verification-failed", fiber.StatusSeeOther)
}
return c.Redirect("http://localhost:3000/auth/verification-success", fiber.StatusSeeOther)
}
func RequestPasswordReset(c *fiber.Ctx) error {
var input struct {
Email string `json:"email"`
}
if err := c.BodyParser(&input); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
}
var user models.User
if err := database.DB().Where("email = ?", input.Email).First(&user).Error; err != nil {
// Don't reveal if email exists or not
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"message": "If your email is registered, you will receive a password reset link",
})
}
resetToken := utils.GenerateRandomToken()
user.ResetToken = resetToken
user.ResetTokenExpiry = time.Now().Add(1 * time.Hour)
// Log the token being set
log.Printf("Setting reset token for user %s: %s", user.Email, resetToken)
if err := database.DB().Save(&user).Error; err != nil {
log.Printf("Error saving user with reset token: %v", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to process password reset",
})
}
// Add this log right before SendPasswordResetEmail
log.Printf("About to send reset email with token: %s", resetToken)
if err := utils.SendPasswordResetEmail(user.Email, resetToken); err != nil {
log.Printf("Error sending reset email: %v", err)
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to send reset email",
})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"message": "If your email is registered, you will receive a password reset link",
})
}
func ResetPassword(c *fiber.Ctx) error {
var body struct {
ResetToken string `json:"reset_token"`
NewPassword string `json:"new_password"`
}
// Log the raw request body
rawBody := string(c.Body())
log.Printf("Received reset password request body: %s", rawBody)
if err := c.BodyParser(&body); err != nil {
log.Printf("Error parsing request body: %v", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
log.Printf("Reset token: %s", body.ResetToken)
// Validate the token
var user models.User
if err := database.DB().Where("reset_token = ?", body.ResetToken).First(&user).Error; err != nil {
log.Printf("No user found with reset token: %s, error: %v", body.ResetToken, err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid reset token",
})
}
// Check if token has expired
if user.ResetTokenExpiry.Before(time.Now()) {
log.Printf("Token expired. Expiry: %v, Current time: %v", user.ResetTokenExpiry, time.Now())
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Reset token has expired",
})
}
// Hash the new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(body.NewPassword), bcrypt.DefaultCost)
if err != nil {
log.Printf("Error hashing password: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to hash password",
})
}
// Update user's password and clear reset token
user.Password = string(hashedPassword)
user.ResetToken = ""
user.ResetTokenExpiry = time.Time{}
if err := database.DB().Save(&user).Error; err != nil {
log.Printf("Error saving user: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to update password",
})
}
log.Printf("Password successfully reset for user: %s", user.Email)
return c.JSON(fiber.Map{
"message": "Password successfully reset",
})
}
func validateResetToken(token string) (bool, error) {
var user models.User
if err := database.DB().Where("reset_token = ?", token).First(&user).Error; err != nil {
log.Printf("No user found with reset token: %s, error: %v", token, err)
return false, err
}
// Check if token has expired
if user.ResetTokenExpiry.Before(time.Now()) {
log.Printf("Token expired. Expiry: %v, Current time: %v", user.ResetTokenExpiry, time.Now())
return false, fmt.Errorf("reset token has expired")
}
log.Printf("Token validated successfully for user: %s", user.Email)
return true, nil
}
// Generic OAuth login handler
func OAuthLogin(c *fiber.Ctx) error {
provider := c.Params("provider")
// Set provider in query for gothic
req := c.Request()
uri := req.URI()
uri.SetQueryString("provider=" + provider)
return adaptor.HTTPHandlerFunc(gothic.BeginAuthHandler)(c)
}
func OAuthCallback(c *fiber.Ctx) error {
provider := c.Params("provider")
req := c.Request()
req.URI().SetQueryString("provider=" + provider)
return adaptor.HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// Check if user with this email already exists
var existingUser models.User
email := strings.ToLower(user.Email)
if err := database.DB().Where("LOWER(email) = ?", email).First(&existingUser).Error; err == nil {
// User exists, do not allow OAuth sign up
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"error": "This email already exists, please sign in.",
})
return
}
// ...proceed with creating new user from OAuth profile...
// (your existing logic here)
json.NewEncoder(w).Encode(user)
})(c)
}