367 lines
11 KiB
Go
367 lines
11 KiB
Go
|
|
package handlers
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"log"
|
||
|
|
"net/http"
|
||
|
|
"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 LoginPage(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// utils.Render(w, "login.html", map[string]interface{}{
|
||
|
|
// "Title": "Login",
|
||
|
|
// "IsAuthenticated": false,
|
||
|
|
// })
|
||
|
|
// }
|
||
|
|
|
||
|
|
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 == "" {
|
||
|
|
renderLoginError(w, "Email and password are required")
|
||
|
|
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)
|
||
|
|
renderLoginError(w, "Invalid email or password")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify password
|
||
|
|
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("Password verification failed for user ID %d", userID)
|
||
|
|
renderLoginError(w, "Invalid email or password")
|
||
|
|
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)
|
||
|
|
http.Error(w, "Could not log in", http.StatusInternalServerError)
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
|
||
|
|
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")
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|
||
|
|
|
||
|
|
// Insert user into database
|
||
|
|
_, err = models.DB.Exec(`
|
||
|
|
INSERT INTO "users" (first_name, last_name, email, phone, password, role_id)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||
|
|
`, firstName, lastName, email, phone, string(hashedPassword), role)
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("User registration failed for email %s: %v", email, err)
|
||
|
|
renderRegisterError(w, "Could not create account. Email might already be in use.")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Printf("User registered successfully: %s %s (%s)", firstName, lastName, email)
|
||
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||
|
|
}
|
||
|
|
|
||
|
|
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
clearSessionCookie(w)
|
||
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||
|
|
}
|
||
|
|
|
||
|
|
// // Admin Dashboard Handler
|
||
|
|
// func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
// userID := r.Context().Value("user_id").(int)
|
||
|
|
|
||
|
|
// // TODO: Fetch real data from database
|
||
|
|
// dashboardData := map[string]interface{}{
|
||
|
|
// "UserID": userID,
|
||
|
|
// "TotalUsers": 100, // Example: get from database
|
||
|
|
// "TotalVolunteers": 50, // Example: get from database
|
||
|
|
// "TotalAddresses": 200, // Example: get from database
|
||
|
|
// "RecentActivity": []string{"User logged in", "New volunteer registered"}, // Example
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("Admin Dashboard", "dashboard", role, true, dashboardData)
|
||
|
|
// utils.Render(w, "dashboard/dashboard.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // Volunteer Management Handler
|
||
|
|
// func VolunteerHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
|
||
|
|
// // TODO: Fetch real volunteer data from database
|
||
|
|
// volunteerData := map[string]interface{}{
|
||
|
|
// "Volunteers": []map[string]interface{}{
|
||
|
|
// {"ID": 1, "Name": "John Doe", "Email": "john@example.com", "Status": "Active"},
|
||
|
|
// {"ID": 2, "Name": "Jane Smith", "Email": "jane@example.com", "Status": "Active"},
|
||
|
|
// }, // Example: get from database
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("Volunteers", "volunteer", role, true, volunteerData)
|
||
|
|
// utils.Render(w, "volunteers/volunteers.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // Address Management Handler
|
||
|
|
// func AddressHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
|
||
|
|
// // TODO: Fetch real address data from database
|
||
|
|
// addressData := map[string]interface{}{
|
||
|
|
// "Addresses": []map[string]interface{}{
|
||
|
|
// {"ID": 1, "Street": "123 Main St", "City": "Calgary", "Status": "Validated"},
|
||
|
|
// {"ID": 2, "Street": "456 Oak Ave", "City": "Calgary", "Status": "Pending"},
|
||
|
|
// }, // Example: get from database
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("Addresses", "address", role, true, addressData)
|
||
|
|
// utils.Render(w, "addresses/addresses.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // Reports Handler
|
||
|
|
// func ReportHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
|
||
|
|
// // TODO: Fetch real report data from database
|
||
|
|
// reportData := map[string]interface{}{
|
||
|
|
// "Reports": []map[string]interface{}{
|
||
|
|
// {"ID": 1, "Name": "Weekly Summary", "Date": "2025-08-25", "Status": "Complete"},
|
||
|
|
// {"ID": 2, "Name": "Monthly Analytics", "Date": "2025-08-01", "Status": "Pending"},
|
||
|
|
// }, // Example: get from database
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("Reports", "report", role, true, reportData)
|
||
|
|
// utils.Render(w, "reports/reports.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // Profile Handler (works for both admin and volunteer)
|
||
|
|
// func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
// userID := r.Context().Value("user_id").(int)
|
||
|
|
|
||
|
|
// // Fetch real user data from database
|
||
|
|
// var firstName, lastName, email, phone string
|
||
|
|
// err := models.DB.QueryRow(`
|
||
|
|
// SELECT first_name, last_name, email, phone
|
||
|
|
// FROM "users"
|
||
|
|
// WHERE user_id = $1
|
||
|
|
// `, userID).Scan(&firstName, &lastName, &email, &phone)
|
||
|
|
|
||
|
|
// profileData := map[string]interface{}{
|
||
|
|
// "UserID": userID,
|
||
|
|
// }
|
||
|
|
|
||
|
|
// if err != nil {
|
||
|
|
// log.Printf("Error fetching user profile for ID %d: %v", userID, err)
|
||
|
|
// profileData["Error"] = "Could not load profile data"
|
||
|
|
// } else {
|
||
|
|
// profileData["FirstName"] = firstName
|
||
|
|
// profileData["LastName"] = lastName
|
||
|
|
// profileData["Email"] = email
|
||
|
|
// profileData["Phone"] = phone
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("Profile", "profile", role, true, profileData)
|
||
|
|
// utils.Render(w, "profile/profile.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // Volunteer Dashboard Handler
|
||
|
|
// func VolunteerDashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
// userID := r.Context().Value("user_id").(int)
|
||
|
|
|
||
|
|
// // TODO: Fetch volunteer-specific data from database
|
||
|
|
// dashboardData := map[string]interface{}{
|
||
|
|
// "UserID": userID,
|
||
|
|
// "AssignedTasks": 5, // Example: get from database
|
||
|
|
// "CompletedTasks": 12, // Example: get from database
|
||
|
|
// "UpcomingEvents": []string{"Community Meeting - Aug 30", "Training Session - Sep 5"}, // Example
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("Volunteer Dashboard", "dashboard", role, true, dashboardData)
|
||
|
|
// utils.Render(w, "volunteer/dashboard.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // Schedule Handler for Volunteers
|
||
|
|
// func ScheduleHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
// role := r.Context().Value("user_role").(int)
|
||
|
|
// userID := r.Context().Value("user_id").(int)
|
||
|
|
|
||
|
|
// // TODO: Fetch schedule data from database
|
||
|
|
// scheduleData := map[string]interface{}{
|
||
|
|
// "UserID": userID,
|
||
|
|
// "Schedule": []map[string]interface{}{
|
||
|
|
// {"Date": "2025-08-26", "Time": "10:00 AM", "Task": "Door-to-door survey", "Location": "Downtown"},
|
||
|
|
// {"Date": "2025-08-28", "Time": "2:00 PM", "Task": "Data entry", "Location": "Office"},
|
||
|
|
// }, // Example: get from database
|
||
|
|
// }
|
||
|
|
|
||
|
|
// data := createTemplateData("My Schedule", "schedual", role, true, scheduleData)
|
||
|
|
// utils.Render(w, "volunteer/schedule.html", data)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// Enhanced middleware to check JWT auth and add user context
|
||
|
|
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cookie, err := r.Cookie("session")
|
||
|
|
if err != nil {
|
||
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
claims := &models.Claims{}
|
||
|
|
token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (interface{}, error) {
|
||
|
|
return jwtKey, nil
|
||
|
|
})
|
||
|
|
|
||
|
|
if err != nil || !token.Valid {
|
||
|
|
log.Printf("Invalid token: %v", err)
|
||
|
|
clearSessionCookie(w) // Clear invalid cookie
|
||
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add user info to context
|
||
|
|
ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
|
||
|
|
ctx = context.WithValue(ctx, "user_role", claims.Role)
|
||
|
|
r = r.WithContext(ctx)
|
||
|
|
|
||
|
|
next.ServeHTTP(w, r)
|
||
|
|
}
|
||
|
|
}
|