Files
bssapp/svc/routes/user_detail.go

483 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}