2025-08-26 14:13:09 -06:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-27 13:21:11 -06:00
|
|
|
"database/sql"
|
2025-08-26 14:13:09 -06:00
|
|
|
"log"
|
|
|
|
|
"net/http"
|
2025-08-27 13:21:11 -06:00
|
|
|
"strconv"
|
2025-08-26 14:13:09 -06:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
|
|
|
|
|
"github.com/patel-mann/poll-system/app/internal/models"
|
|
|
|
|
"github.com/patel-mann/poll-system/app/internal/utils"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var jwtKey = []byte("your-secret-key") //TODO: Move to env/config
|
|
|
|
|
|
|
|
|
|
// Helper function to get redirect URL based on role
|
|
|
|
|
func getDefaultRedirectURL(role int) string {
|
|
|
|
|
switch role {
|
|
|
|
|
case 1: // Admin
|
|
|
|
|
return "/dashboard"
|
|
|
|
|
case 2: // Volunteer
|
|
|
|
|
return "/volunteer/dashboard"
|
|
|
|
|
case 3: // Volunteer
|
|
|
|
|
return "/volunteer/dashboard"
|
|
|
|
|
default:
|
|
|
|
|
return "/" // Fallback to login page
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to render error pages with consistent data
|
|
|
|
|
func renderLoginError(w http.ResponseWriter, errorMsg string) {
|
|
|
|
|
utils.Render(w, "login.html", map[string]interface{}{
|
|
|
|
|
"Error": errorMsg,
|
|
|
|
|
"Title": "Login",
|
|
|
|
|
"IsAuthenticated": false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func renderRegisterError(w http.ResponseWriter, errorMsg string) {
|
|
|
|
|
utils.Render(w, "register.html", map[string]interface{}{
|
|
|
|
|
"Error": errorMsg,
|
|
|
|
|
"Title": "Register",
|
|
|
|
|
"IsAuthenticated": false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to create and sign JWT token
|
|
|
|
|
func createJWTToken(userID, role int) (string, time.Time, error) {
|
|
|
|
|
expirationTime := time.Now().Add(12 * time.Hour)
|
|
|
|
|
claims := &models.Claims{
|
|
|
|
|
UserID: userID,
|
|
|
|
|
Role: role,
|
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
|
|
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
|
tokenString, err := token.SignedString(jwtKey)
|
|
|
|
|
return tokenString, expirationTime, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to set session cookie
|
|
|
|
|
func setSessionCookie(w http.ResponseWriter, tokenString string, expirationTime time.Time) {
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: "session",
|
|
|
|
|
Value: tokenString,
|
|
|
|
|
Path: "/",
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
Secure: false, // Set to true in production with HTTPS
|
|
|
|
|
SameSite: http.SameSiteStrictMode,
|
|
|
|
|
Expires: expirationTime,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to clear session cookie
|
|
|
|
|
func clearSessionCookie(w http.ResponseWriter) {
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: "session",
|
|
|
|
|
Value: "",
|
|
|
|
|
Path: "/",
|
|
|
|
|
MaxAge: -1,
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
Secure: false, // Set to true in production with HTTPS
|
|
|
|
|
SameSite: http.SameSiteStrictMode,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
email := r.FormValue("email")
|
|
|
|
|
password := r.FormValue("password")
|
|
|
|
|
|
|
|
|
|
// Input validation
|
|
|
|
|
if email == "" || password == "" {
|
2025-08-27 13:21:11 -06:00
|
|
|
http.Redirect(w, r, "/?error=EmailAndPasswordRequired", http.StatusSeeOther)
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get user from database
|
|
|
|
|
var storedHash string
|
|
|
|
|
var userID int
|
|
|
|
|
var role int
|
|
|
|
|
|
|
|
|
|
err := models.DB.QueryRow(`
|
|
|
|
|
SELECT user_id, password, role_id
|
|
|
|
|
FROM "users"
|
|
|
|
|
WHERE email = $1
|
|
|
|
|
`, email).Scan(&userID, &storedHash, &role)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Login failed for email %s: %v", email, err)
|
2025-08-27 13:21:11 -06:00
|
|
|
http.Redirect(w, r, "/?error=InvalidCredentials", http.StatusSeeOther)
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify password
|
|
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Password verification failed for user ID %d", userID)
|
2025-08-27 13:21:11 -06:00
|
|
|
http.Redirect(w, r, "/?error=InvalidCredentials", http.StatusSeeOther)
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create JWT token
|
|
|
|
|
tokenString, expirationTime, err := createJWTToken(userID, role)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("JWT token creation failed for user ID %d: %v", userID, err)
|
2025-08-27 13:21:11 -06:00
|
|
|
http.Redirect(w, r, "/?error=InternalError", http.StatusSeeOther)
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set session cookie
|
|
|
|
|
setSessionCookie(w, tokenString, expirationTime)
|
|
|
|
|
|
|
|
|
|
// Redirect based on user role
|
|
|
|
|
redirectURL := getDefaultRedirectURL(role)
|
|
|
|
|
log.Printf("User %d (role %d) logged in successfully, redirecting to %s", userID, role, redirectURL)
|
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
|
2025-08-26 14:13:09 -06:00
|
|
|
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
|
utils.Render(w, "register.html", map[string]interface{}{
|
|
|
|
|
"Title": "Register",
|
|
|
|
|
"IsAuthenticated": false,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
firstName := r.FormValue("first_name")
|
|
|
|
|
lastName := r.FormValue("last_name")
|
|
|
|
|
email := r.FormValue("email")
|
|
|
|
|
phone := r.FormValue("phone")
|
|
|
|
|
role := r.FormValue("role")
|
|
|
|
|
password := r.FormValue("password")
|
2025-08-27 13:21:11 -06:00
|
|
|
adminCode := r.FormValue("admin_code") // for volunteers
|
2025-08-26 14:13:09 -06:00
|
|
|
|
|
|
|
|
// Input validation
|
|
|
|
|
if firstName == "" || lastName == "" || email == "" || password == "" || role == "" {
|
|
|
|
|
renderRegisterError(w, "All fields are required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash password
|
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Password hashing failed: %v", err)
|
|
|
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
// Convert role to int
|
|
|
|
|
roleID, err := strconv.Atoi(role)
|
2025-08-26 14:13:09 -06:00
|
|
|
if err != nil {
|
2025-08-27 13:21:11 -06:00
|
|
|
renderRegisterError(w, "Invalid role")
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
var adminID int
|
|
|
|
|
if roleID == 3 { // volunteer
|
|
|
|
|
if adminCode == "" {
|
|
|
|
|
renderRegisterError(w, "Admin code is required for volunteers")
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-26 14:13:09 -06:00
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
// Check if admin exists
|
|
|
|
|
err = models.DB.QueryRow(`SELECT user_id FROM users WHERE role_id = 1 AND admin_code = $1`, adminCode).Scan(&adminID)
|
2025-08-26 14:13:09 -06:00
|
|
|
if err != nil {
|
2025-08-27 13:21:11 -06:00
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
renderRegisterError(w, "Invalid admin code")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
log.Printf("DB error checking admin code: %v", err)
|
|
|
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
2025-08-27 13:21:11 -06:00
|
|
|
}
|
2025-08-26 14:13:09 -06:00
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
// Insert user and get ID
|
|
|
|
|
var userID int
|
|
|
|
|
err = models.DB.QueryRow(`
|
|
|
|
|
INSERT INTO users (first_name, last_name, email, phone, password, role_id)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
|
|
|
RETURNING user_id
|
|
|
|
|
`, firstName, lastName, email, phone, string(hashedPassword), roleID).Scan(&userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("User registration failed: %v", err)
|
|
|
|
|
renderRegisterError(w, "Could not create account. Email might already be in use.")
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-26 14:13:09 -06:00
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
// Link volunteer to admin if role is volunteer
|
|
|
|
|
if roleID == 3 {
|
|
|
|
|
_, err = models.DB.Exec(`
|
|
|
|
|
INSERT INTO admin_volunteers (admin_id, volunteer_id)
|
|
|
|
|
VALUES ($1, $2)
|
|
|
|
|
`, adminID, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to link volunteer to admin: %v", err)
|
|
|
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
2025-08-26 14:13:09 -06:00
|
|
|
return
|
|
|
|
|
}
|
2025-08-27 13:21:11 -06:00
|
|
|
}
|
2025-08-26 14:13:09 -06:00
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
log.Printf("User registered successfully: %s %s (%s)", firstName, lastName, email)
|
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
|
}
|
2025-08-26 14:13:09 -06:00
|
|
|
|
2025-08-27 13:21:11 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
clearSessionCookie(w)
|
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
|
}
|