package handlers import ( "fmt" "strings" "time" "server/internal/auth" "server/internal/mail" "server/internal/models" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) func normalizeEmail(email string) string { return strings.ToLower(strings.TrimSpace(email)) } func staffPublic(a models.StaffAccount) fiber.Map { return fiber.Map{ "id": a.ID, "email": a.Email, "fullName": a.FullName, "mustChangePassword": a.MustChangePassword, } } // POST /api/auth/provision — lần đầu: gửi mật khẩu qua email nếu đuôi hợp lệ func ProvisionStaffHandler(db *gorm.DB, mailer mail.Config) fiber.Handler { return func(c *fiber.Ctx) error { var req struct { Email string `json:"email"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } email := normalizeEmail(req.Email) if email == "" || !strings.Contains(email, "@") { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Email không hợp lệ"}) } if !auth.IsEmailDomainAllowed(db, email) { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Đuôi email không được phép truy cập hệ thống"}) } var existing models.StaffAccount err := db.Where("email = ?", email).First(&existing).Error if err == nil { if !existing.IsActive { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Tài khoản đã bị khóa"}) } return c.JSON(fiber.Map{"ok": true, "message": "Tài khoản đã tồn tại. Dùng đăng nhập hoặc quên mật khẩu."}) } if err != gorm.ErrRecordNotFound { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } tempPass := auth.RandomPassword(12) hash, err := auth.HashPassword(tempPass) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } staff := models.StaffAccount{ Email: email, PasswordHash: hash, MustChangePassword: true, IsActive: true, } if err := db.Create(&staff).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } body := fmt.Sprintf(`

Xin chào,

Tài khoản Simple Care Management đã được tạo.

Email: %s
Mật khẩu tạm: %s

Đăng nhập và đổi mật khẩu ngay lần đầu.

`, email, tempPass) if err := mailer.Send(email, "Simple Care — Mật khẩu truy cập", body); err != nil { return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": "Tạo tài khoản ok nhưng gửi mail thất bại: " + err.Error()}) } return c.JSON(fiber.Map{"ok": true, "message": "Đã gửi mật khẩu tạm tới email của bạn"}) } } // POST /api/auth/login func LoginStaffHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var req struct { Email string `json:"email"` Password string `json:"password"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } email := normalizeEmail(req.Email) var staff models.StaffAccount if err := db.Where("email = ?", email).First(&staff).Error; err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Email hoặc mật khẩu không đúng"}) } if !staff.IsActive { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Tài khoản đã bị khóa"}) } if !auth.CheckPassword(staff.PasswordHash, req.Password) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Email hoặc mật khẩu không đúng"}) } token, err := auth.IssueToken(&staff) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "token": token, "mustChangePassword": staff.MustChangePassword, "staff": staffPublic(staff), }) } } // GET /api/auth/me func MeStaffHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { staffID, ok := c.Locals("staffId").(uint) if !ok || staffID == 0 { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) } var staff models.StaffAccount if err := db.First(&staff, staffID).Error; err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Account not found"}) } return c.JSON(fiber.Map{"staff": staffPublic(staff)}) } } // POST /api/auth/change-password func ChangePasswordHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { staffID, ok := c.Locals("staffId").(uint) if !ok || staffID == 0 { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) } var req struct { OldPassword string `json:"oldPassword"` NewPassword string `json:"newPassword"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } if len(req.NewPassword) < 8 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Mật khẩu mới tối thiểu 8 ký tự"}) } var staff models.StaffAccount if err := db.First(&staff, staffID).Error; err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Account not found"}) } if !auth.CheckPassword(staff.PasswordHash, req.OldPassword) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Mật khẩu hiện tại không đúng"}) } hash, err := auth.HashPassword(req.NewPassword) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } staff.PasswordHash = hash staff.MustChangePassword = false if err := db.Save(&staff).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } token, _ := auth.IssueToken(&staff) return c.JSON(fiber.Map{"ok": true, "token": token, "staff": staffPublic(staff)}) } } // POST /api/auth/forgot-password func ForgotPasswordHandler(db *gorm.DB, mailer mail.Config) fiber.Handler { return func(c *fiber.Ctx) error { var req struct { Email string `json:"email"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } email := normalizeEmail(req.Email) var staff models.StaffAccount if err := db.Where("email = ?", email).First(&staff).Error; err != nil { return c.JSON(fiber.Map{"ok": true, "message": "Nếu email tồn tại, link đặt lại mật khẩu đã được gửi"}) } token := auth.RandomToken(24) row := models.PasswordResetToken{ StaffID: staff.ID, Token: token, ExpiresAt: time.Now().Add(2 * time.Hour), } if err := db.Create(&row).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } body := fmt.Sprintf(`

Đặt lại mật khẩu Simple Care.

Mã đặt lại (hiệu lực 2 giờ): %s

Nhập mã này trên màn hình đặt lại mật khẩu.

`, token) _ = mailer.Send(email, "Simple Care — Đặt lại mật khẩu", body) return c.JSON(fiber.Map{"ok": true, "message": "Nếu email tồn tại, mã đặt lại đã được gửi"}) } } // POST /api/auth/reset-password func ResetPasswordHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var req struct { Token string `json:"token"` NewPassword string `json:"newPassword"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } if len(req.NewPassword) < 8 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Mật khẩu mới tối thiểu 8 ký tự"}) } var row models.PasswordResetToken if err := db.Where("token = ? AND used_at IS NULL AND expires_at > ?", req.Token, time.Now()).First(&row).Error; err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Mã không hợp lệ hoặc đã hết hạn"}) } var staff models.StaffAccount if err := db.First(&staff, row.StaffID).Error; err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Tài khoản không tồn tại"}) } hash, err := auth.HashPassword(req.NewPassword) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } now := time.Now() staff.PasswordHash = hash staff.MustChangePassword = false row.UsedAt = &now if err := db.Save(&staff).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } _ = db.Save(&row) return c.JSON(fiber.Map{"ok": true, "message": "Đã đặt lại mật khẩu. Vui lòng đăng nhập."}) } } // GET /api/admin/email-domains func ListEmailDomainsHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var rows []models.EmailDomain db.Order("domain asc").Find(&rows) return c.JSON(fiber.Map{"data": rows}) } } // POST /api/admin/email-domains func AddEmailDomainHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var req struct { Domain string `json:"domain"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"}) } domain := strings.ToLower(strings.TrimSpace(req.Domain)) if domain == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "domain required"}) } row := models.EmailDomain{Domain: domain, IsActive: true} if err := db.Where("domain = ?", domain).FirstOrCreate(&row).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true, "data": row}) } } // DELETE /api/admin/email-domains/:id func DeleteEmailDomainHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { id := c.Params("id") if err := db.Delete(&models.EmailDomain{}, id).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } } // GET /api/admin/staff — danh sách tài khoản func ListStaffHandler(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var rows []models.StaffAccount db.Order("email asc").Find(&rows) out := make([]fiber.Map, 0, len(rows)) for _, r := range rows { out = append(out, staffPublic(r)) } return c.JSON(fiber.Map{"data": out}) } }