From 484512ff25cd357571cf9363f69b9821275cdc80 Mon Sep 17 00:00:00 2001 From: MEHMETKECECI Date: Mon, 16 Feb 2026 17:20:58 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 5 ++ svc/routes/user_detail.go | 99 ++++++++++++++++++++++++++++++++ ui/src/pages/UserDetail.vue | 48 +++++++++++++++- ui/src/stores/UserDetailStore.js | 20 ++++++- 4 files changed, 170 insertions(+), 2 deletions(-) diff --git a/svc/main.go b/svc/main.go index a324c12..901c921 100644 --- a/svc/main.go +++ b/svc/main.go @@ -325,6 +325,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "user", "update", wrapV3(routes.UserDetailRoute(pgDB)), ) + bindV3(r, pgDB, + "/api/users/{id}", "DELETE", + "user", "delete", + wrapV3(routes.UserDetailRoute(pgDB)), + ) bindV3(r, pgDB, "/api/users/{id}/admin-reset-password", "POST", diff --git a/svc/routes/user_detail.go b/svc/routes/user_detail.go index 7c65b5e..d2ca072 100644 --- a/svc/routes/user_detail.go +++ b/svc/routes/user_detail.go @@ -1,6 +1,7 @@ package routes import ( + "bssapp-backend/auth" "bssapp-backend/internal/auditlog" "bssapp-backend/internal/mailer" "bssapp-backend/internal/security" @@ -51,6 +52,8 @@ func UserDetailRoute(db *sql.DB) http.Handler { 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: @@ -323,6 +326,102 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID _ = 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 // ====================================================== diff --git a/ui/src/pages/UserDetail.vue b/ui/src/pages/UserDetail.vue index fd41520..087a1ae 100644 --- a/ui/src/pages/UserDetail.vue +++ b/ui/src/pages/UserDetail.vue @@ -46,6 +46,15 @@ :loading="saving" @click="onSave" /> + { const canSaveUser = computed(() => isNew.value ? canWriteUser.value : canUpdateUser.value) const userId = computed(() => (isEdit.value || isView.value) ? Number(route.params.id) : null) +const canDeleteThisUser = computed(() => + (isEdit.value || isView.value) && + !!userId.value && + canDeleteUser.value +) const hasPassword = computed(() => store.hasPassword) @@ -386,6 +402,36 @@ function goList () { router.push({ name: 'user-list' }) } +function confirmDeleteUser () { + if (!canDeleteThisUser.value) { + $q.notify({ type: 'negative', message: 'Silme yetkiniz yok' }) + return + } + + const code = (form.value.code || '').trim() + const label = code || `ID ${userId.value}` + + $q.dialog({ + title: 'Kullanici silinsin mi?', + message: `${label} kaydi tablodan silinecek. Bu islem geri alinamaz.`, + cancel: true, + persistent: true + }).onOk(async () => { + await deleteUser() + }) +} + +async function deleteUser () { + try { + await store.deleteUser(userId.value) + $q.notify({ type: 'positive', message: 'Kullanici silindi' }) + await router.replace({ name: 'user-list' }) + window.location.reload() + } catch { + $q.notify({ type: 'negative', message: store.error || 'Kullanici silinemedi' }) + } +} + function confirmSendPasswordMail () { $q.dialog({ diff --git a/ui/src/stores/UserDetailStore.js b/ui/src/stores/UserDetailStore.js index e2e760c..abe3169 100644 --- a/ui/src/stores/UserDetailStore.js +++ b/ui/src/stores/UserDetailStore.js @@ -1,6 +1,6 @@ // src/stores/userDetailStore.js import { defineStore } from 'pinia' -import api, { get, post, put } from 'src/services/api' +import api, { get, post, put, del } from 'src/services/api' export const useUserDetailStore = defineStore('userDetail', { state: () => ({ @@ -11,6 +11,7 @@ export const useUserDetailStore = defineStore('userDetail', { /* ================= FLAGS ================= */ loading: false, saving: false, + deleting: false, error: null, /* ================= FORM ================= */ @@ -222,6 +223,23 @@ export const useUserDetailStore = defineStore('userDetail', { } }, + /* ===================================================== + πŸ—‘οΈ DELETE USER + ===================================================== */ + async deleteUser (id) { + this.deleting = true + this.error = null + + try { + await del(`/users/${id}`) + } catch (e) { + this.error = 'Kullanici silinemedi' + throw e + } finally { + this.deleting = false + } + }, + /* ===================================================== πŸ“š LOOKUPS (NEW + EDIT ORTAK) ===================================================== */