301 lines
7.9 KiB
Go
301 lines
7.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"html/template"
|
|
"mealprep/auth"
|
|
"mealprep/database"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LoginPageHandler shows the login page
|
|
func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
|
tmpl := `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Login - Meal Prep Planner</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<div class="auth-container">
|
|
<div class="auth-card">
|
|
<h1>🍽️ Meal Prep Planner</h1>
|
|
<h2>Login</h2>
|
|
|
|
{{if .Error}}
|
|
<div class="error-message">{{.Error}}</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="/login" class="auth-form">
|
|
<div class="form-group">
|
|
<label>Email:</label>
|
|
<input type="email" name="email" required autofocus />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password:</label>
|
|
<input type="password" name="password" required />
|
|
</div>
|
|
<button type="submit" class="btn-primary">Login</button>
|
|
</form>
|
|
|
|
<p class="auth-link">
|
|
Don't have an account? <a href="/register">Register here</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
data := struct {
|
|
Error string
|
|
}{
|
|
Error: r.URL.Query().Get("error"),
|
|
}
|
|
|
|
t := template.Must(template.New("login").Parse(tmpl))
|
|
t.Execute(w, data)
|
|
}
|
|
|
|
// RegisterPageHandler shows the registration page
|
|
func RegisterPageHandler(w http.ResponseWriter, r *http.Request) {
|
|
tmpl := `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Register - Meal Prep Planner</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<div class="auth-container">
|
|
<div class="auth-card">
|
|
<h1>🍽️ Meal Prep Planner</h1>
|
|
<h2>Create Account</h2>
|
|
|
|
{{if .Error}}
|
|
<div class="error-message">{{.Error}}</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="/register" class="auth-form">
|
|
<div class="form-group">
|
|
<label>Email:</label>
|
|
<input type="email" name="email" required autofocus />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password (min 8 characters):</label>
|
|
<input type="password" name="password" required minlength="8" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Confirm Password:</label>
|
|
<input type="password" name="password_confirm" required minlength="8" />
|
|
</div>
|
|
<button type="submit" class="btn-primary">Create Account</button>
|
|
</form>
|
|
|
|
<p class="auth-link">
|
|
Already have an account? <a href="/login">Login here</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
data := struct {
|
|
Error string
|
|
}{
|
|
Error: r.URL.Query().Get("error"),
|
|
}
|
|
|
|
t := template.Must(template.New("register").Parse(tmpl))
|
|
t.Execute(w, data)
|
|
}
|
|
|
|
// LoginHandler processes login requests
|
|
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Redirect(w, r, "/login?error=Invalid+request", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
email := strings.TrimSpace(strings.ToLower(r.FormValue("email")))
|
|
password := r.FormValue("password")
|
|
|
|
// Validate input
|
|
if email == "" || password == "" {
|
|
http.Redirect(w, r, "/login?error=Email+and+password+required", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if !auth.ValidateEmail(email) {
|
|
http.Redirect(w, r, "/login?error=Invalid+email+format", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Get user from database
|
|
user, err := database.GetUserByEmail(email)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
http.Redirect(w, r, "/login?error=Invalid+credentials", http.StatusSeeOther)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/login?error=Server+error", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Check password
|
|
if !auth.CheckPassword(password, user.PasswordHash) {
|
|
http.Redirect(w, r, "/login?error=Invalid+credentials", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Create session
|
|
token, err := auth.GenerateSessionToken()
|
|
if err != nil {
|
|
http.Redirect(w, r, "/login?error=Failed+to+create+session", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
expiresAt := auth.GetSessionExpiry()
|
|
if err := database.CreateSession(token, user.ID, expiresAt); err != nil {
|
|
http.Redirect(w, r, "/login?error=Failed+to+create+session", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Set secure cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: token,
|
|
Expires: expiresAt,
|
|
HttpOnly: true,
|
|
Secure: false, // Set to true in production with HTTPS
|
|
SameSite: http.SameSiteStrictMode,
|
|
Path: "/",
|
|
})
|
|
|
|
// Redirect to home
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
// RegisterHandler processes registration requests
|
|
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Redirect(w, r, "/register", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Redirect(w, r, "/register?error=Invalid+request", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
email := strings.TrimSpace(strings.ToLower(r.FormValue("email")))
|
|
password := r.FormValue("password")
|
|
passwordConfirm := r.FormValue("password_confirm")
|
|
|
|
// Validate input
|
|
if email == "" || password == "" || passwordConfirm == "" {
|
|
http.Redirect(w, r, "/register?error=All+fields+required", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if !auth.ValidateEmail(email) {
|
|
http.Redirect(w, r, "/register?error=Invalid+email+format", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if password != passwordConfirm {
|
|
http.Redirect(w, r, "/register?error=Passwords+do+not+match", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if len(password) < 8 {
|
|
http.Redirect(w, r, "/register?error=Password+must+be+at+least+8+characters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Check if user exists
|
|
existingUser, _ := database.GetUserByEmail(email)
|
|
if existingUser != nil {
|
|
http.Redirect(w, r, "/register?error=Email+already+registered", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Hash password
|
|
passwordHash, err := auth.HashPassword(password)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/register?error=Failed+to+create+account", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Create user
|
|
userID, err := database.CreateUser(email, passwordHash)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/register?error=Failed+to+create+account", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Create session
|
|
token, err := auth.GenerateSessionToken()
|
|
if err != nil {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
expiresAt := auth.GetSessionExpiry()
|
|
if err := database.CreateSession(token, int(userID), expiresAt); err != nil {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Set secure cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: token,
|
|
Expires: expiresAt,
|
|
HttpOnly: true,
|
|
Secure: false, // Set to true in production with HTTPS
|
|
SameSite: http.SameSiteStrictMode,
|
|
Path: "/",
|
|
})
|
|
|
|
// Redirect to home
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
// LogoutHandler processes logout requests
|
|
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Get session cookie
|
|
cookie, err := r.Cookie("session_token")
|
|
if err == nil {
|
|
// Delete session from database
|
|
database.DeleteSession(cookie.Value)
|
|
}
|
|
|
|
// Clear cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: "",
|
|
Expires: time.Unix(0, 0),
|
|
HttpOnly: true,
|
|
Secure: false,
|
|
SameSite: http.SameSiteStrictMode,
|
|
Path: "/",
|
|
MaxAge: -1,
|
|
})
|
|
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
}
|