frontend: disabled button while processing password reset
This commit is contained in:
12
go-backend/internal/services/bank_account_service.go
Normal file
12
go-backend/internal/services/bank_account_service.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"GoMembership/internal/repositories"
|
||||
)
|
||||
|
||||
type BankAccountServiceInterface interface {
|
||||
}
|
||||
|
||||
type BankAccountService struct {
|
||||
Repo repositories.BankAccountRepositoryInterface
|
||||
}
|
||||
22
go-backend/internal/services/consent_service.go
Normal file
22
go-backend/internal/services/consent_service.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
)
|
||||
|
||||
type ConsentServiceInterface interface {
|
||||
RegisterConsent(consent *models.Consent) (uint, error)
|
||||
}
|
||||
|
||||
type ConsentService struct {
|
||||
Repo repositories.ConsentRepositoryInterface
|
||||
}
|
||||
|
||||
func (service *ConsentService) RegisterConsent(consent *models.Consent) (uint, error) {
|
||||
consent.CreatedAt = time.Now()
|
||||
consent.UpdatedAt = time.Now()
|
||||
return service.Repo.CreateConsent(consent)
|
||||
}
|
||||
240
go-backend/internal/services/email_service.go
Normal file
240
go-backend/internal/services/email_service.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
|
||||
"GoMembership/internal/config"
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
dialer *gomail.Dialer
|
||||
}
|
||||
|
||||
func NewEmailService(host string, port int, username string, password string) *EmailService {
|
||||
dialer := gomail.NewDialer(host, port, username, password)
|
||||
return &EmailService{dialer: dialer}
|
||||
}
|
||||
|
||||
func (s *EmailService) SendEmail(to string, subject string, body string, bodyTXT string, replyTo string) error {
|
||||
msg := gomail.NewMessage()
|
||||
msg.SetHeader("From", s.dialer.Username)
|
||||
msg.SetHeader("To", to)
|
||||
msg.SetHeader("Subject", subject)
|
||||
if replyTo != "" {
|
||||
msg.SetHeader("REPLY_TO", replyTo)
|
||||
}
|
||||
if bodyTXT != "" {
|
||||
msg.SetBody("text/plain", bodyTXT)
|
||||
}
|
||||
|
||||
msg.AddAlternative("text/html", body)
|
||||
// msg.WriteTo(os.Stdout)
|
||||
|
||||
if err := s.dialer.DialAndSend(msg); err != nil {
|
||||
logger.Error.Printf("Could not send email to %s: %v", to, err)
|
||||
return err
|
||||
}
|
||||
logger.Info.Printf("Email sent to %s", to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseTemplate(filename string, data interface{}) (string, error) {
|
||||
// Read the email template file
|
||||
|
||||
templateDir := config.Templates.MailPath
|
||||
tpl, err := template.ParseFiles(templateDir + "/" + filename)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Failed to parse email template: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Buffer to hold the rendered template
|
||||
var tplBuffer bytes.Buffer
|
||||
if err := tpl.Execute(&tplBuffer, data); err != nil {
|
||||
logger.Error.Printf("Failed to execute email template: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tplBuffer.String(), nil
|
||||
}
|
||||
|
||||
func (s *EmailService) SendVerificationEmail(user *models.User, token *string) error {
|
||||
// Prepare data to be injected into the template
|
||||
data := struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Token string
|
||||
BASEURL string
|
||||
}{
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Token: *token,
|
||||
BASEURL: config.Site.BaseURL,
|
||||
}
|
||||
|
||||
subject := constants.MailVerificationSubject
|
||||
body, err := ParseTemplate("mail_verification.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send verification mail")
|
||||
return err
|
||||
}
|
||||
return s.SendEmail(user.Email, subject, body, "", "")
|
||||
|
||||
}
|
||||
|
||||
func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string) error {
|
||||
// Prepare data to be injected into the template
|
||||
data := struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Token string
|
||||
BASEURL string
|
||||
UserID uint
|
||||
}{
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Token: *token,
|
||||
BASEURL: config.Site.BaseURL,
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
subject := constants.MailChangePasswordSubject
|
||||
htmlBody, err := ParseTemplate("mail_change_password.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't parse password mail")
|
||||
return err
|
||||
}
|
||||
plainBody, err := ParseTemplate("mail_change_password.txt.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't parse password mail")
|
||||
return err
|
||||
}
|
||||
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
|
||||
|
||||
}
|
||||
|
||||
func (s *EmailService) SendWelcomeEmail(user *models.User) error {
|
||||
// Prepare data to be injected into the template
|
||||
data := struct {
|
||||
Company string
|
||||
FirstName string
|
||||
MembershipModel string
|
||||
BASEURL string
|
||||
MembershipID uint
|
||||
MembershipFee float32
|
||||
Logo string
|
||||
WebsiteTitle string
|
||||
RentalFee float32
|
||||
}{
|
||||
Company: user.Company,
|
||||
FirstName: user.FirstName,
|
||||
MembershipModel: user.Membership.SubscriptionModel.Name,
|
||||
MembershipID: user.Membership.ID,
|
||||
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
|
||||
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
|
||||
BASEURL: config.Site.BaseURL,
|
||||
WebsiteTitle: config.Site.WebsiteTitle,
|
||||
Logo: config.Templates.LogoURI,
|
||||
}
|
||||
|
||||
subject := constants.MailWelcomeSubject
|
||||
htmlBody, err := ParseTemplate("mail_welcome.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send welcome mail")
|
||||
return err
|
||||
}
|
||||
plainBody, err := ParseTemplate("mail_welcome.txt.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't parse password mail")
|
||||
return err
|
||||
}
|
||||
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
|
||||
}
|
||||
|
||||
func (s *EmailService) SendRegistrationNotification(user *models.User) error {
|
||||
// Prepare data to be injected into the template
|
||||
data := struct {
|
||||
FirstName string
|
||||
DateOfBirth string
|
||||
LastName string
|
||||
MembershipModel string
|
||||
Address string
|
||||
IBAN string
|
||||
Email string
|
||||
Phone string
|
||||
City string
|
||||
Company string
|
||||
ZipCode string
|
||||
BASEURL string
|
||||
MembershipID uint
|
||||
RentalFee float32
|
||||
MembershipFee float32
|
||||
Logo string
|
||||
WebsiteTitle string
|
||||
}{
|
||||
Company: user.Company,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
MembershipModel: user.Membership.SubscriptionModel.Name,
|
||||
MembershipID: user.Membership.ID,
|
||||
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
|
||||
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
|
||||
Address: user.Address,
|
||||
ZipCode: user.ZipCode,
|
||||
City: user.City,
|
||||
DateOfBirth: user.DateOfBirth.Format("20060102"),
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
IBAN: user.BankAccount.IBAN,
|
||||
BASEURL: config.Site.BaseURL,
|
||||
Logo: config.Templates.LogoURI,
|
||||
WebsiteTitle: config.Site.WebsiteTitle,
|
||||
}
|
||||
|
||||
subject := constants.MailRegistrationSubject
|
||||
htmlBody, err := ParseTemplate("mail_registration.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send admin notification mail")
|
||||
return err
|
||||
}
|
||||
plainBody, err := ParseTemplate("mail_registration.txt.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't parse password mail")
|
||||
return err
|
||||
}
|
||||
return s.SendEmail(config.Recipients.UserRegistration, subject, htmlBody, plainBody, "")
|
||||
}
|
||||
|
||||
func (s *EmailService) RelayContactFormMessage(sender string, name string, message string) error {
|
||||
data := struct {
|
||||
Message string
|
||||
Name string
|
||||
BASEURL string
|
||||
Logo string
|
||||
WebsiteTitle string
|
||||
}{
|
||||
Message: message,
|
||||
Name: name,
|
||||
BASEURL: config.Site.BaseURL,
|
||||
Logo: config.Templates.LogoURI,
|
||||
WebsiteTitle: config.Site.WebsiteTitle,
|
||||
}
|
||||
subject := constants.MailContactSubject
|
||||
htmlBody, err := ParseTemplate("mail_contact_form.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send contact form message mail")
|
||||
return err
|
||||
}
|
||||
plainBody, err := ParseTemplate("mail_contact_form.txt.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't parse password mail")
|
||||
return err
|
||||
}
|
||||
return s.SendEmail(config.Recipients.ContactForm, subject, htmlBody, plainBody, sender)
|
||||
}
|
||||
18
go-backend/internal/services/licence_service.go
Normal file
18
go-backend/internal/services/licence_service.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
)
|
||||
|
||||
type LicenceInterface interface {
|
||||
GetAllCategories() ([]models.Category, error)
|
||||
}
|
||||
|
||||
type LicenceService struct {
|
||||
Repo repositories.LicenceInterface
|
||||
}
|
||||
|
||||
func (s *LicenceService) GetAllCategories() ([]models.Category, error) {
|
||||
return s.Repo.GetAllCategories()
|
||||
}
|
||||
100
go-backend/internal/services/membership_service.go
Normal file
100
go-backend/internal/services/membership_service.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
"GoMembership/pkg/errors"
|
||||
)
|
||||
|
||||
type MembershipServiceInterface interface {
|
||||
RegisterMembership(membership *models.Membership) (uint, error)
|
||||
FindMembershipByUserID(userID uint) (*models.Membership, error)
|
||||
RegisterSubscription(subscription *models.SubscriptionModel) (uint, error)
|
||||
UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error)
|
||||
DeleteSubscription(id *uint, name *string) error
|
||||
GetSubscriptionModelNames() ([]string, error)
|
||||
GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error)
|
||||
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
|
||||
}
|
||||
|
||||
type MembershipService struct {
|
||||
Repo repositories.MembershipRepositoryInterface
|
||||
SubscriptionRepo repositories.SubscriptionModelsRepositoryInterface
|
||||
}
|
||||
|
||||
func (service *MembershipService) RegisterMembership(membership *models.Membership) (uint, error) {
|
||||
membership.StartDate = time.Now()
|
||||
return service.Repo.CreateMembership(membership)
|
||||
}
|
||||
|
||||
func (service *MembershipService) UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error) {
|
||||
|
||||
existingSubscription, err := repositories.GetSubscriptionByName(&subscription.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingSubscription == nil {
|
||||
return nil, errors.ErrSubscriptionNotFound
|
||||
}
|
||||
if existingSubscription.MonthlyFee != subscription.MonthlyFee ||
|
||||
existingSubscription.HourlyRate != subscription.HourlyRate ||
|
||||
existingSubscription.Conditions != subscription.Conditions ||
|
||||
existingSubscription.IncludedPerYear != subscription.IncludedPerYear ||
|
||||
existingSubscription.IncludedPerMonth != subscription.IncludedPerMonth {
|
||||
return nil, errors.ErrInvalidSubscriptionData
|
||||
}
|
||||
subscription.ID = existingSubscription.ID
|
||||
return service.SubscriptionRepo.UpdateSubscription(subscription)
|
||||
|
||||
}
|
||||
|
||||
func (service *MembershipService) DeleteSubscription(id *uint, name *string) error {
|
||||
if *name == "" {
|
||||
return errors.ErrNoData
|
||||
}
|
||||
exists, err := repositories.GetSubscriptionByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists == nil {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
if *id != exists.ID {
|
||||
return errors.ErrInvalidSubscriptionData
|
||||
}
|
||||
usersInSubscription, err := repositories.GetUsersBySubscription(*id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(*usersInSubscription) > 0 {
|
||||
return errors.ErrSubscriptionInUse
|
||||
}
|
||||
return service.SubscriptionRepo.DeleteSubscription(id)
|
||||
}
|
||||
|
||||
func (service *MembershipService) FindMembershipByUserID(userID uint) (*models.Membership, error) {
|
||||
return service.Repo.FindMembershipByUserID(userID)
|
||||
}
|
||||
|
||||
// Membership_Subscriptions
|
||||
func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) {
|
||||
return service.SubscriptionRepo.CreateSubscriptionModel(subscription)
|
||||
}
|
||||
|
||||
func (service *MembershipService) GetSubscriptionModelNames() ([]string, error) {
|
||||
return service.SubscriptionRepo.GetSubscriptionModelNames()
|
||||
}
|
||||
|
||||
func (service *MembershipService) GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error) {
|
||||
return repositories.GetSubscriptionByName(modelname)
|
||||
}
|
||||
|
||||
func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) {
|
||||
if where == nil {
|
||||
where = map[string]interface{}{}
|
||||
}
|
||||
return service.SubscriptionRepo.GetSubscriptions(where)
|
||||
}
|
||||
21
go-backend/internal/services/user_password.go
Normal file
21
go-backend/internal/services/user_password.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
)
|
||||
|
||||
func (s *UserService) HandlePasswordChangeRequest(user *models.User) (token string, err error) {
|
||||
// Deactivate user and reset Verification
|
||||
if err := s.SetUserStatus(user.ID, constants.DisabledStatus); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := s.RevokeVerification(&user.ID, constants.VerificationTypes.Password); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Generate a token
|
||||
return s.SetVerificationToken(&user.ID, &constants.VerificationTypes.Password)
|
||||
|
||||
}
|
||||
5
go-backend/internal/services/user_permissions.go
Normal file
5
go-backend/internal/services/user_permissions.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package services
|
||||
|
||||
func (s *UserService) SetUserStatus(id uint, status uint) error {
|
||||
return s.Repo.SetUserStatus(id, status)
|
||||
}
|
||||
116
go-backend/internal/services/user_service.go
Normal file
116
go-backend/internal/services/user_service.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
"GoMembership/pkg/errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserServiceInterface interface {
|
||||
RegisterUser(user *models.User) (id uint, token string, err error)
|
||||
GetUserByEmail(email string) (*models.User, error)
|
||||
GetUserByID(id uint) (*models.User, error)
|
||||
GetUsers(where map[string]interface{}) (*[]models.User, error)
|
||||
UpdateUser(user *models.User) (*models.User, error)
|
||||
DeleteUser(lastname string, id uint) error
|
||||
SetUserStatus(id uint, status uint) error
|
||||
VerifyUser(token *string, verificationType *string) (*models.Verification, error)
|
||||
SetVerificationToken(id *uint, verificationType *string) (string, error)
|
||||
RevokeVerification(id *uint, verificationType string) error
|
||||
HandlePasswordChangeRequest(user *models.User) (token string, err error)
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
Repo repositories.UserRepositoryInterface
|
||||
Licences repositories.LicenceInterface
|
||||
}
|
||||
|
||||
func (service *UserService) DeleteUser(lastname string, id uint) error {
|
||||
if id == 0 || lastname == "" {
|
||||
return errors.ErrNoData
|
||||
}
|
||||
|
||||
user, err := service.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return errors.ErrUserNotFound
|
||||
}
|
||||
|
||||
return service.Repo.DeleteUser(id)
|
||||
}
|
||||
|
||||
func (service *UserService) UpdateUser(user *models.User) (*models.User, error) {
|
||||
|
||||
if user.ID == 0 {
|
||||
return nil, errors.ErrUserNotFound
|
||||
}
|
||||
|
||||
user.SetPassword(user.Password)
|
||||
|
||||
// Validate subscription model
|
||||
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name)
|
||||
if err != nil {
|
||||
return nil, errors.ErrSubscriptionNotFound
|
||||
}
|
||||
user.Membership.SubscriptionModel = *selectedModel
|
||||
user.Membership.SubscriptionModelID = selectedModel.ID
|
||||
|
||||
updatedUser, err := service.Repo.UpdateUser(user)
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.ErrUserNotFound
|
||||
}
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
return nil, errors.ErrDuplicateEntry
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
func (service *UserService) RegisterUser(user *models.User) (id uint, token string, err error) {
|
||||
|
||||
user.SetPassword(user.Password)
|
||||
|
||||
user.Status = constants.UnverifiedStatus
|
||||
user.CreatedAt = time.Now()
|
||||
user.UpdatedAt = time.Now()
|
||||
user.PaymentStatus = constants.AwaitingPaymentStatus
|
||||
user.BankAccount.MandateDateSigned = time.Now()
|
||||
id, err = service.Repo.CreateUser(user)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
token, err = service.SetVerificationToken(&id, &constants.VerificationTypes.Email)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return id, token, nil
|
||||
}
|
||||
|
||||
func (service *UserService) GetUserByID(id uint) (*models.User, error) {
|
||||
return repositories.GetUserByID(&id)
|
||||
}
|
||||
|
||||
func (service *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||
return service.Repo.GetUserByEmail(email)
|
||||
}
|
||||
|
||||
func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.User, error) {
|
||||
if where == nil {
|
||||
where = map[string]interface{}{}
|
||||
}
|
||||
return service.Repo.GetUsers(where)
|
||||
}
|
||||
59
go-backend/internal/services/user_verification.go
Normal file
59
go-backend/internal/services/user_verification.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *UserService) SetVerificationToken(id *uint, verificationType *string) (string, error) {
|
||||
|
||||
token, err := utils.GenerateVerificationToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if user is already verified
|
||||
verified, err := s.Repo.IsVerified(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if verified {
|
||||
return "", errors.ErrAlreadyVerified
|
||||
}
|
||||
|
||||
// Prepare the Verification record
|
||||
verification := models.Verification{
|
||||
UserID: *id,
|
||||
VerificationToken: token,
|
||||
Type: *verificationType,
|
||||
}
|
||||
|
||||
return s.Repo.SetVerificationToken(&verification)
|
||||
}
|
||||
|
||||
func (s *UserService) RevokeVerification(id *uint, verificationType string) error {
|
||||
return s.Repo.DeleteVerification(*id, verificationType)
|
||||
|
||||
}
|
||||
|
||||
func (service *UserService) VerifyUser(token *string, verificationType *string) (*models.Verification, error) {
|
||||
verification, err := service.Repo.GetVerificationOfToken(token, verificationType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the user is already verified
|
||||
verified, err := service.Repo.IsVerified(&verification.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if verified {
|
||||
return nil, errors.ErrAlreadyVerified
|
||||
}
|
||||
t := time.Now()
|
||||
verification.VerifiedAt = &t
|
||||
|
||||
return verification, nil
|
||||
}
|
||||
Reference in New Issue
Block a user