added account
This commit is contained in:
300
handlers/auth.go
Normal file
300
handlers/auth.go
Normal file
@@ -0,0 +1,300 @@
|
||||
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)
|
||||
}
|
||||
@@ -2,13 +2,15 @@ package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"mealprep/auth"
|
||||
"mealprep/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// GroceryListHandler handles the grocery list page
|
||||
func GroceryListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
groceryItems, err := database.GetGroceryList()
|
||||
userID := auth.GetUserID(r)
|
||||
groceryItems, err := database.GetGroceryList(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"mealprep/auth"
|
||||
"mealprep/database"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -10,7 +11,8 @@ import (
|
||||
|
||||
// IngredientsHandler handles the ingredients page
|
||||
func IngredientsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ingredients, err := database.GetAllIngredients()
|
||||
userID := auth.GetUserID(r)
|
||||
ingredients, err := database.GetAllIngredients(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -51,6 +53,8 @@ func IngredientsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// AddIngredientHandler handles adding a new ingredient
|
||||
func AddIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -64,7 +68,7 @@ func AddIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := database.AddIngredient(name, unit)
|
||||
id, err := database.AddIngredient(userID, name, unit)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -97,6 +101,8 @@ func AddIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DeleteIngredientHandler handles deleting an ingredient
|
||||
func DeleteIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/ingredients/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
@@ -104,7 +110,7 @@ func DeleteIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DeleteIngredient(id); err != nil {
|
||||
if err := database.DeleteIngredient(userID, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"mealprep/auth"
|
||||
"mealprep/database"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -10,7 +11,8 @@ import (
|
||||
|
||||
// MealsHandler handles the meals page
|
||||
func MealsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
meals, err := database.GetAllMeals()
|
||||
userID := auth.GetUserID(r)
|
||||
meals, err := database.GetAllMeals(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -72,6 +74,8 @@ func MealsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// AddMealHandler handles adding a new meal
|
||||
func AddMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -92,7 +96,7 @@ func AddMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := database.AddMeal(name, description, mealType)
|
||||
id, err := database.AddMeal(userID, name, description, mealType)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -141,6 +145,8 @@ func AddMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DeleteMealHandler handles deleting a meal
|
||||
func DeleteMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/meals/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
@@ -148,7 +154,7 @@ func DeleteMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DeleteMeal(id); err != nil {
|
||||
if err := database.DeleteMeal(userID, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -158,6 +164,8 @@ func DeleteMealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetMealIngredientsHandler shows ingredients for a specific meal
|
||||
func GetMealIngredientsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||
@@ -170,13 +178,13 @@ func GetMealIngredientsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
mealIngredients, err := database.GetMealIngredients(mealID)
|
||||
mealIngredients, err := database.GetMealIngredients(userID, mealID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
allIngredients, err := database.GetAllIngredients()
|
||||
allIngredients, err := database.GetAllIngredients(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -233,6 +241,8 @@ func GetMealIngredientsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// AddMealIngredientHandler adds an ingredient to a meal
|
||||
func AddMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||
@@ -262,13 +272,13 @@ func AddMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.AddMealIngredient(mealID, ingredientID, quantity); err != nil {
|
||||
if err := database.AddMealIngredient(userID, mealID, ingredientID, quantity); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the ingredient details to display
|
||||
ingredients, err := database.GetMealIngredients(mealID)
|
||||
ingredients, err := database.GetMealIngredients(userID, mealID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -308,6 +318,8 @@ func AddMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DeleteMealIngredientHandler removes an ingredient from a meal
|
||||
func DeleteMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
if len(parts) < 5 {
|
||||
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||
@@ -326,7 +338,7 @@ func DeleteMealIngredientHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DeleteMealIngredient(mealID, ingredientID); err != nil {
|
||||
if err := database.DeleteMealIngredient(userID, mealID, ingredientID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"mealprep/auth"
|
||||
"mealprep/database"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -11,13 +12,15 @@ import (
|
||||
|
||||
// WeekPlanHandler handles the week plan page
|
||||
func WeekPlanHandler(w http.ResponseWriter, r *http.Request) {
|
||||
weekPlan, err := database.GetWeekPlan()
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
weekPlan, err := database.GetWeekPlan(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
meals, err := database.GetAllMeals()
|
||||
meals, err := database.GetAllMeals(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -224,6 +227,8 @@ func WeekPlanHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// AddWeekPlanEntryHandler adds a meal to a specific day
|
||||
func AddWeekPlanEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -249,13 +254,13 @@ func AddWeekPlanEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.AddWeekPlanEntry(date, mealID); err != nil {
|
||||
if err := database.AddWeekPlanEntry(userID, date, mealID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the meal details
|
||||
meal, err := database.GetMealByID(mealID)
|
||||
meal, err := database.GetMealByID(userID, mealID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -268,7 +273,7 @@ func AddWeekPlanEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Get the ID of the entry we just added
|
||||
weekPlan, err := database.GetWeekPlan()
|
||||
weekPlan, err := database.GetWeekPlan(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -305,6 +310,8 @@ func AddWeekPlanEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// DeleteWeekPlanEntryHandler removes a meal from the week plan
|
||||
func DeleteWeekPlanEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r)
|
||||
|
||||
idStr := strings.TrimPrefix(r.URL.Path, "/week-plan/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
@@ -312,7 +319,7 @@ func DeleteWeekPlanEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DeleteWeekPlanEntry(id); err != nil {
|
||||
if err := database.DeleteWeekPlanEntry(userID, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user