I am batman
This commit is contained in:
396
internal/handlers/authHandler.go
Normal file
396
internal/handlers/authHandler.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user