Files
go_nuxt_admin/app/pages/admin/security/index.vue
Beyhan Oğur 5285a0dd86 first commit
2026-04-26 22:07:47 +03:00

502 lines
25 KiB
Vue
Raw 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.
<template>
<div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Güvenlik & Yönetimi</h2>
</div>
<ul class="nav nav-tabs mb-4" id="securityTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="whitelist-tab" data-bs-toggle="tab" data-bs-target="#whitelist"
type="button" role="tab" aria-controls="whitelist" aria-selected="true">CORS Whitelist</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="blacklist-tab" data-bs-toggle="tab" data-bs-target="#blacklist"
type="button" role="tab" aria-controls="blacklist" aria-selected="false">CORS Blacklist</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="ratelimit-tab" data-bs-toggle="tab" data-bs-target="#ratelimit"
type="button" role="tab" aria-controls="ratelimit" aria-selected="false">Rate Limit</button>
</li>
</ul>
<div class="tab-content" id="securityTabContent">
<!-- WHITELIST TAB -->
<div class="tab-pane fade show active" id="whitelist" role="tabpanel" aria-labelledby="whitelist-tab">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h5 class="mb-0">Whitelist Origins</h5>
<div class="d-flex align-items-center">
<div class="form-check form-switch me-3 mb-0">
<input class="form-check-input" type="checkbox" v-model="showDeleted"
id="whitelistDeleted">
<label class="form-check-label" for="whitelistDeleted">Silinenler</label>
</div>
<button class="btn btn-success btn-sm" @click="openModal('whitelist')"><i
class="fas fa-plus me-2"></i> Yeni
Ekle</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Origin</th>
<th>ıklama</th>
<th>Durum</th>
<th class="text-end" width="150">İşlemler</th>
</tr>
</thead>
<tbody>
<tr v-for="item in whitelistItems" :key="item.ID">
<td>{{ item.origin }}</td>
<td>{{ item.description }}</td>
<td>
<span class="badge" :class="item.is_active ? 'bg-success' : 'bg-secondary'">
{{ item.is_active ? 'Aktif' : 'Pasif' }}
</span>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<template v-if="!item.DeletedAt">
<button class="btn btn-outline-primary"
@click="openModal('whitelist', item)" title="Düzenle">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger"
@click="deleteItem('whitelist', item.ID)" title="Sil">
<i class="fas fa-trash"></i>
</button>
</template>
<button v-else class="btn btn-outline-success"
@click="restoreItem('whitelist', item.ID)" title="Geri Yükle">
<i class="fas fa-trash-restore"></i>
</button>
</div>
</td>
</tr>
<tr v-if="whitelistItems.length === 0">
<td colspan="4" class="text-center py-4">Kayıt bulunamadı.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- BLACKLIST TAB -->
<div class="tab-pane fade" id="blacklist" role="tabpanel" aria-labelledby="blacklist-tab">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h5 class="mb-0">Blacklist Origins</h5>
<div class="d-flex align-items-center">
<div class="form-check form-switch me-3 mb-0">
<input class="form-check-input" type="checkbox" v-model="showDeleted"
id="blacklistDeleted">
<label class="form-check-label" for="blacklistDeleted">Silinenler</label>
</div>
<button class="btn btn-success btn-sm" @click="openModal('blacklist')"><i
class="fas fa-plus me-2"></i>
Yeni
Ekle</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Origin</th>
<th>Sebep</th>
<th>Durum</th>
<th class="text-end" width="150">İşlemler</th>
</tr>
</thead>
<tbody>
<tr v-for="item in blacklistItems" :key="item.ID">
<td>{{ item.origin }}</td>
<td>{{ item.reason }}</td>
<td>
<span class="badge" :class="item.is_active ? 'bg-danger' : 'bg-secondary'">
{{ item.is_active ? 'Aktif' : 'Pasif' }}
</span>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<template v-if="!item.DeletedAt">
<button class="btn btn-outline-primary"
@click="openModal('blacklist', item)" title="Düzenle">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger"
@click="deleteItem('blacklist', item.ID)" title="Sil">
<i class="fas fa-trash"></i>
</button>
</template>
<button v-else class="btn btn-outline-success"
@click="restoreItem('blacklist', item.ID)" title="Geri Yükle">
<i class="fas fa-trash-restore"></i>
</button>
</div>
</td>
</tr>
<tr v-if="blacklistItems.length === 0">
<td colspan="4" class="text-center py-4">Kayıt bulunamadı.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- RATE LIMIT TAB -->
<div class="tab-pane fade" id="ratelimit" role="tabpanel" aria-labelledby="ratelimit-tab">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h5 class="mb-0">Rate Limits</h5>
<div class="d-flex align-items-center">
<div class="form-check form-switch me-3 mb-0">
<input class="form-check-input" type="checkbox" v-model="showDeleted"
id="ratelimitDeleted">
<label class="form-check-label" for="ratelimitDeleted">Silinenler</label>
</div>
<button class="btn btn-success btn-sm" @click="openModal('ratelimit')"><i
class="fas fa-plus me-2"></i>
Yeni
Ekle</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>İsim</th>
<th>Max İstek / Süre (sn)</th>
<th>ıklama</th>
<th>Durum</th>
<th class="text-end" width="150">İşlemler</th>
</tr>
</thead>
<tbody>
<tr v-for="item in rateLimitItems" :key="item.ID">
<td>{{ item.name }}</td>
<td>{{ item.max_requests }} / {{ item.window_seconds }}sn</td>
<td>{{ item.description }}</td>
<td>
<span class="badge" :class="item.is_active ? 'bg-success' : 'bg-secondary'">
{{ item.is_active ? 'Aktif' : 'Pasif' }}
</span>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<template v-if="!item.DeletedAt">
<button class="btn btn-outline-primary"
@click="openModal('ratelimit', item)" title="Düzenle">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger"
@click="deleteItem('ratelimit', item.ID)" title="Sil">
<i class="fas fa-trash"></i>
</button>
</template>
<button v-else class="btn btn-outline-success"
@click="restoreItem('ratelimit', item.ID)" title="Geri Yükle">
<i class="fas fa-trash-restore"></i>
</button>
</div>
</td>
</tr>
<tr v-if="rateLimitItems.length === 0">
<td colspan="5" class="text-center py-4">Kayıt bulunamadı.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- MODAL -->
<div class="modal fade" id="securityModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ modalTitle }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="saveItem">
<!-- Common Fields -->
<div v-if="activeTab === 'whitelist' || activeTab === 'blacklist'" class="mb-3">
<label class="form-label">Origin (URL)</label>
<input v-model="formData.origin" type="text" class="form-control"
placeholder="http://example.com" required>
</div>
<div v-if="activeTab === 'whitelist'" class="mb-3">
<label class="form-label">ıklama</label>
<input v-model="formData.description" type="text" class="form-control">
</div>
<div v-if="activeTab === 'blacklist'" class="mb-3">
<label class="form-label">Sebep</label>
<input v-model="formData.reason" type="text" class="form-control">
</div>
<!-- Rate Limit Fields -->
<div v-if="activeTab === 'ratelimit'" class="mb-3">
<label class="form-label">İsim (Endpoint/Key)</label>
<input v-model="formData.name" type="text" class="form-control" required>
</div>
<div v-if="activeTab === 'ratelimit'" class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Max İstek</label>
<input v-model="formData.max_requests" type="number" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Süre (Saniye)</label>
<input v-model="formData.window_seconds" type="number" class="form-control"
required>
</div>
</div>
<div v-if="activeTab === 'ratelimit'" class="mb-3">
<label class="form-label">ıklama</label>
<input v-model="formData.description" type="text" class="form-control">
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" v-model="formData.is_active"
id="isActiveCheck">
<label class="form-check-label" for="isActiveCheck">Aktif</label>
</div>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2"
data-bs-dismiss="modal">İptal</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
Kaydet
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { CorsWhitelistItem, CorsBlacklistItem, RateLimitItem } from '~~/types/security';
import Swal from 'sweetalert2';
definePageMeta({
layout: 'admin',
middleware: 'admin'
});
const config = useRuntimeConfig();
const { data: authData } = useAuth();
const loading = ref(false);
const activeTab = ref<'whitelist' | 'blacklist' | 'ratelimit'>('whitelist');
const showDeleted = ref(false); // Global toggle for simplicity, or could be per tab
const whitelistItems = ref<CorsWhitelistItem[]>([]);
const blacklistItems = ref<CorsBlacklistItem[]>([]);
const rateLimitItems = ref<RateLimitItem[]>([]);
const formData = ref<any>({});
const isEditing = ref(false);
let securityModal: any = null;
const modalTitle = computed(() => {
const action = isEditing.value ? 'Düzenle' : 'Ekle';
if (activeTab.value === 'whitelist') return `Whitelist ${action}`;
if (activeTab.value === 'blacklist') return `Blacklist ${action}`;
return `Rate Limit ${action}`;
});
// Watch for tab or filter changes
watch([activeTab, showDeleted], () => {
fetchItems();
});
// -- API HANDLERS --
const fetchItems = async () => {
try {
const query = showDeleted.value ? { deleted: 'true' } : {};
const headers = { Authorization: `Bearer ${(authData.value as any)?.accessToken}` };
if (activeTab.value === 'whitelist') {
const res = await $fetch<any>('/api/v1/admin/cors/whitelist', { baseURL: config.public.BASE_API_URL, headers, query });
whitelistItems.value = res.items || [];
} else if (activeTab.value === 'blacklist') {
const res = await $fetch<any>('/api/v1/admin/cors/blacklist', { baseURL: config.public.BASE_API_URL, headers, query });
blacklistItems.value = res.items || [];
} else {
const res = await $fetch<any>('/api/v1/admin/rate-limit', { baseURL: config.public.BASE_API_URL, headers, query });
rateLimitItems.value = res.items || [];
}
} catch (error) {
console.error('Fetch error:', error);
// Fallback or empty list on error
if (activeTab.value === 'whitelist') whitelistItems.value = [];
else if (activeTab.value === 'blacklist') blacklistItems.value = [];
else rateLimitItems.value = [];
}
};
const openModal = (tab: 'whitelist' | 'blacklist' | 'ratelimit', item: any = null) => {
activeTab.value = tab;
if (item) {
isEditing.value = true;
formData.value = { ...item };
} else {
isEditing.value = false;
formData.value = { is_active: true };
}
const modalEl = document.getElementById('securityModal');
if (modalEl) {
// @ts-ignore
securityModal = new bootstrap.Modal(modalEl);
securityModal.show();
}
};
const saveItem = async () => {
loading.value = true;
try {
let endpoint = '';
if (activeTab.value === 'whitelist') endpoint = '/api/v1/admin/cors/whitelist';
else if (activeTab.value === 'blacklist') endpoint = '/api/v1/admin/cors/blacklist';
else endpoint = '/api/v1/admin/rate-limit';
let method: 'POST' | 'PUT' = 'POST';
if (isEditing.value && formData.value.ID) {
endpoint += `/${formData.value.ID}`;
method = 'PUT';
}
// Convert numbers for RateLimit
if (activeTab.value === 'ratelimit') {
formData.value.max_requests = Number(formData.value.max_requests);
formData.value.window_seconds = Number(formData.value.window_seconds);
}
await $fetch(endpoint, {
method,
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
body: formData.value
});
if (securityModal) securityModal.hide();
Swal.fire('Başarılı', 'İşlem başarıyla tamamlandı.', 'success');
fetchItems();
} catch (error) {
console.error(error);
Swal.fire('Hata', 'İşlem başarısız.', 'error');
} finally {
loading.value = false;
}
};
const deleteItem = async (tab: string, id: number) => {
const result = await Swal.fire({
title: 'Silme Tipi?',
text: "Kayıt nasıl silinsin?",
icon: 'warning',
showCancelButton: true,
showDenyButton: true,
confirmButtonText: 'Soft Sil (Arşivle)',
denyButtonText: 'Hard Sil (Kalıcı)',
cancelButtonText: 'İptal'
});
if (result.isDismissed) return;
const isHard = result.isDenied;
try {
let endpoint = '';
if (tab === 'whitelist') endpoint = `/api/v1/admin/cors/whitelist/${id}`;
else if (tab === 'blacklist') endpoint = `/api/v1/admin/cors/blacklist/${id}`;
else endpoint = `/api/v1/admin/rate-limit/${id}`;
if (isHard) endpoint += '/hard';
await $fetch(endpoint, {
method: 'DELETE',
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` }
});
Swal.fire('Silindi!', isHard ? 'Kayıt kalıcı olarak silindi.' : 'Kayıt arşivlendi (soft delete).', 'success');
fetchItems();
} catch (error) {
console.error(error);
Swal.fire('Hata', 'Silme işlemi başarısız.', 'error');
}
};
const restoreItem = async (tab: string, id: number) => {
const result = await Swal.fire({
title: 'Emin misiniz?',
text: "Kayıt geri yüklenecek.",
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Geri Yükle',
cancelButtonText: 'İptal'
});
if (!result.isConfirmed) return;
try {
let endpoint = '';
if (tab === 'whitelist') endpoint = `/api/v1/admin/cors/whitelist/${id}/restore`;
else if (tab === 'blacklist') endpoint = `/api/v1/admin/cors/blacklist/${id}/restore`;
else endpoint = `/api/v1/admin/rate-limit/${id}/restore`;
await $fetch(endpoint, {
method: 'POST', // Assuming POST for restore action
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` }
});
Swal.fire('Başarılı', 'Kayıt geri yüklendi.', 'success');
fetchItems();
} catch (error) {
console.error(error);
Swal.fire('Hata', 'Geri yükleme başarısız. Endpoint mevcut olmayabilir.', 'error');
}
};
onMounted(() => {
// Bootstrap tabs events
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
tabEls.forEach(tabEl => {
tabEl.addEventListener('shown.bs.tab', (event: any) => {
const targetId = event.target.getAttribute('data-bs-target');
if (targetId === '#whitelist') activeTab.value = 'whitelist';
if (targetId === '#blacklist') activeTab.value = 'blacklist';
if (targetId === '#ratelimit') activeTab.value = 'ratelimit';
// fetchItems() is called by watcher
});
});
fetchItems(); // Initial fetch
});
</script>
```