Spaces:
Sleeping
Sleeping
Download frontend/src/views/admin/SecurityView.vue from erranova/EduPlatform: direct link, hf CLI and curl.
- Browser
- Download file 4.33 kB
-
https://huggingface.co/spaces/erranova/EduPlatform/resolve/main/frontend/src/views/admin/SecurityView.vue
- Command line
-
hf download hf://spaces/erranova/EduPlatform/frontend/src/views/admin/SecurityView.vue
-
curl -L -o SecurityView.vue https://huggingface.co/spaces/erranova/EduPlatform/resolve/main/frontend/src/views/admin/SecurityView.vue
4.33 kB
| <template> | |
| <div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <h3>{{ t('admin.security.title') }}</h3> | |
| <div class="panel-actions"> | |
| <span class="badge badge-red">{{ logs.length }} {{ t('admin.security.entries') }}</span> | |
| <button class="btn-clear" @click="clearLogs" :disabled="clearing || logs.length === 0" id="btn-clear-security"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> | |
| {{ t('admin.security.clear') }} | |
| </button> | |
| </div> | |
| </div> | |
| <div class="loading-state" v-if="loading"><div class="spinner"></div></div> | |
| <div class="log-table-wrap" v-else> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>{{ t('admin.security.action') }}</th> | |
| <th>{{ t('admin.security.user') }}</th> | |
| <th>{{ t('admin.security.ip') }}</th> | |
| <th>{{ t('admin.security.date') }}</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr v-for="log in logs" :key="log.id" :class="rowClass(log.action)"> | |
| <td><span :class="actionBadge(log.action)">{{ log.action }}</span></td> | |
| <td>{{ log.user }}</td> | |
| <td><code>{{ log.ip_address }}</code></td> | |
| <td>{{ formatTime(log.timestamp) }}</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <div class="empty-state" v-if="logs.length === 0"><p>{{ t('admin.security.empty') }}</p></div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref, onMounted } from 'vue' | |
| import { useI18n } from 'vue-i18n' | |
| import api from '@/api/client' | |
| const { t } = useI18n() | |
| const logs = ref([]) | |
| const loading = ref(true) | |
| const clearing = ref(false) | |
| function formatTime(t) { return new Date(t).toLocaleString('fr-MA') } | |
| function rowClass(a) { return a.includes('failed') || a.includes('locked') || a.includes('suspicious') ? 'danger-row' : '' } | |
| function actionBadge(a) { | |
| if (a.includes('success') || a.includes('réussie')) return 'badge badge-green' | |
| if (a.includes('failed') || a.includes('locked') || a.includes('échouée')) return 'badge badge-red' | |
| return 'badge badge-gold' | |
| } | |
| async function fetchLogs() { | |
| loading.value = true | |
| try { | |
| const { data } = await api.get('/auth/admin/security-logs/') | |
| logs.value = data | |
| } catch(e) { console.error(e) } finally { loading.value = false } | |
| } | |
| async function clearLogs() { | |
| if (!confirm(t('admin.security.clear_confirm'))) return | |
| clearing.value = true | |
| try { | |
| await api.delete('/auth/admin/security-logs/') | |
| logs.value = [] | |
| } catch(e) { console.error(e) } finally { clearing.value = false } | |
| } | |
| onMounted(fetchLogs) | |
| </script> | |
| <style scoped> | |
| .panel { background:#fff; border-radius:14px; padding:24px; box-shadow:0 2px 12px rgba(0,0,0,0.06); } | |
| .panel-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap: wrap; gap: 12px; } | |
| .panel-header h3 { font-family:'Playfair Display',serif; font-size:1.05rem; color:var(--ink); } | |
| .panel-actions { display: flex; align-items: center; gap: 12px; } | |
| .btn-clear { display: flex; align-items: center; gap: 6px; padding: 6px 14px; background: rgba(229,62,62,0.06); color: var(--error); border: 1px solid rgba(229,62,62,0.2); border-radius: 8px; font-size: 0.8rem; font-weight: 600; font-family: 'DM Sans', sans-serif; cursor: pointer; transition: all 0.25s; } | |
| .btn-clear:hover:not(:disabled) { background: var(--error); color: #fff; border-color: var(--error); } | |
| .btn-clear:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .log-table-wrap { overflow-x:auto; } | |
| table { width:100%; border-collapse:collapse; font-size:0.85rem; } | |
| th { padding:10px 14px; background:#f8f5ee; text-align:left; font-size:0.76rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--muted); } | |
| td { padding:11px 14px; border-top:1px solid #f0ece2; } | |
| code { background:var(--cream); padding:2px 8px; border-radius:6px; font-size:0.82rem; } | |
| .danger-row td { background:rgba(229,62,62,0.03); } | |
| .loading-state { display:flex; justify-content:center; padding:40px; } | |
| .empty-state { text-align:center; padding:30px; color:var(--muted); } | |
| </style> | |