Dev / controllers /auth_controller.go
Mhamdans17
feat: single session enforcement + UI improvements
c6e89e6
Raw
History Blame Contribute Delete
23.7 kB
package controllers
import (
crand "crypto/rand"
"encoding/hex"
"fmt"
"log"
"math/rand"
"net/http"
"strconv"
"time"
"service-warungpos-go/config"
"service-warungpos-go/middleware"
"service-warungpos-go/models"
"service-warungpos-go/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// SeedDefaultAdmin inisialisasi default super admin jika belum ada
func SeedDefaultAdmin() {
var count int64
config.DB.Model(&models.User{}).Where("email = ?", "admin@xogmgo.com").Count(&count)
if count == 0 {
hashedPassword, _ := middleware.HashPassword("admin123")
admin := models.User{
Name: "Admin XOGM Go",
Email: "admin@xogmgo.com",
Password: hashedPassword,
Role: "super_admin",
CompanyID: nil,
BranchID: nil,
IsActive: true,
}
if err := config.DB.Create(&admin).Error; err != nil {
log.Printf("[SEED] Gagal membuat default admin: %v", err)
} else {
log.Println("[SEED] Sukses membuat default admin!")
}
} else {
log.Println("[SEED] Admin default sudah terbuat.")
}
}
// SeedDefaultSetting inisialisasi setting bawaan di database dari konfigurasi .env
func SeedDefaultSetting() {
var count int64
config.DB.Model(&models.Setting{}).Count(&count)
if count == 0 {
setting := models.Setting{
ID: 1,
TripayMerchantCode: config.GlobalConfig.TripayMerchantCode,
TripayAPIKey: config.GlobalConfig.TripayAPIKey,
TripayPrivateKey: config.GlobalConfig.TripayPrivateKey,
TripayProxyURL: config.GlobalConfig.TripayProxyURL,
TripayPaymentMethod: "QRISC",
}
if err := config.DB.Create(&setting).Error; err != nil {
log.Printf("[SEED] Gagal membuat default setting: %v", err)
} else {
log.Println("[SEED] Sukses membuat default setting dari file .env!")
}
} else {
log.Println("[SEED] Setting default sudah terbuat di DB.")
}
}
// Request & Response DTOs
type LoginRequest struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required"`
}
type UserResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
Role string `json:"role"`
CompanyID *uint `json:"company_id"`
BranchID *uint `json:"branch_id"`
IsActive bool `json:"is_active"`
CompanyName string `json:"company_name,omitempty"`
BranchName string `json:"branch_name,omitempty"`
CompanySettings map[string]interface{} `json:"company_settings,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
User UserResponse `json:"user"`
}
type RegisterRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
Role string `json:"role"`
CompanyID *uint `json:"company_id"`
BranchID *uint `json:"branch_id"`
}
type TenantRegistrationRequest struct {
CompanyName string `json:"company_name" binding:"required"`
CompanyAddress string `json:"company_address"`
NPWP string `json:"npwp"`
OwnerName string `json:"owner_name" binding:"required"`
OwnerEmail string `json:"owner_email" binding:"required,email"`
OwnerPhone string `json:"owner_phone" binding:"required"`
OwnerPassword string `json:"owner_password" binding:"required"`
BankAccountNumber string `json:"bank_account_number"`
BankName string `json:"bank_name"`
}
type VerifyOTPRequest struct {
Phone string `json:"phone" binding:"required"`
OTPCode string `json:"otp_code" binding:"required"`
}
type ResendOTPRequest struct {
Phone string `json:"phone" binding:"required"`
}
type ForgotPasswordRequest struct {
Phone string `json:"phone" binding:"required"`
}
type ResetPasswordRequest struct {
Phone string `json:"phone" binding:"required"`
OTPCode string `json:"otp_code" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
type UpdateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
IsActive *bool `json:"is_active"`
}
type UpdateKasirRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
BranchID *uint `json:"branch_id"`
IsActive *bool `json:"is_active"`
}
func toUserResponse(user *models.User) UserResponse {
resp := UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Phone: user.Phone,
Role: user.Role,
CompanyID: user.CompanyID,
BranchID: user.BranchID,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
}
if user.Company != nil {
resp.CompanyName = user.Company.Name
resp.CompanySettings = map[string]interface{}{
"enable_member": user.Company.EnableMember,
"enable_voucher": user.Company.EnableVoucher,
"enable_multi_branch": user.Company.EnableMultiBranch,
"enable_multi_kasir": user.Company.EnableMultiKasir,
}
}
if user.Branch != nil {
resp.BranchName = user.Branch.Name
}
return resp
}
// Route Handlers
func Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Data tidak valid"})
return
}
var user models.User
if err := config.DB.Select("id, name, email, password, phone, role, company_id, branch_id, is_active, created_at").
Preload("Company", func(db *gorm.DB) *gorm.DB {
return db.Select("id, name, approved_at, is_active, enable_member, enable_voucher, enable_multi_branch, enable_multi_kasir")
}).
Preload("Branch", func(db *gorm.DB) *gorm.DB {
return db.Select("id, name")
}).
Where("email = ?", req.Email).
First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Email atau password salah"})
return
}
if !middleware.VerifyPassword(req.Password, user.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Email atau password salah"})
return
}
// Cek status keaktifan user
if !user.IsActive {
if user.Role == "owner" && user.Company != nil {
if user.Company.ApprovedAt != nil {
// Pernah diapprove tapi sekarang dinonaktifkan
c.JSON(http.StatusUnauthorized, gin.H{"detail": "ACCOUNT_DEACTIVATED"})
return
} else {
// Belum diapprove super_admin
c.JSON(http.StatusUnauthorized, gin.H{"detail": "PENDING_APPROVAL"})
return
}
}
c.JSON(http.StatusForbidden, gin.H{"detail": "Akun tidak aktif, hubungi admin"})
return
}
// --- Single Session Enforcement ---
var sessionTokenStr string
if user.Role == "kasir" && user.CompanyID != nil {
// Ambil company settings (termasuk enforce_single_session)
var company models.Company
if err := config.DB.Select("id, enforce_single_session").First(&company, *user.CompanyID).Error; err == nil {
if company.EnforceSingleSession {
// Generate secure session token
tokenBytes := make([]byte, 32)
crand.Read(tokenBytes)
sessionTokenStr = hex.EncodeToString(tokenBytes)
// Hapus semua session lama milik user ini
config.DB.Where("user_id = ?", user.ID).Delete(&models.UserSession{})
// Buat session baru
newSession := models.UserSession{
UserID: user.ID,
SessionToken: sessionTokenStr,
UserAgent: c.GetHeader("User-Agent"),
IPAddress: c.ClientIP(),
ExpiresAt: time.Now().Add(24 * time.Hour),
}
config.DB.Create(&newSession)
}
}
}
token, err := middleware.CreateAccessToken(user.ID, user.Role, user.CompanyID, sessionTokenStr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal men-generate token"})
return
}
c.JSON(http.StatusOK, TokenResponse{
AccessToken: token,
TokenType: "bearer",
User: toUserResponse(&user),
})
}
func Logout(c *gin.Context) {
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
return
}
user := userVal.(*models.User)
// Hapus session dari DB (jika ada)
config.DB.Where("user_id = ?", user.ID).Delete(&models.UserSession{})
c.JSON(http.StatusOK, gin.H{"message": "Berhasil logout"})
}
func Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var existingCount int64
config.DB.Model(&models.User{}).Where("email = ?", req.Email).Count(&existingCount)
if existingCount > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Email sudah terdaftar"})
return
}
hashedPassword, _ := middleware.HashPassword(req.Password)
role := "kasir"
if req.Role != "" {
role = req.Role
}
newUser := models.User{
Name: req.Name,
Email: req.Email,
Password: hashedPassword,
Role: role,
CompanyID: req.CompanyID,
BranchID: req.BranchID,
IsActive: true,
}
if err := config.DB.Create(&newUser).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan user"})
return
}
c.JSON(http.StatusCreated, toUserResponse(&newUser))
}
func RegisterTenant(c *gin.Context) {
var req TenantRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var existingCount int64
config.DB.Model(&models.User{}).Where("email = ?", req.OwnerEmail).Count(&existingCount)
if existingCount > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Email sudah digunakan!"})
return
}
tx := config.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 1. Buat Company
comp := models.Company{
Name: req.CompanyName,
Address: req.CompanyAddress,
NPWP: req.NPWP,
BankAccountNumber: req.BankAccountNumber,
BankName: req.BankName,
Email: req.OwnerEmail,
Phone: req.OwnerPhone,
IsActive: false,
}
if err := tx.Create(&comp).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal membuat perusahaan"})
return
}
// 2. Buat Owner User
hashedPassword, _ := middleware.HashPassword(req.OwnerPassword)
owner := models.User{
Name: req.OwnerName,
Email: req.OwnerEmail,
Password: hashedPassword,
Phone: req.OwnerPhone,
Role: "owner",
CompanyID: &comp.ID,
IsActive: false,
}
if err := tx.Create(&owner).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal membuat akun owner"})
return
}
// 3. Buat OTP Entry
code := fmt.Sprintf("%06d", rand.Intn(900000)+100000)
otpEntry := models.OTP{
PhoneNumber: req.OwnerPhone,
OTPCode: code,
IsVerified: false,
ExpiresAt: time.Now().Add(5 * time.Minute),
}
if err := tx.Create(&otpEntry).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal membuat OTP"})
return
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal melakukan registrasi"})
return
}
// Kirim Notifikasi OTP via WA secara async agar request HTTP cepat
go services.SendOtpWa(req.OwnerPhone, code)
// Kirim Notifikasi ke Admin (085789599065) ada pendaftar baru
go services.SendNewRegistrationAdminWa("085789599065", req.CompanyName, req.OwnerName, req.OwnerEmail, req.OwnerPhone)
c.JSON(http.StatusCreated, gin.H{
"status": "success",
"message": "OTP sent to phone",
})
}
func VerifyOTP(c *gin.Context) {
var req VerifyOTPRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var record models.OTP
if err := config.DB.Where("phone_number = ? AND otp_code = ? AND is_verified = ?", req.Phone, req.OTPCode, false).Order("id desc").First(&record).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Kode OTP salah atau tidak ditemukan"})
return
}
if record.ExpiresAt.Before(time.Now()) {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Kode OTP sudah kadaluarsa"})
return
}
record.IsVerified = true
config.DB.Save(&record)
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Nomor Hape diverifikasi! Tunggu Approve Super Admin.",
})
}
func ResendOTP(c *gin.Context) {
var req ResendOTPRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var lastOTP models.OTP
err := config.DB.Where("phone_number = ? AND is_verified = ?", req.Phone, false).Order("id desc").First(&lastOTP).Error
if err == nil {
// Cooldown 3 menit (ExpiresAt - 5 menit = CreatedAt)
createdAt := lastOTP.ExpiresAt.Add(-5 * time.Minute)
timeSince := time.Since(createdAt)
if timeSince < 3*time.Minute {
remaining := int(180 - timeSince.Seconds())
c.JSON(http.StatusTooManyRequests, gin.H{
"detail": fmt.Sprintf("Tunggu %d detik lagi sebelum kirim ulang OTP", remaining),
})
return
}
}
code := fmt.Sprintf("%06d", rand.Intn(900000)+100000)
otpEntry := models.OTP{
PhoneNumber: req.Phone,
OTPCode: code,
IsVerified: false,
ExpiresAt: time.Now().Add(5 * time.Minute),
}
if err := config.DB.Create(&otpEntry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengirim ulang OTP"})
return
}
go services.SendOtpWa(req.Phone, code)
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "OTP baru telah dikirim ke WhatsApp Anda",
})
}
func ForgotPassword(c *gin.Context) {
var req ForgotPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// Cari user berdasarkan nomor telepon (OwnerPhone atau Phone)
var user models.User
if err := config.DB.Where("phone = ?", req.Phone).First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Nomor WhatsApp tidak terdaftar"})
return
}
// Generate OTP 6 digit
code := fmt.Sprintf("%06d", rand.Intn(900000)+100000)
// Simpan ke database otps
otpEntry := models.OTP{
PhoneNumber: req.Phone,
OTPCode: code,
IsVerified: false,
ExpiresAt: time.Now().Add(10 * time.Minute), // Kadaluarsa dalam 10 menit
}
if err := config.DB.Create(&otpEntry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal membuat OTP reset password"})
return
}
// Kirim Notifikasi OTP Reset Password via WA
go services.SendResetPasswordOtpWa(req.Phone, code)
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Kode OTP reset password telah dikirim ke WhatsApp Anda",
})
}
func ResetPassword(c *gin.Context) {
var req ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// Validasi OTP
var record models.OTP
if err := config.DB.Where("phone_number = ? AND otp_code = ? AND is_verified = ?", req.Phone, req.OTPCode, false).Order("id desc").First(&record).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Kode OTP salah atau tidak ditemukan"})
return
}
if record.ExpiresAt.Before(time.Now()) {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Kode OTP sudah kadaluarsa"})
return
}
// Temukan user
var user models.User
if err := config.DB.Where("phone = ?", req.Phone).First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "User tidak ditemukan"})
return
}
// Hash password baru
hashed, err := middleware.HashPassword(req.NewPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal enkripsi password"})
return
}
// Mulai Transaksi Database untuk memastikan data konsisten
tx := config.DB.Begin()
// Simpan password baru ke user
if err := tx.Model(&user).Update("password", hashed).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mereset password"})
return
}
// Tandai OTP telah digunakan
record.IsVerified = true
if err := tx.Save(&record).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal memperbarui status OTP"})
return
}
tx.Commit()
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "Password Anda berhasil diperbarui. Silakan login kembali dengan password baru Anda.",
})
}
func GetMe(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
resp := toUserResponse(user)
// Attach Company & Branch name jika ada
if user.CompanyID != nil {
var company models.Company
if err := config.DB.First(&company, *user.CompanyID).Error; err == nil {
resp.CompanyName = company.Name
resp.CompanySettings = map[string]interface{}{
"enable_member": company.EnableMember,
"enable_voucher": company.EnableVoucher,
"enable_multi_branch": company.EnableMultiBranch,
"enable_multi_kasir": company.EnableMultiKasir,
}
}
}
if user.BranchID != nil {
var branch models.Branch
if err := config.DB.Select("id, name").First(&branch, *user.BranchID).Error; err == nil {
resp.BranchName = branch.Name
}
}
c.JSON(http.StatusOK, resp)
}
func ListUsers(c *gin.Context) {
limitStr := c.Query("limit")
skipStr := c.Query("skip")
searchQuery := c.Query("search")
query := config.DB.Preload("Company").Preload("Branch")
if searchQuery != "" {
query = query.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ?",
"%"+searchQuery+"%",
"%"+searchQuery+"%",
"%"+searchQuery+"%")
}
if limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil {
query = query.Limit(limit)
}
}
if skipStr != "" {
if skip, err := strconv.Atoi(skipStr); err == nil {
query = query.Offset(skip)
}
}
var users []models.User
if err := query.Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data user"})
return
}
var resp []UserResponse
for _, u := range users {
resp = append(resp, toUserResponse(&u))
}
c.JSON(http.StatusOK, resp)
}
func UpdateUser(c *gin.Context) {
userIDStr := c.Param("user_id")
userID, err := strconv.Atoi(userIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID user tidak valid"})
return
}
var req UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var user models.User
if err := config.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "User tidak ditemukan"})
return
}
if req.Name != "" {
user.Name = req.Name
}
if req.Email != "" {
var count int64
config.DB.Model(&models.User{}).Where("email = ? AND id != ?", req.Email, userID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Email sudah digunakan user lain"})
return
}
user.Email = req.Email
}
if req.Password != "" {
hashed, _ := middleware.HashPassword(req.Password)
user.Password = hashed
}
if req.Role != "" {
user.Role = req.Role
}
if req.IsActive != nil {
user.IsActive = *req.IsActive
}
if err := config.DB.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update user"})
return
}
c.JSON(http.StatusOK, toUserResponse(&user))
}
// Owner endpoints for Kasir Management
func ListKasir(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
var kasir []models.User
if err := config.DB.Select("id, name, email, phone, role, company_id, branch_id, is_active, created_at").Preload("Branch").Where("company_id = ? AND role = ?", *owner.CompanyID, "kasir").Find(&kasir).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data kasir"})
return
}
resp := []UserResponse{}
for _, k := range kasir {
resp = append(resp, toUserResponse(&k))
}
c.JSON(http.StatusOK, resp)
}
func CreateKasir(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
if req.Role != "" && req.Role != "kasir" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Owner hanya bisa membuat role 'kasir'"})
return
}
var existingCount int64
config.DB.Model(&models.User{}).Where("email = ?", req.Email).Count(&existingCount)
if existingCount > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Email sudah terdaftar"})
return
}
var company models.Company
if err := config.DB.First(&company, *owner.CompanyID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data perusahaan"})
return
}
if !company.EnableMultiKasir && company.MaxCashiers <= 0 {
var kasirCount int64
config.DB.Model(&models.User{}).Where("company_id = ? AND role = ?", *owner.CompanyID, "kasir").Count(&kasirCount)
if kasirCount >= 1 {
c.JSON(http.StatusForbidden, gin.H{"detail": "Paket perusahaan Anda tidak mendukung pembuatan lebih dari 1 kasir"})
return
}
} else if company.MaxCashiers > 0 {
var kasirCount int64
config.DB.Model(&models.User{}).Where("company_id = ? AND role = ?", *owner.CompanyID, "kasir").Count(&kasirCount)
if kasirCount >= int64(company.MaxCashiers) {
c.JSON(http.StatusForbidden, gin.H{"detail": fmt.Sprintf("Batas maksimal kasir (%d) telah tercapai untuk perusahaan Anda", company.MaxCashiers)})
return
}
}
hashedPassword, _ := middleware.HashPassword(req.Password)
newUser := models.User{
Name: req.Name,
Email: req.Email,
Password: hashedPassword,
Role: "kasir",
CompanyID: owner.CompanyID,
BranchID: req.BranchID,
IsActive: true,
}
if err := config.DB.Create(&newUser).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan kasir"})
return
}
c.JSON(http.StatusCreated, toUserResponse(&newUser))
}
func UpdateKasir(c *gin.Context) {
userVal, _ := c.Get("user")
owner := userVal.(*models.User)
kasirIDStr := c.Param("user_id")
kasirID, err := strconv.Atoi(kasirIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID kasir tidak valid"})
return
}
var req UpdateKasirRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var kasir models.User
if err := config.DB.Where("id = ? AND company_id = ? AND role = ?", kasirID, *owner.CompanyID, "kasir").First(&kasir).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Kasir tidak ditemukan di perusahaan ini"})
return
}
if req.Name != "" {
kasir.Name = req.Name
}
if req.Email != "" {
var count int64
config.DB.Model(&models.User{}).Where("email = ? AND id != ?", req.Email, kasirID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Email sudah digunakan user lain"})
return
}
kasir.Email = req.Email
}
if req.Password != "" {
hashed, _ := middleware.HashPassword(req.Password)
kasir.Password = hashed
}
if req.BranchID != nil {
kasir.BranchID = req.BranchID
}
if req.IsActive != nil {
kasir.IsActive = *req.IsActive
}
if err := config.DB.Save(&kasir).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update kasir"})
return
}
c.JSON(http.StatusOK, toUserResponse(&kasir))
}