Fase 3 (islas): historiales de seguridad y transacciones en React/TSX
Tabla genérica reutilizable HistoryTable (columnas por data-columns, filas por json_script) + entry history_table: - security_history: intentos de conexión (username/status/ip/fecha). Se elimina la caja "actividad" duplicada. Las vistas pasan login_attempts_data serializable. - trans_history: historial de transacciones (StripeLog) con transactions_data. Datos embebidos con json_script (sin endpoint extra). Verificado: check OK, ambas plantillas renderizan la isla con datos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
interface Column {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
columns: Column[]
|
||||||
|
rows: Record<string, unknown>[]
|
||||||
|
emptyText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tabla genérica de solo lectura para los historiales (seguridad, transacciones, baneos). */
|
||||||
|
export function HistoryTable({ columns, rows, emptyText }: Props) {
|
||||||
|
return (
|
||||||
|
<table className="max-center-table-aligned">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<th key={c.key}>{c.label}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.length > 0 ? (
|
||||||
|
rows.map((row, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
{columns.map((c) => (
|
||||||
|
<td key={c.key}>{String(row[c.key] ?? '')}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="centered">
|
||||||
|
<span>{emptyText}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { HistoryTable } from '../components/HistoryTable'
|
||||||
|
|
||||||
|
// Entry genérico para los historiales de solo lectura. La plantilla define las
|
||||||
|
// columnas en data-columns (JSON) y las filas en un json_script.
|
||||||
|
const el = document.getElementById('history-table-app')
|
||||||
|
if (el) {
|
||||||
|
const dEl = document.getElementById('history-table-data')
|
||||||
|
const rows = dEl ? JSON.parse(dEl.textContent || '[]') : []
|
||||||
|
const columns = JSON.parse(el.dataset.columns || '[]')
|
||||||
|
createRoot(el).render(
|
||||||
|
<StrictMode>
|
||||||
|
<HistoryTable columns={columns} rows={rows} emptyText={el.dataset.emptyText || 'No hay datos'} />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export default defineConfig({
|
|||||||
transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'),
|
transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'),
|
||||||
rename_guild: resolve(__dirname, 'src/entries/rename_guild.tsx'),
|
rename_guild: resolve(__dirname, 'src/entries/rename_guild.tsx'),
|
||||||
vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'),
|
vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'),
|
||||||
|
history_table: resolve(__dirname, 'src/entries/history_table.tsx'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,53 +11,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="title-box-content"><h2>Historial de actividad</h2></div>
|
|
||||||
<div class="body-box-content centered">
|
|
||||||
<table class="max-center-table-aligned2">
|
|
||||||
<tbody>
|
|
||||||
{% if login_attempts %}
|
|
||||||
{% for attempt in login_attempts %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ attempt.username }}</td> <!-- Agregado: Mostrar el nombre de usuario -->
|
|
||||||
<td>{{ attempt.timestamp|date:"d-m-Y H:i:s" }}</td>
|
|
||||||
<td>{{ attempt.status }}</td>
|
|
||||||
<td>{{ attempt.ip_address }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="centered"><span>No hay actividad</span></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="title-box-content"><h2>Historial de conexiones</h2></div>
|
<div class="title-box-content"><h2>Historial de conexiones</h2></div>
|
||||||
<div class="body-box-content centered">
|
<div class="body-box-content centered">
|
||||||
<table class="max-center-table-aligned">
|
{% load django_vite %}
|
||||||
<thead>
|
<!-- Isla React (Vite): tabla de intentos de conexión (últimos 20). -->
|
||||||
<tr>
|
<div id="history-table-app"
|
||||||
<th>Usuario</th>
|
data-columns='[{"key":"username","label":"Usuario"},{"key":"status","label":"Estado"},{"key":"ip_address","label":"IP"},{"key":"timestamp","label":"Fecha"}]'
|
||||||
<th>Estado</th>
|
data-empty-text="No hay actividad"></div>
|
||||||
<th>IP</th>
|
{{ login_attempts_data|json_script:"history-table-data" }}
|
||||||
<th>Fecha</th>
|
{% vite_asset 'src/entries/history_table.tsx' %}
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for attempt in login_attempts %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ attempt.username }}</td>
|
|
||||||
<td>{{ attempt.status }}</td>
|
|
||||||
<td>{{ attempt.ip_address }}</td>
|
|
||||||
<td>{{ attempt.timestamp|date:"d-m-Y H:i:s" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,42 +22,13 @@
|
|||||||
<h2>Historial de transacciones</h2>
|
<h2>Historial de transacciones</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="body-box-content">
|
<div class="body-box-content">
|
||||||
<table class="max-center-table-aligned">
|
{% load django_vite %}
|
||||||
<thead>
|
<!-- Isla React (Vite): tabla del historial de transacciones. -->
|
||||||
<tr>
|
<div id="history-table-app"
|
||||||
<th>ID de Cuenta</th>
|
data-columns='[{"key":"account_id","label":"ID de Cuenta"},{"key":"username","label":"Usuario"},{"key":"character_name","label":"Personaje"},{"key":"acore_ip","label":"IP"},{"key":"email","label":"Email"},{"key":"product_name","label":"Producto"},{"key":"amount","label":"Monto"},{"key":"timestamp","label":"Fecha"},{"key":"mode","label":"Modo"}]'
|
||||||
<th>Usuario</th> <!-- Columna para el nombre de la cuenta -->
|
data-empty-text="No hay movimientos"></div>
|
||||||
<th>Personaje</th>
|
{{ transactions_data|json_script:"history-table-data" }}
|
||||||
<th>IP</th>
|
{% vite_asset 'src/entries/history_table.tsx' %}
|
||||||
<th>Email</th>
|
|
||||||
<th>Producto</th>
|
|
||||||
<th>Monto</th>
|
|
||||||
<th>Fecha</th>
|
|
||||||
<th>Modo</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% if transactions %}
|
|
||||||
{% for transaction in transactions %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ transaction.account_id }}</td> <!-- ID de la cuenta -->
|
|
||||||
<td>{{ transaction.username }}</td> <!-- Nombre de la cuenta -->
|
|
||||||
<td>{{ transaction.character_name }}</td>
|
|
||||||
<td>{{ transaction.acore_ip }}</td> <!-- IP de acore_auth -->
|
|
||||||
<td>{{ transaction.email }}</td> <!-- Email -->
|
|
||||||
<td>{{ transaction.product_name }}</td>
|
|
||||||
<td>{{ transaction.amount }} €</td>
|
|
||||||
<td>{{ transaction.timestamp|date:"d-m-Y H:i:s" }}</td>
|
|
||||||
<td>{{ transaction.mode }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="9"><span>No hay movimientos</span></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -68,4 +68,17 @@ def security_history_view(request):
|
|||||||
# Obtener los últimos 20 intentos de inicio de sesión
|
# Obtener los últimos 20 intentos de inicio de sesión
|
||||||
login_attempts = LoginAttempt.objects.filter(username=username).order_by('-timestamp')[:20]
|
login_attempts = LoginAttempt.objects.filter(username=username).order_by('-timestamp')[:20]
|
||||||
|
|
||||||
return render(request, 'account/security_history.html', {'login_attempts': login_attempts})
|
login_attempts_data = [
|
||||||
|
{
|
||||||
|
'username': a.username,
|
||||||
|
'status': a.status,
|
||||||
|
'ip_address': a.ip_address,
|
||||||
|
'timestamp': a.timestamp.strftime('%d-%m-%Y %H:%M:%S') if a.timestamp else '',
|
||||||
|
}
|
||||||
|
for a in login_attempts
|
||||||
|
]
|
||||||
|
|
||||||
|
return render(request, 'account/security_history.html', {
|
||||||
|
'login_attempts': login_attempts,
|
||||||
|
'login_attempts_data': login_attempts_data,
|
||||||
|
})
|
||||||
|
|||||||
+19
-1
@@ -86,8 +86,26 @@ def trans_history_view(request):
|
|||||||
else:
|
else:
|
||||||
transactions = []
|
transactions = []
|
||||||
|
|
||||||
|
transactions_data = [
|
||||||
|
{
|
||||||
|
'account_id': t.account_id,
|
||||||
|
'username': t.username,
|
||||||
|
'character_name': t.character_name,
|
||||||
|
'acore_ip': t.acore_ip,
|
||||||
|
'email': t.email,
|
||||||
|
'product_name': t.product_name,
|
||||||
|
'amount': f"{t.amount} €",
|
||||||
|
'timestamp': t.timestamp.strftime('%d-%m-%Y %H:%M:%S') if t.timestamp else '',
|
||||||
|
'mode': t.mode,
|
||||||
|
}
|
||||||
|
for t in transactions
|
||||||
|
]
|
||||||
|
|
||||||
# Renderizamos la vista con el historial de transacciones
|
# Renderizamos la vista con el historial de transacciones
|
||||||
return render(request, 'account/trans_history.html', {'transactions': transactions})
|
return render(request, 'account/trans_history.html', {
|
||||||
|
'transactions': transactions,
|
||||||
|
'transactions_data': transactions_data,
|
||||||
|
})
|
||||||
def trade_points_view(request):
|
def trade_points_view(request):
|
||||||
"""
|
"""
|
||||||
Vista para la página de intercambio de puntos.
|
Vista para la página de intercambio de puntos.
|
||||||
|
|||||||
Reference in New Issue
Block a user