package routes import ( "bssapp-backend/auth" "bssapp-backend/internal/auditlog" "bssapp-backend/internal/mailer" "bssapp-backend/internal/security" "bssapp-backend/models" "bssapp-backend/queries" "bytes" "database/sql" "encoding/json" "fmt" "io" "log" "net/http" "os" "strconv" "strings" "time" "github.com/gorilla/mux" ) // ====================================================== // 👤 USER DETAIL ROUTE (GET + PUT) // URL: /api/users/{id} // ====================================================== type LoginUser struct { ID int64 `json:"id"` Username string `json:"username"` Email string `json:"email"` RoleID int `json:"role_id"` RoleCode string `json:"role_code"` ForcePasswordChange bool `json:"force_password_change"` } func UserDetailRoute(db *sql.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") idStr := mux.Vars(r)["id"] userID, err := strconv.ParseInt(idStr, 10, 64) if err != nil || userID <= 0 { http.Error(w, "Geçersiz kullanıcı id", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: handleUserGet(db, w, userID) case http.MethodPut: handleUserUpdate(db, w, r, userID) case http.MethodDelete: handleUserDelete(db, w, r, userID) case http.MethodOptions: w.WriteHeader(http.StatusOK) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }) } // ====================================================== // 📥 GET USER // ====================================================== // ====================================================== // 📥 GET USER (FINAL - mk_dfusr Uyumlu) // ====================================================== func handleUserGet(db *sql.DB, w http.ResponseWriter, userID int64) { var u models.UserDetail // -------------------------------------------------- // 🟢 HEADER // -------------------------------------------------- err := db.QueryRow( queries.GetUserHeader, userID, ).Scan( &u.ID, &u.Code, // username &u.IsActive, &u.FullName, &u.Email, &u.Mobile, &u.Address, &u.HasPassword, ) if err != nil { // detaylı log (çok önemli) fmt.Println("❌ [UserDetail] HEADER SCAN ERROR:", err) if err == sql.ErrNoRows { http.Error(w, "Kullanıcı bulunamadı", http.StatusNotFound) return } http.Error(w, "User header error", http.StatusInternalServerError) return } // -------------------------------------------------- // 🟢 ROLES // -------------------------------------------------- roleRows, err := db.Query(queries.GetUserRoles, userID) if err != nil { fmt.Println("⚠️ [UserDetail] ROLE QUERY:", err) } else { defer roleRows.Close() for roleRows.Next() { var code string if err := roleRows.Scan(&code); err == nil { u.Roles = append(u.Roles, code) } } } // -------------------------------------------------- // 🟢 DEPARTMENTS // -------------------------------------------------- deptRows, err := db.Query(queries.GetUserDepartments, userID) if err != nil { fmt.Println("⚠️ [UserDetail] DEPT QUERY:", err) } else { defer deptRows.Close() for deptRows.Next() { var d models.DeptOption if err := deptRows.Scan(&d.Code, &d.Title); err == nil { u.Departments = append(u.Departments, d) } } } // -------------------------------------------------- // 🟢 PIYASALAR // -------------------------------------------------- piyRows, err := db.Query(queries.GetUserPiyasalar, userID) if err != nil { fmt.Println("⚠️ [UserDetail] PIYASA QUERY:", err) } else { defer piyRows.Close() for piyRows.Next() { var p models.DeptOption if err := piyRows.Scan(&p.Code, &p.Title); err == nil { u.Piyasalar = append(u.Piyasalar, p) } } } // -------------------------------------------------- // 🟢 NEBIM USERS // -------------------------------------------------- nebimRows, err := db.Query(queries.GetUserNebim, userID) if err != nil { fmt.Println("⚠️ [UserDetail] NEBIM QUERY:", err) } else { defer nebimRows.Close() for nebimRows.Next() { var n models.NebimOption if err := nebimRows.Scan( &n.Username, &n.UserGroupCode, ); err == nil { u.NebimUsers = append(u.NebimUsers, n) } } } // -------------------------------------------------- // 🟢 RESPONSE // -------------------------------------------------- w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(u); err != nil { fmt.Println("❌ [UserDetail] JSON ENCODE:", err) } } // ====================================================== // ✍️ UPDATE USER (PUT) // ====================================================== func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID int64) { raw, _ := io.ReadAll(r.Body) fmt.Println("🟥 RAW BODY:", string(raw)) // body tekrar okunabilsin diye reset r.Body = io.NopCloser(bytes.NewBuffer(raw)) var payload models.UserWrite if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { fmt.Printf("🟦 DECODED PAYLOAD: %+v\n", payload) fmt.Println("❌ JSON DECODE ERROR:", err) http.Error(w, "Geçersiz payload", http.StatusBadRequest) return } payload.Code = strings.TrimSpace(payload.Code) payload.FullName = strings.TrimSpace(payload.FullName) payload.Email = strings.TrimSpace(payload.Email) payload.Mobile = strings.TrimSpace(payload.Mobile) payload.Address = strings.TrimSpace(payload.Address) if payload.Code == "" { http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity) return } tx, err := db.Begin() if err != nil { http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError) return } defer tx.Rollback() _, err = tx.Exec( queries.UpdateUserHeader, userID, payload.Code, payload.IsActive, payload.FullName, payload.Email, payload.Mobile, payload.Address, ) if err != nil { log.Printf("❌ [UserDetail] UpdateUserHeader failed user_id=%d err=%v payload=%+v", userID, err, payload) http.Error(w, "Header güncellenemedi", http.StatusInternalServerError) return } if _, err := tx.Exec(`DELETE FROM dfrole_usr WHERE dfusr_id = $1`, userID); err != nil { log.Printf("❌ [UserDetail] delete roles failed user_id=%d err=%v", userID, err) http.Error(w, "Roller temizlenemedi", http.StatusInternalServerError) return } for _, code := range payload.Roles { code = strings.TrimSpace(code) if code == "" { continue } if _, err := tx.Exec(queries.InsertUserRole, userID, code); err != nil { log.Printf("❌ [UserDetail] insert role failed user_id=%d role=%q err=%v", userID, code, err) http.Error(w, "Rol eklenemedi", http.StatusInternalServerError) return } } if _, err := tx.Exec(`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, userID); err != nil { log.Printf("❌ [UserDetail] delete departments failed user_id=%d err=%v", userID, err) http.Error(w, "Departmanlar temizlenemedi", http.StatusInternalServerError) return } for _, d := range payload.Departments { code := strings.TrimSpace(d.Code) if code == "" { continue } if _, err := tx.Exec(queries.InsertUserDepartment, userID, code); err != nil { log.Printf("❌ [UserDetail] insert department failed user_id=%d dept=%q err=%v", userID, code, err) http.Error(w, "Departman eklenemedi", http.StatusInternalServerError) return } } if _, err := tx.Exec(`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, userID); err != nil { log.Printf("❌ [UserDetail] delete piyasalar failed user_id=%d err=%v", userID, err) http.Error(w, "Piyasalar temizlenemedi", http.StatusInternalServerError) return } for _, p := range payload.Piyasalar { code := strings.TrimSpace(p.Code) if code == "" { continue } if _, err := tx.Exec(queries.InsertUserPiyasa, userID, code); err != nil { log.Printf("❌ [UserDetail] insert piyasa failed user_id=%d piyasa=%q err=%v", userID, code, err) http.Error(w, "Piyasa eklenemedi", http.StatusInternalServerError) return } } if _, err := tx.Exec(`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, userID); err != nil { log.Printf("❌ [UserDetail] delete nebim users failed user_id=%d err=%v", userID, err) http.Error(w, "Nebim kullanıcıları temizlenemedi", http.StatusInternalServerError) return } for _, n := range payload.NebimUsers { username := strings.TrimSpace(n.Username) if username == "" { continue } if _, err := tx.Exec(queries.InsertUserNebim, userID, username); err != nil { log.Printf("❌ [UserDetail] insert nebim user failed user_id=%d username=%q err=%v", userID, username, err) http.Error(w, "Nebim kullanıcısı eklenemedi", http.StatusInternalServerError) return } } if err := tx.Commit(); err != nil { log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err) http.Error(w, "Commit başarısız", http.StatusInternalServerError) return } _ = json.NewEncoder(w).Encode(map[string]any{"success": true}) } // ====================================================== // 🗑️ DELETE USER (HARD DELETE) // ====================================================== func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID int64) { claims, _ := auth.GetClaimsFromContext(r.Context()) if claims != nil && int64(claims.ID) == userID { http.Error(w, "Kendi kullanicinizi silemezsiniz", http.StatusConflict) return } tx, err := db.Begin() if err != nil { http.Error(w, "Transaction baslatilamadi", http.StatusInternalServerError) return } defer tx.Rollback() var username string _ = tx.QueryRow(` SELECT username FROM mk_dfusr WHERE id = $1 `, userID).Scan(&username) if strings.TrimSpace(username) == "" { _ = tx.QueryRow(` SELECT code FROM dfusr WHERE id = $1 `, userID).Scan(&username) } if strings.TrimSpace(username) == "" { http.Error(w, "Kullanici bulunamadi", http.StatusNotFound) return } cleanupQueries := []string{ `DELETE FROM mk_refresh_tokens WHERE mk_user_id = $1`, `DELETE FROM mk_dfusr_password_reset WHERE mk_dfusr_id = $1`, `DELETE FROM dfusr_password_reset WHERE dfusr_id = $1`, `DELETE FROM mk_sys_user_permissions WHERE user_id = $1`, `DELETE FROM dfrole_usr WHERE dfusr_id = $1`, `DELETE FROM dfusr_dprt WHERE dfusr_id = $1`, `DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`, `DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`, } for _, q := range cleanupQueries { if _, err := tx.Exec(q, userID); err != nil { log.Printf("❌ [UserDetail] cleanup failed user_id=%d err=%v query=%s", userID, err, q) http.Error(w, "Kullanici baglantilari silinemedi", http.StatusInternalServerError) return } } if _, err := tx.Exec(`DELETE FROM mk_dfusr WHERE id = $1`, userID); err != nil { log.Printf("❌ [UserDetail] delete mk_dfusr failed user_id=%d err=%v", userID, err) http.Error(w, "Kullanici silinemedi", http.StatusInternalServerError) return } if _, err := tx.Exec(`DELETE FROM dfusr WHERE id = $1`, userID); err != nil { log.Printf("❌ [UserDetail] delete dfusr failed user_id=%d err=%v", userID, err) http.Error(w, "Kullanici silinemedi", http.StatusInternalServerError) return } if err := tx.Commit(); err != nil { log.Printf("❌ [UserDetail] delete commit failed user_id=%d err=%v", userID, err) http.Error(w, "Commit basarisiz", http.StatusInternalServerError) return } if claims != nil { auditlog.Enqueue(r.Context(), auditlog.ActivityLog{ ActionType: "user_delete", ActionCategory: "user_admin", ActionTarget: fmt.Sprintf("/api/users/%d", userID), Description: "user deleted from mk_dfusr and dfusr", Username: claims.Username, RoleCode: claims.RoleCode, DfUsrID: int64(claims.ID), TargetDfUsrID: userID, TargetUsername: username, IsSuccess: true, }) } _ = json.NewEncoder(w).Encode(map[string]any{ "success": true, "deleted": userID, "username": username, }) } // ====================================================== // 🔐 ADMIN — PASSWORD RESET MAIL // ====================================================== func SendPasswordResetMailHandler( db *sql.DB, mailer *mailer.GraphMailer, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) var email string err := db.QueryRow(` SELECT email FROM mk_dfusr WHERE id = $1 AND is_active = true `, userID).Scan(&email) if err != nil || email == "" { w.WriteHeader(http.StatusOK) return } // 🔑 TOKEN (PLAIN + HASH) plain, hash, err := security.GenerateResetToken() if err != nil { w.WriteHeader(http.StatusOK) return } expires := time.Now().Add(30 * time.Minute) // 💾 DB → SADECE HASH _, _ = db.Exec(` INSERT INTO dfusr_password_reset (dfusr_id, token, expires_at) VALUES ($1,$2,$3) `, userID, hash, expires) // 🔗 URL → PLAIN resetURL := fmt.Sprintf( "%s/password-reset/%s", os.Getenv("FRONTEND_URL"), plain, ) _ = mailer.SendPasswordResetMail(email, resetURL) // 🕵️ AUDIT auditlog.ForcePasswordChangeStarted( r.Context(), userID, "admin_reset", ) w.WriteHeader(http.StatusOK) } }