diff --git a/.env.sample b/.env.sample index c9eb7bd..b78a8a4 100644 --- a/.env.sample +++ b/.env.sample @@ -26,4 +26,9 @@ R2_ACCOUNT_ID=oui R2_ACCESS_KEY_ID=access_key_id R2_ACCESS_KEY_SECRET=access_key_secret R2_BUCKET_NAME=bucket_name -R2_PUBLIC_DOMAIN=https://your-custom-domain.com \ No newline at end of file +R2_PUBLIC_DOMAIN=https://your-custom-domain.com + +# Whatsapp pour la vérification du numéro de téléphone +WHATSAPP_API_ACCESS_TOKEN=whatsapp_api_access_token +WHATSAPP_BUSINESS_ACCOUNT_ID=whatsapp_business_account_id +WHATSAPP_PHONE_NUMBER_ID=whatsapp_phone_number_id \ No newline at end of file diff --git a/db/migrations/00019_add_phone_verif_whatsapp.sql b/db/migrations/00019_add_phone_verif_whatsapp.sql new file mode 100644 index 0000000..7df3db7 --- /dev/null +++ b/db/migrations/00019_add_phone_verif_whatsapp.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE newf + ADD COLUMN phone_number_verified BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN phone_number_verification_code VARCHAR(6), + ADD COLUMN phone_number_verification_code_expiration TIMESTAMP; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE newf + DROP COLUMN phone_number_verified, + DROP COLUMN phone_number_verification_code, + DROP COLUMN phone_number_verification_code_expiration; +-- +goose StatementEnd diff --git a/go.mod b/go.mod index 6acb04c..951f11e 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.24.2 github.com/robfig/cron/v3 v3.0.1 + github.com/wapikit/wapi.go v0.4.2 golang.org/x/crypto v0.36.0 golang.org/x/text v0.23.0 google.golang.org/api v0.224.0 @@ -49,13 +50,20 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 // indirect github.com/aws/smithy-go v1.22.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/labstack/echo/v4 v4.12.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -66,6 +74,7 @@ require ( github.com/tinylib/msgp v1.2.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.57.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/tcplisten v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect diff --git a/go.sum b/go.sum index 648b8d2..c056250 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/getbrevo/brevo-go v1.1.3 h1:8TYrhhxbfAJLGArlPzCDKzbNfzvjIykBRhTDzLJqmyw= github.com/getbrevo/brevo-go v1.1.3/go.mod h1:ExhytIoPxt/cOBl6ZEMeEZNLUKrWEYA5U3hM/8WP2bg= github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= @@ -69,6 +71,14 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -89,6 +99,12 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -131,8 +147,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/wapikit/wapi.go v0.4.2 h1:6jaa+zX4IESjQmIWi26aDgjF6/VWzGht2DvkV5a5LxU= +github.com/wapikit/wapi.go v0.4.2/go.mod h1:kd2cevBgVL/90JLbhi/dK14T+d2R2T/JnvBA2UcTcgs= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/handlers/user/user_handlers.go b/handlers/user/user_handlers.go index 8388511..3b16a9c 100644 --- a/handlers/user/user_handlers.go +++ b/handlers/user/user_handlers.go @@ -16,15 +16,17 @@ import ( // UserHandler handles user profile and related actions. type UserHandler struct { - DB *sql.DB - NotifService *services.NotificationService // Inject if needed for notification handlers + DB *sql.DB + NotifService *services.NotificationService + PhoneVerificationService *services.PhoneVerificationService } // NewUserHandler creates a new UserHandler. -func NewUserHandler(db *sql.DB, notifService *services.NotificationService) *UserHandler { +func NewUserHandler(db *sql.DB, notifService *services.NotificationService, phoneVerificationService *services.PhoneVerificationService) *UserHandler { return &UserHandler{ - DB: db, - NotifService: notifService, + DB: db, + NotifService: notifService, + PhoneVerificationService: phoneVerificationService, } } @@ -172,8 +174,55 @@ func (h *UserHandler) UpdateNewf(c *fiber.Ctx) error { if req.LastName != "" { updateFields["last_name"] = req.LastName } + // cas spécial: si le numéro de téléphone change, on vérifie le numéro avec Whatsapp if req.PhoneNumber != "" { - // Add validation for phone number format if needed + // il FAUT le préfixe international pour WA + if !strings.HasPrefix(req.PhoneNumber, "+") { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid phone number"}) + } + + var currentPhoneNumber sql.NullString + phoneQuery := `SELECT phone_number FROM newf WHERE email = $1` + err := h.DB.QueryRow(phoneQuery, email).Scan(¤tPhoneNumber) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to fetch current phone number") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + } else if !currentPhoneNumber.Valid || currentPhoneNumber.String != req.PhoneNumber { + utils.LogMessage(utils.LevelInfo, "Phone number changed, sending verification code") + + // faut la langue pour wassap + languageCode, langErr := utils.GetLanguageCode(h.DB, email) + if langErr != nil { + utils.LogMessage(utils.LevelError, "Failed to get language code") + utils.LogLineKeyValue(utils.LevelError, "Error", langErr) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to get language code"}) + } + + verificationCode, err := h.PhoneVerificationService.SendPhoneVerificationCode(req.PhoneNumber, languageCode) + + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to send phone verification code") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + } else { + expirationTime := time.Now().Add(10 * time.Minute) + updateVerifQuery := ` + UPDATE newf + SET phone_number_verification_code = $1, + phone_number_verification_code_expiration = $2, + phone_number_verified = false + WHERE email = $3 + ` + _, err := h.DB.Exec(updateVerifQuery, verificationCode, expirationTime, email) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to update verification code in database") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + } + } + } + + // on update tout de suite (une fois qu'on est sûr que le code est parti), + // mais il ne faut PAS afficher ce numéro dans l'appli car pas encore vérifié updateFields["phone_number"] = req.PhoneNumber } if req.GraduationYear != 0 { @@ -269,6 +318,33 @@ func (h *UserHandler) UpdateNewf(c *fiber.Ctx) error { utils.LogLineKeyValue(utils.LevelInfo, "Fields Updated", updateFields) // Log only keys for brevity/privacy utils.LogFooter() + // Vérifier si une vérification de téléphone est nécessaire + var phoneVerificationRequired bool + var phoneNumber string + if req.PhoneNumber != "" { + // Vérifier si le numéro a changé et n'est pas encore vérifié + verifQuery := ` + SELECT phone_number, phone_number_verified + FROM newf + WHERE email = $1 + ` + err := h.DB.QueryRow(verifQuery, email).Scan(&phoneNumber, &phoneVerificationRequired) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to check phone verification status") + } else { + phoneVerificationRequired = !phoneVerificationRequired + } + } + + // Retourner une réponse différente si une vérification est nécessaire + if phoneVerificationRequired && req.PhoneNumber != "" { + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "message": "Profile updated successfully. Phone number verification required.", + "phone_verification_required": true, + "phone_number": req.PhoneNumber, + }) + } + return c.SendStatus(fiber.StatusOK) } @@ -587,3 +663,168 @@ func (h *UserHandler) SendNotification(c *fiber.Ctx) error { // It needs to be implemented fully to handle parsing, target resolution, etc. return h.NotifService.SendNotification(c) } + +// whatsapp +func (h *UserHandler) VerifyPhoneNumber(c *fiber.Ctx) error { + email := c.Locals("email").(string) + var req struct { + VerificationCode string `json:"verification_code"` + } + + utils.LogHeader("📱 Verifying Phone Number (avec whatsapp)") + utils.LogLineKeyValue(utils.LevelInfo, "User", email) + + if err := c.BodyParser(&req); err != nil { + utils.LogMessage(utils.LevelError, "Failed to parse request body") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + utils.LogFooter() + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request format"}) + } + + if req.VerificationCode == "" { + utils.LogMessage(utils.LevelWarn, "Missing verification code") + utils.LogFooter() + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Verification code is required"}) + } + + var storedCode, expirationTime string + var phoneNumber string + query := ` + SELECT phone_number_verification_code, + phone_number_verification_code_expiration, + phone_number + FROM newf + WHERE email = $1 + ` + + err := h.DB.QueryRow(query, email).Scan(&storedCode, &expirationTime, &phoneNumber) + if err != nil { + if err == sql.ErrNoRows { + utils.LogMessage(utils.LevelWarn, "User not found???") + utils.LogFooter() + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) + } + utils.LogMessage(utils.LevelError, "Failed to fetch verification data") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch verification data"}) + } + + if !h.PhoneVerificationService.ValidateVerificationCode(storedCode, req.VerificationCode, expirationTime) { + utils.LogMessage(utils.LevelWarn, "Invalid or expired verification code") + utils.LogFooter() + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid or expired verification code"}) + } + + // marquer comme verified + updateQuery := ` + UPDATE newf + SET phone_number_verified = true, + phone_number_verification_code = NULL, + phone_number_verification_code_expiration = NULL + WHERE email = $1 + ` + + _, err = h.DB.Exec(updateQuery, email) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to update phone verification status") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify phone number"}) + } + + utils.LogMessage(utils.LevelInfo, "Phone number verified successfully") + utils.LogLineKeyValue(utils.LevelInfo, "Phone", phoneNumber) + utils.LogFooter() + + return c.JSON(fiber.Map{ + "message": "Phone number verified successfully", + "phone_number": phoneNumber, + "verified": true, + }) +} + +func (h *UserHandler) ResendPhoneVerificationCode(c *fiber.Ctx) error { + email := c.Locals("email").(string) + + utils.LogHeader("📱 Resend Phone Verification Code") + utils.LogLineKeyValue(utils.LevelInfo, "User", email) + + // Récupérer le numéro de téléphone et la langue de l'utilisateur, et vérifier s'il est pas déjà vérifié + var phoneNumber string + var verified bool + query := ` + SELECT n.phone_number, n.phone_number_verified + FROM newf n + WHERE n.email = $1 + ` + + err := h.DB.QueryRow(query, email).Scan(&phoneNumber, &verified) + if err != nil { + if err == sql.ErrNoRows { + utils.LogMessage(utils.LevelWarn, "User not found") + utils.LogFooter() + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) + } + utils.LogMessage(utils.LevelError, "Failed to fetch user data") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch user data"}) + } + + if verified { + utils.LogMessage(utils.LevelWarn, "Phone number already verified") + utils.LogFooter() + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Phone number already verified"}) + } + + if phoneNumber == "" { + utils.LogMessage(utils.LevelWarn, "No phone number found for user") + utils.LogFooter() + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No phone number found"}) + } + + languageCode, langErr := utils.GetLanguageCode(h.DB, email) + if langErr != nil { + utils.LogMessage(utils.LevelError, "Failed to get language code") + utils.LogLineKeyValue(utils.LevelError, "Error", langErr) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to get language code"}) + } + + // Envoyer un nouveau code de vérification + verificationCode, err := h.PhoneVerificationService.SendPhoneVerificationCode(phoneNumber, languageCode) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to send verification code") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send verification code"}) + } + + // Mettre à jour le code et l'expiration dans la base de données + expirationTime := time.Now().Add(10 * time.Minute) + updateQuery := ` + UPDATE newf + SET phone_number_verification_code = $1, + phone_number_verification_code_expiration = $2, + phone_number_verified = false + WHERE email = $3 + ` + + _, err = h.DB.Exec(updateQuery, verificationCode, expirationTime, email) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to update verification code in database") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + utils.LogFooter() + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save verification code"}) + } + + utils.LogMessage(utils.LevelInfo, "Verification code resent successfully") + utils.LogLineKeyValue(utils.LevelInfo, "Phone", phoneNumber) + utils.LogFooter() + + return c.JSON(fiber.Map{ + "message": "Verification code sent successfully", + "phone_number": phoneNumber, + }) +} diff --git a/main.go b/main.go index 280a453..8ba64e5 100644 --- a/main.go +++ b/main.go @@ -100,6 +100,14 @@ func main() { eventHandler := event.NewEventHandler(db) + whatsappService, err := services.NewWhatsAppService() + if err != nil { + log.Fatalf("💥 Failed to create WhatsApp Service: %v", err) + } + + // Initialize Phone Verification Service + phoneVerificationService := services.NewPhoneVerificationService(whatsappService) + appScheduler := scheduler.NewScheduler(restHandler) appScheduler.StartAll() defer appScheduler.StopAll() @@ -156,7 +164,7 @@ func main() { // API Group --- NEW ROUTES routes.SetupAuthRoutes(app, db, jwtSecret, notificationService, emailService, discordService) - routes.SetupUserRoutes(app, db, notificationService) + routes.SetupUserRoutes(app, db, notificationService, phoneVerificationService) routes.SetupTraqRoutes(app, db) routes.SetupFileRoutes(app, db, r2Service) routes.SetupRestaurantRoutes(app, restHandler) diff --git a/models/user.go b/models/user.go index 8a86784..7601d66 100644 --- a/models/user.go +++ b/models/user.go @@ -8,17 +8,20 @@ type Newf struct { NewPasswordConfirmation string `json:"new_password_confirmation"` PasswordUpdatedDate string `json:"password_updated_date"` VerificationCodeData - CreationDate string `json:"creation_date"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - PhoneNumber string `json:"phone_number"` - ProfilePicture string `json:"profile_picture"` - NotificationToken string `json:"notification_token"` - GraduationYear int `json:"graduation_year"` - FormationName string `json:"formation_name"` - Campus string `json:"campus"` - TotalUsers int `json:"total_newf"` - Language string `json:"language"` + CreationDate string `json:"creation_date"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + PhoneNumber string `json:"phone_number"` + PhoneNumberVerified bool `json:"phone_number_verified"` + PhoneNumberVerificationCode string `json:"phone_number_verification_code"` + PhoneNumberVerificationCodeExpiration string `json:"phone_number_verification_code_expiration"` + ProfilePicture string `json:"profile_picture"` + NotificationToken string `json:"notification_token"` + GraduationYear int `json:"graduation_year"` + FormationName string `json:"formation_name"` + Campus string `json:"campus"` + TotalUsers int `json:"total_newf"` + Language string `json:"language"` } // VerificationCodeData holds verification code details. diff --git a/routes/user.go b/routes/user.go index 946eabb..0d65432 100644 --- a/routes/user.go +++ b/routes/user.go @@ -12,29 +12,33 @@ import ( ) // SetupUserRoutes configures user profile and related routes. -func SetupUserRoutes(router fiber.Router, db *sql.DB, notifService *services.NotificationService) { +func SetupUserRoutes(router fiber.Router, db *sql.DB, notifService *services.NotificationService, phoneVerificationService *services.PhoneVerificationService) { // Initialize User Handler with dependencies - userHandler := user.NewUserHandler(db, notifService) + userHandler := user.NewUserHandler(db, notifService, phoneVerificationService) // Group routes that require JWT authentication // Changed group name from "/newf" to "/user" for clarity userGroup := router.Group("/newf", middlewares.JWTMiddleware, utils.EnhanceSentryEventWithEmail) // Profile routes - userGroup.Get("/me", userHandler.GetNewf) // GET /api/user/me - userGroup.Patch("/me", userHandler.UpdateNewf) // PATCH /api/user/me - userGroup.Delete("/me", userHandler.DeleteNewf) // DELETE /api/user/me (Use with caution!) + userGroup.Get("/me", userHandler.GetNewf) // GET /user/me + userGroup.Patch("/me", userHandler.UpdateNewf) // PATCH /user/me + userGroup.Delete("/me", userHandler.DeleteNewf) // DELETE /user/me (Use with caution!) // Notification Preferences routes (within the authenticated user group) // Combine add/remove into one endpoint toggling state - userGroup.Post("/notifications/subscriptions", userHandler.AddOrRemoveNotificationSubscription) // POST /api/user/notifications/subscriptions expects {"service": "..."} + userGroup.Post("/notifications/subscriptions", userHandler.AddOrRemoveNotificationSubscription) // POST /user/notifications/subscriptions expects {"service": "..."} // Endpoint to get subscription status (all or specific) // Use GET with optional body - userGroup.Get("/notifications/subscriptions", userHandler.GetNotificationSubscriptions) // GET /api/user/notifications/subscriptions (body optional for specific check) + userGroup.Get("/notifications/subscriptions", userHandler.GetNotificationSubscriptions) // GET /user/notifications/subscriptions (body optional for specific check) // Route to *trigger* sending a notification (might require admin) // This was previously POST /newf/send-notification // Kept under /user for now, but permissions must be checked in the handler. // Consider moving to /admin or /notifications group later. - userGroup.Post("/send-notification", userHandler.SendNotification) // POST /api/user/send-notification (Requires Permissions!) + userGroup.Post("/send-notification", userHandler.SendNotification) // POST /user/send-notification (Requires Permissions!) + + // code whatsapp + userGroup.Post("/phone/verify", userHandler.VerifyPhoneNumber) // POST /newf/phone/verify + userGroup.Post("/phone/resend", userHandler.ResendPhoneVerificationCode) // POST /newf/phone/resend } diff --git a/services/phone_verification.go b/services/phone_verification.go new file mode 100644 index 0000000..ed01fe7 --- /dev/null +++ b/services/phone_verification.go @@ -0,0 +1,61 @@ +package services + +import ( + "fmt" + "time" + + "github.com/plugimt/transat-backend/utils" +) + +type PhoneVerificationService struct { + whatsappService *WhatsAppService +} + +func NewPhoneVerificationService(whatsappService *WhatsAppService) *PhoneVerificationService { + return &PhoneVerificationService{ + whatsappService: whatsappService, + } +} + +func (s *PhoneVerificationService) SendPhoneVerificationCode(phoneNumber, language string) (string, error) { + verificationCode := utils.Generate2FACode(6) + + err := s.whatsappService.SendVerificationCode(phoneNumber, verificationCode, language) + if err != nil { + return "", fmt.Errorf("failed to send WhatsApp verification code: %w", err) + } + + utils.LogMessage(utils.LevelInfo, "Phone verification code sent successfully") + utils.LogLineKeyValue(utils.LevelInfo, "Phone", phoneNumber) + utils.LogLineKeyValue(utils.LevelInfo, "Code", verificationCode) + + return verificationCode, nil +} + +func (s *PhoneVerificationService) IsVerificationCodeExpired(expirationTime string) bool { + expiration, err := time.Parse(time.RFC3339, expirationTime) + if err != nil { + utils.LogMessage(utils.LevelError, "Failed to parse expiration time") + utils.LogLineKeyValue(utils.LevelError, "Error", err) + return true // je vois pas comment ça pourrait arriver (null?), mais dans le doute on refuse + } + + return time.Now().After(expiration) +} + +func (s *PhoneVerificationService) ValidateVerificationCode(storedCode, providedCode, expirationTime string) bool { + if s.IsVerificationCodeExpired(expirationTime) { + utils.LogMessage(utils.LevelWarn, "Verification code expired") + return false + } + + if storedCode != providedCode { + utils.LogMessage(utils.LevelWarn, "Invalid verification code") + utils.LogLineKeyValue(utils.LevelWarn, "Stored", storedCode) + utils.LogLineKeyValue(utils.LevelWarn, "Provided", providedCode) + return false + } + + utils.LogMessage(utils.LevelInfo, "Verification code validated successfully") + return true +} diff --git a/services/whatsapp.go b/services/whatsapp.go new file mode 100644 index 0000000..d6c483d --- /dev/null +++ b/services/whatsapp.go @@ -0,0 +1,92 @@ +package services + +import ( + "fmt" + "os" + + "github.com/plugimt/transat-backend/utils" + "github.com/wapikit/wapi.go/manager" + wapi "github.com/wapikit/wapi.go/pkg/client" + "github.com/wapikit/wapi.go/pkg/components" + "github.com/wapikit/wapi.go/pkg/messaging" +) + +type WhatsAppService struct { + client *wapi.Client + messageClient *messaging.MessagingClient +} + +func NewWhatsAppService() (*WhatsAppService, error) { + apiAccessToken := os.Getenv("WHATSAPP_API_ACCESS_TOKEN") + businessAccountId := os.Getenv("WHATSAPP_BUSINESS_ACCOUNT_ID") + phoneNumberId := os.Getenv("WHATSAPP_PHONE_NUMBER_ID") + + if apiAccessToken == "" || businessAccountId == "" || phoneNumberId == "" { + return nil, fmt.Errorf("missing WhatsApp env vars: WHATSAPP_API_ACCESS_TOKEN, WHATSAPP_BUSINESS_ACCOUNT_ID, or WHATSAPP_PHONE_NUMBER_ID not set") + } + + client := wapi.New(&wapi.ClientConfig{ + ApiAccessToken: apiAccessToken, + BusinessAccountId: businessAccountId, + // si besoin de gérer les réponses, on peut rajouter un truc pour faire un callback, mais flemme + }) + + messageClient := client.NewMessagingClient(phoneNumberId) + + return &WhatsAppService{ + client: client, + messageClient: messageClient, + }, nil +} + +func (s *WhatsAppService) SendTemplateMessage(templateName, phoneNumber string, language string, templateComponents []components.TemplateMessageComponent) (*manager.MessageSendResponse, error) { + templateMsg := &components.TemplateMessage{ + Name: templateName, + Language: components.TemplateMessageLanguage{ + Code: language, + Policy: "deterministic", + }, + Components: templateComponents, + } + + res, err := s.messageClient.Message.Send(templateMsg, phoneNumber) + if err != nil { + return nil, err + } + + return res, nil +} + +func (s *WhatsAppService) SendVerificationCode(phoneNumber, code string, language string) error { + res, err := s.SendTemplateMessage("verify_code", phoneNumber, language, []components.TemplateMessageComponent{ + components.TemplateMessageComponentBodyType{ + Type: components.TemplateMessageComponentTypeBody, + Parameters: []components.TemplateMessageParameter{ + components.TemplateMessageBodyAndHeaderParameter{ + Type: "text", + Text: &code, + }, + }, + }, + components.TemplateMessageComponentButtonType{ + Type: components.TemplateMessageComponentTypeButton, + SubType: components.TemplateMessageButtonComponentTypeUrl, + Index: 0, + Parameters: &[]components.TemplateMessageParameter{ + components.TemplateMessageBodyAndHeaderParameter{ + Type: "text", + Text: &code, + }, + }, + }, + }) + + if err != nil { + return err + } + + utils.LogMessage(utils.LevelInfo, "Message sent successfully") + utils.LogLineKeyValue(utils.LevelInfo, "Message", res.Messages[0].ID) + + return nil +}