tam chat
This commit is contained in:
289
server/internal/handlers/handlers_auth.go
Normal file
289
server/internal/handlers/handlers_auth.go
Normal file
@@ -0,0 +1,289 @@
|
||||
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(`<p>Xin chào,</p><p>Tài khoản Simple Care Management đã được tạo.</p>
|
||||
<p><strong>Email:</strong> %s<br/><strong>Mật khẩu tạm:</strong> %s</p>
|
||||
<p>Đăng nhập và đổi mật khẩu ngay lần đầu.</p>`, 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(`<p>Đặt lại mật khẩu Simple Care.</p>
|
||||
<p>Mã đặt lại (hiệu lực 2 giờ): <strong>%s</strong></p>
|
||||
<p>Nhập mã này trên màn hình đặt lại mật khẩu.</p>`, 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})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user