Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-16 17:20:58 +03:00
parent 0a14f87a3e
commit 484512ff25
4 changed files with 170 additions and 2 deletions

View File

@@ -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",

View File

@@ -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
// ======================================================

View File

@@ -46,6 +46,15 @@
:loading="saving"
@click="onSave"
/>
<q-btn
v-if="canDeleteThisUser"
label="SIL"
color="negative"
icon="delete"
class="q-ml-sm"
:loading="deleting"
@click="confirmDeleteUser"
/>
<q-btn
v-if="canReadUser"
label="LİSTEYE DÖN"
@@ -259,10 +268,11 @@ import { storeToRefs } from 'pinia'
import { useUserDetailStore } from 'src/stores/UserDetailStore'
import { usePermission } from 'src/composables/usePermission'
const { canRead, canWrite, canUpdate } = usePermission()
const { canRead, canWrite, canUpdate, canDelete } = usePermission()
const canReadUser = canRead('user')
const canWriteUser = canWrite('user')
const canUpdateUser = canUpdate('user')
const canDeleteUser = canDelete('user')
const $q = useQuasar()
const route = useRoute()
@@ -274,6 +284,7 @@ const {
form,
loading,
saving,
deleting,
roleOptions,
departmentOptions,
piyasaOptions,
@@ -296,6 +307,11 @@ const canAccessPage = computed(() => {
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({

View File

@@ -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)
===================================================== */