fatto ordine
This commit is contained in:
520
controllers/auth_controller.go
Normal file
520
controllers/auth_controller.go
Normal file
@@ -0,0 +1,520 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthController struct {
|
||||
DB *gorm.DB
|
||||
IsUniqueConstraint func(error) bool
|
||||
AppBaseURL func() string
|
||||
MailConfigured func() bool
|
||||
SendPasswordResetEmail func(recipientEmail, resetURL string) error
|
||||
SendEmailVerificationMail func(recipientEmail, verifyURL string) error
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
ConfirmPassword string `json:"confirmPassword"`
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type forgotPasswordRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type resetPasswordRequest struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
ConfirmPassword string `json:"confirmPassword"`
|
||||
}
|
||||
|
||||
type resendVerificationRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
const sessionCookieName = "bag_exchange_session"
|
||||
const sessionDurationDays = 7
|
||||
const passwordResetDurationMinutes = 60
|
||||
const emailVerificationDurationHours = 24
|
||||
|
||||
func (a *AuthController) Register(c fiber.Ctx) error {
|
||||
var req registerRequest
|
||||
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_payload"})
|
||||
}
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_email"})
|
||||
}
|
||||
|
||||
if req.Password != req.ConfirmPassword {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "password_mismatch"})
|
||||
}
|
||||
|
||||
if !isStrongPassword(req.Password) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "password_too_weak"})
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("unable to hash password: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_register"})
|
||||
}
|
||||
|
||||
err = a.DB.Table("users").Create(map[string]any{
|
||||
"email": email,
|
||||
"password_hash": string(passwordHash),
|
||||
"email_verified": 0,
|
||||
}).Error
|
||||
if err != nil {
|
||||
if a.IsUniqueConstraint != nil && a.IsUniqueConstraint(err) {
|
||||
return c.Status(fiber.StatusConflict).JSON(map[string]string{"error": "email_exists"})
|
||||
}
|
||||
log.Printf("unable to register user: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_register"})
|
||||
}
|
||||
|
||||
var createdUser struct {
|
||||
ID int64 `gorm:"column:id"`
|
||||
}
|
||||
err = a.DB.Table("users").Select("id").Where("email = ?", email).Take(&createdUser).Error
|
||||
if err != nil {
|
||||
log.Printf("unable to read new user id: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_register"})
|
||||
}
|
||||
userID := createdUser.ID
|
||||
|
||||
verifyToken, err := createEmailVerificationToken(a.DB, userID)
|
||||
if err != nil {
|
||||
log.Printf("unable to create verify token: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_register"})
|
||||
}
|
||||
|
||||
if a.MailConfigured != nil && a.MailConfigured() && a.AppBaseURL != nil && a.SendEmailVerificationMail != nil {
|
||||
verifyURL := fmt.Sprintf("%s/auth/verify-email?token=%s", a.AppBaseURL(), verifyToken)
|
||||
if err := a.SendEmailVerificationMail(email, verifyURL); err != nil {
|
||||
log.Printf("unable to send verification email: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(map[string]string{"status": "registered_pending_verification"})
|
||||
}
|
||||
|
||||
func (a *AuthController) Login(c fiber.Ctx) error {
|
||||
var req loginRequest
|
||||
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_payload"})
|
||||
}
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if _, err := mail.ParseAddress(email); err != nil || req.Password == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(map[string]string{"error": "invalid_credentials"})
|
||||
}
|
||||
|
||||
var loginUser struct {
|
||||
ID int64 `gorm:"column:id"`
|
||||
PasswordHash string `gorm:"column:password_hash"`
|
||||
EmailVerified int `gorm:"column:email_verified"`
|
||||
}
|
||||
err := a.DB.Table("users").
|
||||
Select("id, password_hash, email_verified").
|
||||
Where("email = ?", email).
|
||||
Take(&loginUser).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(map[string]string{"error": "invalid_credentials"})
|
||||
}
|
||||
log.Printf("unable to fetch user for login: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_login"})
|
||||
}
|
||||
userID := loginUser.ID
|
||||
passwordHash := loginUser.PasswordHash
|
||||
emailVerified := loginUser.EmailVerified
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(map[string]string{"error": "invalid_credentials"})
|
||||
}
|
||||
if emailVerified == 0 {
|
||||
return c.Status(fiber.StatusForbidden).JSON(map[string]string{"error": "email_not_verified"})
|
||||
}
|
||||
|
||||
sessionToken, err := generateSessionToken()
|
||||
if err != nil {
|
||||
log.Printf("unable to generate session token: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_login"})
|
||||
}
|
||||
|
||||
expiresAt := time.Now().AddDate(0, 0, sessionDurationDays).Unix()
|
||||
err = a.DB.Table("sessions").Create(map[string]any{
|
||||
"user_id": userID,
|
||||
"token_hash": hashToken(sessionToken),
|
||||
"expires_at": expiresAt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
log.Printf("unable to create session: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_login"})
|
||||
}
|
||||
|
||||
setSessionCookie(c, sessionToken, expiresAt)
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]any{
|
||||
"status": "authenticated",
|
||||
"email": email,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *AuthController) Logout(c fiber.Ctx) error {
|
||||
sessionToken := c.Cookies(sessionCookieName)
|
||||
if sessionToken != "" {
|
||||
if err := a.DB.Table("sessions").Where("token_hash = ?", hashToken(sessionToken)).Delete(nil).Error; err != nil {
|
||||
log.Printf("unable to delete session: %v", err)
|
||||
}
|
||||
}
|
||||
clearSessionCookie(c)
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "logged_out"})
|
||||
}
|
||||
|
||||
func (a *AuthController) Me(c fiber.Ctx) error {
|
||||
sessionToken := c.Cookies(sessionCookieName)
|
||||
if sessionToken == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(map[string]any{"authenticated": false})
|
||||
}
|
||||
|
||||
var sessionUser struct {
|
||||
UserID int64 `gorm:"column:user_id"`
|
||||
Email string `gorm:"column:email"`
|
||||
}
|
||||
err := a.DB.Table("sessions").
|
||||
Select("users.id AS user_id, users.email").
|
||||
Joins("JOIN users ON users.id = sessions.user_id").
|
||||
Where("sessions.token_hash = ? AND sessions.expires_at > ?", hashToken(sessionToken), time.Now().Unix()).
|
||||
Take(&sessionUser).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
clearSessionCookie(c)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(map[string]any{"authenticated": false})
|
||||
}
|
||||
log.Printf("unable to resolve session: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_fetch_session"})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]any{
|
||||
"authenticated": true,
|
||||
"userId": sessionUser.UserID,
|
||||
"email": sessionUser.Email,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||
var req forgotPasswordRequest
|
||||
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_payload"})
|
||||
}
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
var userID int64
|
||||
err := a.DB.Table("users").Select("id").Where("email = ?", email).Take(&userID).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
log.Printf("unable to fetch user for forgot-password: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_process"})
|
||||
}
|
||||
|
||||
resetToken, err := generateSessionToken()
|
||||
if err != nil {
|
||||
log.Printf("unable to generate reset token: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_process"})
|
||||
}
|
||||
expiresAt := time.Now().Add(time.Minute * passwordResetDurationMinutes).Unix()
|
||||
|
||||
err = a.DB.Table("password_reset_tokens").Create(map[string]any{
|
||||
"user_id": userID,
|
||||
"token_hash": hashToken(resetToken),
|
||||
"expires_at": expiresAt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
log.Printf("unable to persist reset token: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_process"})
|
||||
}
|
||||
|
||||
if a.MailConfigured != nil && a.MailConfigured() && a.AppBaseURL != nil && a.SendPasswordResetEmail != nil {
|
||||
resetURL := fmt.Sprintf("%s/reset-password?token=%s", a.AppBaseURL(), resetToken)
|
||||
if err := a.SendPasswordResetEmail(email, resetURL); err != nil {
|
||||
log.Printf("unable to send reset email: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("smtp not configured: skip password reset email")
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (a *AuthController) ResendVerification(c fiber.Ctx) error {
|
||||
var req resendVerificationRequest
|
||||
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_payload"})
|
||||
}
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
var resendUser struct {
|
||||
ID int64 `gorm:"column:id"`
|
||||
EmailVerified int `gorm:"column:email_verified"`
|
||||
}
|
||||
err := a.DB.Table("users").Select("id, email_verified").Where("email = ?", email).Take(&resendUser).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
log.Printf("unable to fetch user for resend verification: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_process"})
|
||||
}
|
||||
userID := resendUser.ID
|
||||
verified := resendUser.EmailVerified
|
||||
if verified != 0 {
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
verifyToken, err := createEmailVerificationToken(a.DB, userID)
|
||||
if err != nil {
|
||||
log.Printf("unable to create verify token on resend: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_process"})
|
||||
}
|
||||
if a.MailConfigured != nil && a.MailConfigured() && a.AppBaseURL != nil && a.SendEmailVerificationMail != nil {
|
||||
verifyURL := fmt.Sprintf("%s/auth/verify-email?token=%s", a.AppBaseURL(), verifyToken)
|
||||
if err := a.SendEmailVerificationMail(email, verifyURL); err != nil {
|
||||
log.Printf("unable to resend verification email: %v", err)
|
||||
}
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (a *AuthController) ResetPassword(c fiber.Ctx) error {
|
||||
var req resetPasswordRequest
|
||||
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_payload"})
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(req.Token)
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_token"})
|
||||
}
|
||||
|
||||
if req.Password != req.ConfirmPassword {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "password_mismatch"})
|
||||
}
|
||||
|
||||
if !isStrongPassword(req.Password) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "password_too_weak"})
|
||||
}
|
||||
|
||||
var resetTokenRow struct {
|
||||
ID int64 `gorm:"column:id"`
|
||||
UserID int64 `gorm:"column:user_id"`
|
||||
}
|
||||
err := a.DB.Table("password_reset_tokens").
|
||||
Select("id, user_id").
|
||||
Where("token_hash = ? AND used_at IS NULL AND expires_at > ?", hashToken(token), time.Now().Unix()).
|
||||
Order("id DESC").
|
||||
Take(&resetTokenRow).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid_or_expired_token"})
|
||||
}
|
||||
log.Printf("unable to fetch reset token: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
resetID := resetTokenRow.ID
|
||||
userID := resetTokenRow.UserID
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Printf("unable to hash password in reset: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
|
||||
tx := a.DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
|
||||
if err := tx.Table("users").Where("id = ?", userID).Updates(map[string]any{
|
||||
"password_hash": string(passwordHash),
|
||||
"updated_at": gorm.Expr("datetime('now')"),
|
||||
}).Error; err != nil {
|
||||
_ = tx.Rollback().Error
|
||||
log.Printf("unable to update password: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
if err := tx.Table("password_reset_tokens").Where("id = ?", resetID).Update("used_at", time.Now().Unix()).Error; err != nil {
|
||||
_ = tx.Rollback().Error
|
||||
log.Printf("unable to mark reset token used: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
if err := tx.Table("sessions").Where("user_id = ?", userID).Delete(nil).Error; err != nil {
|
||||
_ = tx.Rollback().Error
|
||||
log.Printf("unable to delete sessions after reset: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Printf("unable to commit reset tx: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable_to_reset_password"})
|
||||
}
|
||||
|
||||
clearSessionCookie(c)
|
||||
return c.Status(fiber.StatusOK).JSON(map[string]string{"status": "password_reset"})
|
||||
}
|
||||
|
||||
func (a *AuthController) VerifyEmail(c fiber.Ctx) error {
|
||||
token := strings.TrimSpace(c.Query("token"))
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Invalid verification token.")
|
||||
}
|
||||
|
||||
var verifyTokenRow struct {
|
||||
ID int64 `gorm:"column:id"`
|
||||
UserID int64 `gorm:"column:user_id"`
|
||||
}
|
||||
err := a.DB.Table("email_verification_tokens").
|
||||
Select("id, user_id").
|
||||
Where("token_hash = ? AND used_at IS NULL AND expires_at > ?", hashToken(token), time.Now().Unix()).
|
||||
Order("id DESC").
|
||||
Take(&verifyTokenRow).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Verification link is invalid or expired.")
|
||||
}
|
||||
log.Printf("unable to validate verification token: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Unable to verify email right now.")
|
||||
}
|
||||
tokenID := verifyTokenRow.ID
|
||||
userID := verifyTokenRow.UserID
|
||||
|
||||
tx := a.DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Unable to verify email right now.")
|
||||
}
|
||||
if err := tx.Table("users").Where("id = ?", userID).Updates(map[string]any{
|
||||
"email_verified": 1,
|
||||
"updated_at": gorm.Expr("datetime('now')"),
|
||||
}).Error; err != nil {
|
||||
_ = tx.Rollback().Error
|
||||
log.Printf("unable to mark email verified: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Unable to verify email right now.")
|
||||
}
|
||||
if err := tx.Table("email_verification_tokens").Where("id = ?", tokenID).Update("used_at", time.Now().Unix()).Error; err != nil {
|
||||
_ = tx.Rollback().Error
|
||||
log.Printf("unable to mark verify token used: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Unable to verify email right now.")
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Printf("unable to commit verify email tx: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Unable to verify email right now.")
|
||||
}
|
||||
|
||||
return c.SendString("Email verified successfully. You can now log in.")
|
||||
}
|
||||
|
||||
func setSessionCookie(c fiber.Ctx, value string, expiresAtUnix int64) {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
HTTPOnly: true,
|
||||
Secure: false,
|
||||
SameSite: "Lax",
|
||||
Expires: time.Unix(expiresAtUnix, 0),
|
||||
MaxAge: 60 * 60 * 24 * sessionDurationDays,
|
||||
})
|
||||
}
|
||||
|
||||
func clearSessionCookie(c fiber.Ctx) {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HTTPOnly: true,
|
||||
Secure: false,
|
||||
SameSite: "Lax",
|
||||
Expires: time.Unix(0, 0),
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
func hashToken(value string) string {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func generateSessionToken() (string, error) {
|
||||
raw := make([]byte, 32)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(raw), nil
|
||||
}
|
||||
|
||||
func isStrongPassword(value string) bool {
|
||||
if len(value) < 8 {
|
||||
return false
|
||||
}
|
||||
|
||||
hasLetter := false
|
||||
hasDigit := false
|
||||
for _, r := range value {
|
||||
if unicode.IsLetter(r) {
|
||||
hasLetter = true
|
||||
}
|
||||
if unicode.IsDigit(r) {
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
return hasLetter && hasDigit
|
||||
}
|
||||
|
||||
func createEmailVerificationToken(db *gorm.DB, userID int64) (string, error) {
|
||||
token, err := generateSessionToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expiresAt := time.Now().Add(time.Hour * emailVerificationDurationHours).Unix()
|
||||
err = db.Table("email_verification_tokens").Create(map[string]any{
|
||||
"user_id": userID,
|
||||
"token_hash": hashToken(token),
|
||||
"expires_at": expiresAt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
88
controllers/subscribe_controller.go
Normal file
88
controllers/subscribe_controller.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SubscribeController struct {
|
||||
DB *gorm.DB
|
||||
IsUniqueConstraint func(error) bool
|
||||
}
|
||||
|
||||
type subscribeRequest struct {
|
||||
Email string `json:"email"`
|
||||
BrowserData map[string]any `json:"browserData"`
|
||||
}
|
||||
|
||||
const subscriptionCookieName = "bag_exchange_subscribed"
|
||||
|
||||
func (s *SubscribeController) Subscribe(c fiber.Ctx) error {
|
||||
var req subscribeRequest
|
||||
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid payload"})
|
||||
}
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid email"})
|
||||
}
|
||||
|
||||
emailHash := hashEmail(email)
|
||||
if c.Cookies(subscriptionCookieName) == emailHash {
|
||||
return c.Status(fiber.StatusConflict).JSON(map[string]string{"status": "already_submitted"})
|
||||
}
|
||||
|
||||
browserData := "{}"
|
||||
if len(req.BrowserData) > 0 {
|
||||
payload, err := json.Marshal(req.BrowserData)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(map[string]string{"error": "invalid browser data"})
|
||||
}
|
||||
browserData = string(payload)
|
||||
}
|
||||
|
||||
err := s.DB.Table("subscribers").Create(map[string]any{
|
||||
"email": email,
|
||||
"ip_address": c.IP(),
|
||||
"user_agent": c.Get("User-Agent"),
|
||||
"accept_language": c.Get("Accept-Language"),
|
||||
"browser_data": browserData,
|
||||
}).Error
|
||||
if err != nil {
|
||||
if s.IsUniqueConstraint != nil && s.IsUniqueConstraint(err) {
|
||||
setSubscriptionCookie(c, emailHash)
|
||||
return c.Status(fiber.StatusConflict).JSON(map[string]string{"status": "already_submitted"})
|
||||
}
|
||||
log.Printf("unable to insert subscriber: %v", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(map[string]string{"error": "unable to save subscription"})
|
||||
}
|
||||
|
||||
setSubscriptionCookie(c, emailHash)
|
||||
return c.Status(fiber.StatusCreated).JSON(map[string]string{"status": "subscribed"})
|
||||
}
|
||||
|
||||
func hashEmail(email string) string {
|
||||
sum := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(email))))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func setSubscriptionCookie(c fiber.Ctx, value string) {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: subscriptionCookieName,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
HTTPOnly: false,
|
||||
Secure: false,
|
||||
Expires: time.Now().AddDate(1, 0, 0),
|
||||
MaxAge: 60 * 60 * 24 * 365,
|
||||
})
|
||||
}
|
||||
7
controllers/user_conntroller.go
Normal file
7
controllers/user_conntroller.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type UserConntroller struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
Reference in New Issue
Block a user