225 lines
6.9 KiB
HTML
225 lines
6.9 KiB
HTML
{% extends "ewoooc_base.html" %}
|
|
|
|
{% block title %}登入歷史 - EwoooC{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.login-history-page {
|
|
color: var(--momo-text-primary);
|
|
}
|
|
|
|
.login-history-table-head th {
|
|
background: var(--momo-bg-paper) !important;
|
|
color: var(--momo-text-secondary) !important;
|
|
font-family: var(--momo-font-mono, monospace);
|
|
font-size: var(--momo-text-label);
|
|
font-weight: 800;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.login-history-empty {
|
|
padding: var(--momo-space-5) !important;
|
|
color: var(--momo-text-secondary) !important;
|
|
text-align: center;
|
|
}
|
|
|
|
.login-history-empty.is-danger {
|
|
color: var(--momo-danger-text) !important;
|
|
}
|
|
|
|
.login-status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 22px;
|
|
padding: 3px 8px;
|
|
border: 1px solid var(--momo-border-light);
|
|
border-radius: var(--momo-radius-sm);
|
|
font-family: var(--momo-font-mono, monospace);
|
|
font-size: var(--momo-text-label);
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
}
|
|
|
|
.login-status-badge.is-success {
|
|
background: var(--momo-success-bg);
|
|
border-color: var(--momo-success-border);
|
|
color: var(--momo-success-text);
|
|
}
|
|
|
|
.login-status-badge.is-danger {
|
|
background: var(--momo-danger-bg);
|
|
border-color: var(--momo-danger-border);
|
|
color: var(--momo-danger-text);
|
|
}
|
|
|
|
.login-status-badge.is-warning {
|
|
background: var(--momo-warning-bg);
|
|
border-color: var(--momo-warning-border);
|
|
color: var(--momo-warning-text);
|
|
}
|
|
|
|
.login-status-badge.is-muted {
|
|
background: var(--momo-tag-muted-bg);
|
|
border-color: var(--momo-tag-muted-border);
|
|
color: var(--momo-tag-muted-text);
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.login-history-table {
|
|
min-width: 0;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
.login-history-table,
|
|
.login-history-table tbody,
|
|
.login-history-table tr,
|
|
.login-history-table td {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
.login-history-table thead {
|
|
display: none;
|
|
}
|
|
|
|
.login-history-table tbody {
|
|
display: grid;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.login-history-table tbody tr {
|
|
overflow: hidden;
|
|
border: 1px solid var(--momo-border-light, #e5dccd);
|
|
border-radius: 8px;
|
|
background: var(--momo-bg-surface, #fffaf1);
|
|
}
|
|
|
|
.login-history-table tbody td {
|
|
display: grid;
|
|
grid-template-columns: 5.6rem minmax(0, 1fr);
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--momo-border-light, #e5dccd);
|
|
text-align: left !important;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.login-history-table tbody td:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.login-history-table tbody td::before {
|
|
color: var(--momo-text-tertiary, #9a8f80);
|
|
font-family: var(--momo-font-mono, monospace);
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.login-history-table tbody td:nth-child(1)::before { content: "時間"; }
|
|
.login-history-table tbody td:nth-child(2)::before { content: "帳號"; }
|
|
.login-history-table tbody td:nth-child(3)::before { content: "狀態"; }
|
|
.login-history-table tbody td:nth-child(4)::before { content: "IP"; }
|
|
.login-history-table tbody td:nth-child(5)::before { content: "原因"; }
|
|
.login-history-table tbody td:nth-child(6)::before { content: "裝置"; }
|
|
|
|
.login-history-table tbody td[colspan] {
|
|
display: block;
|
|
}
|
|
|
|
.login-history-table tbody td[colspan]::before {
|
|
content: none;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4 login-history-page">
|
|
<div class="page-header">
|
|
<h1><i class="fas fa-clock-rotate-left me-2"></i>登入歷史</h1>
|
|
<p>追蹤登入風險,避免未授權操作影響業績流程。</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-list me-2"></i>最近記錄</span>
|
|
<select class="form-select form-select-sm" id="historyLimit" style="width: 120px;">
|
|
<option value="50">50 筆</option>
|
|
<option value="100" selected>100 筆</option>
|
|
<option value="200">200 筆</option>
|
|
</select>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 login-history-table">
|
|
<thead class="login-history-table-head">
|
|
<tr>
|
|
<th>時間</th>
|
|
<th>帳號</th>
|
|
<th>狀態</th>
|
|
<th>IP</th>
|
|
<th>原因</th>
|
|
<th>裝置資訊</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="loginHistoryBody">
|
|
<tr>
|
|
<td colspan="6" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
const statusClass = {
|
|
success: 'is-success',
|
|
failed: 'is-danger',
|
|
locked: 'is-warning'
|
|
};
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '').replace(/[&<>"']/g, char => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[char]));
|
|
}
|
|
|
|
async function loadLoginHistory() {
|
|
const limit = document.getElementById('historyLimit').value;
|
|
const tbody = document.getElementById('loginHistoryBody');
|
|
const response = await fetch(`/api/login_history?limit=${limit}`);
|
|
const result = await response.json();
|
|
if (!result.success) {
|
|
tbody.innerHTML = `<tr><td colspan="6" class="login-history-empty is-danger">${escapeHtml(result.message)}</td></tr>`;
|
|
return;
|
|
}
|
|
if (!result.data.length) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="login-history-empty">尚無登入記錄</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = result.data.map(item => `
|
|
<tr>
|
|
<td>${escapeHtml(item.login_time ? new Date(item.login_time).toLocaleString('zh-TW') : '')}</td>
|
|
<td>${escapeHtml(item.username_attempted || item.user_id || '')}</td>
|
|
<td><span class="login-status-badge ${statusClass[item.status] || 'is-muted'}">${escapeHtml(item.status)}</span></td>
|
|
<td>${escapeHtml(item.ip_address)}</td>
|
|
<td>${escapeHtml(item.failure_reason)}</td>
|
|
<td class="text-truncate" style="max-width: 360px;">${escapeHtml(item.user_agent)}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
document.getElementById('historyLimit').addEventListener('change', loadLoginHistory);
|
|
document.addEventListener('DOMContentLoaded', loadLoginHistory);
|
|
</script>
|
|
{% endblock %}
|