397 lines
12 KiB
Go
397 lines
12 KiB
Go
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)
|
|
}
|