Allocation tokens can be managed from panel, fixed #164

This commit is contained in:
Pinga 2025-03-31 15:17:38 +03:00
parent 18e6eafb41
commit 664102d65d
13 changed files with 365 additions and 64 deletions

View file

@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Container\ContainerInterface;
use Respect\Validation\Validator as v;
use League\ISO3166\ISO3166;
use Ramsey\Uuid\Uuid;
class SystemController extends Controller
{
@ -1307,24 +1308,90 @@ class SystemController extends Controller
]);
}
public function generateTokens(Request $request, Response $response)
{
if ($_SESSION["auth_roles"] != 0) {
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
$db = $this->container->get('db');
try {
$currentDateTime = new \DateTime();
$crdate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp
$db->beginTransaction();
for ($i = 0; $i < 10; $i++) {
$uuid = Uuid::uuid4()->toString();
$db->insert('allocation_tokens', [
'token' => $uuid,
'domain_name' => null,
'tokenStatus' => 'new',
'tokenType' => 'simple',
'crdate' => $crdate
]);
}
$db->commit();
$this->container->get('flash')->addMessage('success', '10 tokens successfully created');
return $response->withHeader('Location', '/registry/tokens')->withStatus(302);
} catch (Exception $e) {
$db->rollBack();
$this->container->get('flash')->addMessage('error', 'Database failure: ' . $e->getMessage());
return $response->withHeader('Location', '/registry/tokens')->withStatus(302);
}
}
public function manageTokens(Request $request, Response $response)
{
if ($_SESSION["auth_roles"] != 0) {
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
if ($request->getMethod() === 'POST') {
$uri = $request->getUri()->getPath();
return view($response,'admin/system/manageTokens.twig', [
'currentUri' => $uri
]);
}
public function deleteToken(Request $request, Response $response, $args)
{
if ($_SESSION["auth_roles"] != 0) {
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
$db = $this->container->get('db');
$uri = $request->getUri()->getPath();
$tokens = $db->select("SELECT * FROM allocation_tokens");
// if ($request->getMethod() === 'POST') {
$db = $this->container->get('db');
// Get the current URI
$uri = $request->getUri()->getPath();
return view($response,'admin/system/manageTokens.twig', [
'tokens' => $tokens,
'currentUri' => $uri
]);
if ($args) {
$args = trim($args);
if (!preg_match('/^[a-zA-Z0-9\-]+$/', $args)) {
$this->container->get('flash')->addMessage('error', 'Invalid token format');
return $response->withHeader('Location', '/registry/tokens')->withStatus(302);
}
$db->delete(
'allocation_tokens',
[
'token' => $args
]
);
$this->container->get('flash')->addMessage('success', 'Token ' . $args . ' deleted successfully');
return $response->withHeader('Location', '/registry/tokens')->withStatus(302);
} else {
// Redirect to the tokens view
return $response->withHeader('Location', '/registry/tokens')->withStatus(302);
}
//}
}
public function managePromo(Request $request, Response $response)

View file

@ -1698,3 +1698,15 @@ msgstr "تصدير جدول IDN"
msgid "Published"
msgstr "منشور"
msgid "Manage Allocation Tokens"
msgstr "إدارة رموز التخصيص"
msgid "Generate 10 Tokens"
msgstr "إنشاء 10 رموز"
msgid "Token"
msgstr "رمز"
msgid "Are you sure you want to delete this token?"
msgstr "هل أنت متأكد أنك تريد حذف هذا الرمز؟"

View file

@ -1701,3 +1701,15 @@ msgstr "Export IDN Table"
msgid "Published"
msgstr "Published"
msgid "Manage Allocation Tokens"
msgstr "Manage Allocation Tokens"
msgid "Generate 10 Tokens"
msgstr "Generate 10 Tokens"
msgid "Token"
msgstr "Token"
msgid "Are you sure you want to delete this token?"
msgstr "Are you sure you want to delete this token?"

View file

@ -1701,3 +1701,15 @@ msgstr "Exportar tabla IDN"
msgid "Published"
msgstr "Publicado"
msgid "Manage Allocation Tokens"
msgstr "Gestionar los tokens de asignación"
msgid "Generate 10 Tokens"
msgstr "Generar 10 tokens"
msgid "Token"
msgstr "Token"
msgid "Are you sure you want to delete this token?"
msgstr "¿Está seguro de que desea eliminar este token?"

View file

@ -1701,3 +1701,15 @@ msgstr "Exporter le tableau IDN"
msgid "Published"
msgstr "Publié"
msgid "Manage Allocation Tokens"
msgstr "Gérer les jetons dallocation"
msgid "Generate 10 Tokens"
msgstr "Générer 10 jetons"
msgid "Token"
msgstr "Jeton"
msgid "Are you sure you want to delete this token?"
msgstr "Êtes-vous sûr de vouloir supprimer ce jeton ?"

View file

@ -1701,3 +1701,15 @@ msgstr "IDNテーブルをエクスポート"
msgid "Published"
msgstr "公開済み"
msgid "Manage Allocation Tokens"
msgstr "割り当てトークンの管理"
msgid "Generate 10 Tokens"
msgstr "トークンを10個生成"
msgid "Token"
msgstr "トークン"
msgid "Are you sure you want to delete this token?"
msgstr "このトークンを削除してもよろしいですか?"

View file

@ -1701,3 +1701,15 @@ msgstr ""
msgid "Published"
msgstr ""
msgid "Manage Allocation Tokens"
msgstr ""
msgid "Generate 10 Tokens"
msgstr ""
msgid "Token"
msgstr ""
msgid "Are you sure you want to delete this token?"
msgstr ""

View file

@ -1701,3 +1701,15 @@ msgstr "Exportar tabela IDN"
msgid "Published"
msgstr "Publicado"
msgid "Manage Allocation Tokens"
msgstr "Gerenciar tokens de alocação"
msgid "Generate 10 Tokens"
msgstr "Gerar 10 tokens"
msgid "Token"
msgstr "Token"
msgid "Are you sure you want to delete this token?"
msgstr "Tem certeza de que deseja excluir este token?"

View file

@ -1701,3 +1701,15 @@ msgstr "Експортувати таблицю IDN"
msgid "Published"
msgstr "Опубліковано"
msgid "Manage Allocation Tokens"
msgstr "Керування токенами розподілу"
msgid "Generate 10 Tokens"
msgstr "Створити 10 токенів"
msgid "Token"
msgstr "Токен"
msgid "Are you sure you want to delete this token?"
msgstr "Ви впевнені, що хочете видалити цей токен?"

View file

@ -17,6 +17,18 @@
{{ __('Manage Allocation Tokens') }}
</h2>
</div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="/registry/tokens/generate" class="btn btn-primary d-none d-sm-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12l8 -4.5" /><path d="M12 12v9" /><path d="M12 12l-8 -4.5" /><path d="M12 12l8 4.5" /><path d="M12 3v9" /><path d="M12 12l-8 4.5" /></svg>
{{ __('Generate 10 Tokens') }}
</a>
<a href="/registry/tokens/generate" class="btn btn-primary d-sm-none btn-icon" aria-label="{{ __('Generate 10 Tokens') }}" title="{{ __('Generate 10 Tokens') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12l8 -4.5" /><path d="M12 12v9" /><path d="M12 12l-8 -4.5" /><path d="M12 12l8 4.5" /><path d="M12 3v9" /><path d="M12 12l-8 4.5" /></svg>
</a>
</div>
</div>
</div>
</div>
</div>
@ -24,61 +36,28 @@
<div class="page-body">
<div class="container-xl">
<div class="col-12">
<div class="alert alert-info" role="alert">
<div class="alert-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M12 9h.01" /><path d="M11 12h1v4h1" /></svg>
</div>
<div>
<h4 class="alert-heading">{{ __('Note on Allocation Tokens') }}</h4>
<div class="alert-description">{{ __('For the moment, allocation tokens are managed directly via the database and can only be viewed in the control panel.') }}</div>
</div>
</div>
{% include 'partials/flash.twig' %}
<div class="card">
<form action="/registry/tokens" method="post">
{{ csrf.field | raw }}
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>{{ __('Token') }}</th>
<th>{{ __('Domain') }}</th>
<th>{{ __('Type') }}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Registrars') }}</th>
<th>{{ __('TLDs') }}</th>
<th>{{ __('Actions') }}</th>
<th>{{ __('Premiums') }}</th>
<th>{{ __('Discounts') }}</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
{% if tokens is not empty %}
{% for token in tokens %}
<tr>
<td class="user-select-all">{{ token.token }}</td>
<td><strong>{{ token.domain_name }}</strong></td>
<td>{{ token.tokenType is empty ? 'Default' : token.tokenType }}</td>
<td><span class="status status-blue">{{ token.tokenStatus is empty ? 'OK' : token.tokenStatus }}</span></td>
<td class="text-secondary">{{ token.registrars is empty ? 'All' : token.registrars }}</td>
<td class="text-secondary">{{ token.tlds is empty ? 'All' : token.tlds }}</td>
<td class="text-secondary">{{ token.eppActions is empty ? 'All' : token.eppActions }}</td>
<td class="text-secondary">{{ token.reducePremium is empty ? 'N/A' : token.reducePremium }}</td>
<td class="text-secondary">{{ token.reduceYears is empty ? 'N/A' : token.reduceYears }}</td>
<td>
<a href="#">Edit</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="10">No tokens found.</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="card-body py-3">
<div class="d-flex">
<div class="text-secondary">
<button class="btn btn-info btn-icon" onclick="downloadCSV()" title="Export page"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 15a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1"></path><path d="M17 8l2 8l2 -8"></path><path d="M7 10a2 2 0 1 0 -4 0v4a2 2 0 1 0 4 0"></path></svg></button>
<button class="btn btn-info btn-icon" onclick="downloadJSON()" title="Export page"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M20 16v-8l3 8v-8"></path><path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"></path><path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"></path><path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"></path></svg></button>
<button class="btn btn-green btn-icon" onclick="downloadXLSX()" title="Export page"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M14 3v4a1 1 0 0 0 1 1h4"></path><path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path><path d="M4 15l4 6"></path><path d="M4 21l4 -6"></path><path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path><path d="M11 15v6h3"></path></svg></button>
<button class="btn btn-red btn-icon" onclick="downloadPDF()" title="Export page"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 8v8h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-2z"></path><path d="M3 12h2a2 2 0 1 0 0 -4h-2v8"></path><path d="M17 12h3"></path><path d="M21 8h-4v8"></path></svg></button>
</div>
<div class="ms-auto text-secondary">
{{ __('Search') }}:
<div class="ms-2 d-inline-block">
<input id="search-input" type="text" class="form-control" aria-label="{{ __('Search tokens') }}" autocapitalize="none">
</div>
</div>
</div>
</div>
<div class="table-responsive">
<div id="tokenTable"></div>
</div>
<div class="card-footer d-flex align-items-center"><div id="page-count"></div></div>
</div>
</div>
</div>

View file

@ -7,7 +7,7 @@
<title>{% block title %}{% endblock %} | Namingo</title>
<meta name="theme-color" content="#066fd1">
<!-- CSS files -->
{% if route_is('domains') or route_is('applications') or route_is('contacts') or route_is('hosts') or route_is('epphistory') or is_current_url('registrars') or route_is('transactions') or route_is('overview') or route_is('reports') or route_is('transfers') or route_is('users') or is_current_url('ticketview') or route_is('poll') or route_is('log') or route_is('invoices') or route_is('registry/tlds') or route_is('profile') %}{% include 'partials/css-tables.twig' %}{% else %}{% include 'partials/css.twig' %}{% endif %}
{% if route_is('domains') or route_is('applications') or route_is('contacts') or route_is('hosts') or route_is('epphistory') or is_current_url('registrars') or route_is('transactions') or route_is('overview') or route_is('reports') or route_is('transfers') or route_is('users') or is_current_url('ticketview') or route_is('poll') or route_is('log') or route_is('invoices') or route_is('registry/tlds') or route_is('profile') or route_is('registry/tokens') %}{% include 'partials/css-tables.twig' %}{% else %}{% include 'partials/css.twig' %}{% endif %}
</head>
<body{% if screen_mode == 'dark' %} data-bs-theme="dark"{% endif %}>
<div class="page">
@ -308,6 +308,8 @@
{% include 'partials/js-server.twig' %}
{% elseif route_is('deposit') %}
{% include 'partials/js-server.twig' %}
{% elseif route_is('registry/tokens') %}
{% include 'partials/js-tokens.twig' %}
{% else %}
{% include 'partials/js.twig' %}
{% endif %}

View file

@ -0,0 +1,155 @@
<script src="/assets/js/tabulator.min.js" defer></script>
<script src="/assets/js/sweetalert2.min.js" defer></script>
<script src="/assets/js/tabler.min.js" defer></script>
<script src="/assets/js/xlsx.full.min.js" defer></script>
<script src="/assets/js/jspdf.umd.min.js" defer></script>
<script src="/assets/js/jspdf.plugin.autotable.min.js" defer></script>
<script>
var table;
document.addEventListener("DOMContentLoaded", function(){
function copyableFormatter(cell) {
const displayName = cell.getValue();
return `<span style="cursor:text; font-weight:bold;"
onclick="window.getSelection().selectAllChildren(this)">
${displayName}
</span>`;
}
function typeStatusFormatter(cell) {
const type = cell.getValue();
let colorClass = "blue"; // default
switch (type) {
case "simple":
colorClass = "blue";
break;
case "advanced":
colorClass = "orange";
break;
case "complex":
colorClass = "red";
break;
default:
colorClass = "gray";
}
return `<span class="status status-${colorClass}">${type}</span>`;
}
function lifecycleStatusFormatter(cell) {
const status = cell.getValue();
let colorClass = "gray"; // fallback
switch (status) {
case "new":
colorClass = "green";
break;
case "used":
colorClass = "yellow";
break;
case "deprecated":
colorClass = "red";
break;
default:
colorClass = "gray";
}
return `<span class="status status-${colorClass}">${status}</span>`;
}
function domainFormatter(cell) {
const value = cell.getValue();
if (value) {
return `<strong>${value}</strong>`;
} else {
return `<span class="status status-info">any</span>`;
}
}
function actionsFormatter(cell, formatterParams, onRendered) {
return `<a class="btn btn-outline-danger btn-icon delete-btn" id="delete-btn" href="javascript:void(0);" data-delete-url="/registry/tokens/delete/${cell.getRow().getData().token}" title="{{ __('Delete Token') }}"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M4 7h16"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path><path d="M10 12l4 4m0 -4l-4 4"></path></svg></a>`;
}
table = new Tabulator("#tokenTable", {
ajaxURL:"/api/records/allocation_tokens", // Set the URL for your JSON data
ajaxConfig:"GET",
pagination:"local",
paginationSize: 10,
paginationSizeSelector:[10, 25, 50, 100],
paginationCounter:"rows",
paginationCounterElement:"#page-count",
clipboard:true,
clipboardPasteAction:"replace",
ajaxResponse:function(url, params, response){
return response.records;
},
layout:"fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseStartOpen:false,
resizableColumns:false,
placeholder: "{{ __('No Data') }}",
columns:[
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0},
{title:"{{ __('Token') }}", field:"token", minWidth:300, minWidth:100, headerSort:true, resizable:false, formatter: copyableFormatter, responsive:0},
{ title: "{{ __('Domain') }}", field: "domain_name", width:300, minWidth:80, headerSort:true, resizable:false, formatter: domainFormatter, responsive:2},
{title:"{{ __('Type') }}", field:"tokenType", width:120, minWidth:80, headerSort:true, resizable:false, formatter: typeStatusFormatter, responsive:2},
{title:"{{ __('Status') }}", field:"tokenStatus", width:120, minWidth:80, headerSort:true, resizable:false, formatter: lifecycleStatusFormatter, responsive:2},
{title: "{{ __('Actions') }}", formatter: actionsFormatter, resizable:false, headerSort:false, download:false, hozAlign: "center", responsive:0, cellClick: function(e, cell){
if (e.target.closest('.delete-btn')) {
e.preventDefault(); // Prevent the default link behavior
Swal.fire({
title: "{{ __('Are you sure you want to delete this token?') }}",
showCancelButton: true,
confirmButtonText: "{{ __('Confirm') }}"
}).then((result) => {
if (result.isConfirmed) {
let deleteUrl = e.target.closest('.delete-btn').getAttribute('data-delete-url');
window.location.href = deleteUrl;
}
});
}
}},
]
});
var searchInput = document.getElementById("search-input");
searchInput.addEventListener("input", function () {
var term = searchInput.value.toLowerCase();
if (term) { // Only apply the filter when there's a term to search for
table.setFilter(function (data) {
// Check if any of the fields contain the search term
return (
String(data.token).toLowerCase().includes(term) ||
String(data.domain_name).toLowerCase().includes(term) ||
String(data.tokenType).toLowerCase().includes(term) ||
String(data.tokenStatus).toLowerCase().includes(term)
);
});
} else {
table.clearFilter(); // Clear the filter when the search box is emptied
}
});
});
function downloadCSV() {
table.download("csv", "tokens.csv");
}
function downloadJSON() {
table.download("json", "tokens.json");
}
function downloadXLSX() {
table.download("xlsx", "tokens.xlsx", {sheetName:"My Tokens"});
}
function downloadPDF() {
table.download("pdf", "tokens.pdf", {
orientation:"portrait",
title:"My Tokens"
});
}
</script>

View file

@ -139,7 +139,9 @@ $app->group('', function ($route) {
$route->map(['GET', 'POST'], '/registry/tld/{tld}', SystemController::class . ':manageTld')->setName('manageTld');
$route->get('/registry/tlds', SystemController::class .':listTlds')->setName('listTlds');
$route->map(['GET', 'POST'], '/registry/reserved', SystemController::class .':manageReserved')->setName('manageReserved');
$route->map(['GET', 'POST'], '/registry/tokens', SystemController::class .':manageTokens')->setName('manageTokens');
$route->get('/registry/tokens', SystemController::class .':manageTokens')->setName('manageTokens');
$route->get('/registry/tokens/generate', SystemController::class .':generateTokens')->setName('generateTokens');
$route->map(['GET', 'POST'], '/registry/tokens/delete/{token}', SystemController::class . ':deleteToken')->setName('deleteToken');
$route->post('/registry/promotions', SystemController::class . ':managePromo')->setName('managePromo');
$route->post('/registry/phases', SystemController::class . ':managePhases')->setName('managePhases');
$route->get('/registry/idnexport/{script}', SystemController::class .':idnexport')->setName('idnexport');