mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2025-10-26 00:36:31 +02:00
restore tool - but not curretly working
This commit is contained in:
16
admin.php
16
admin.php
@@ -32,6 +32,7 @@ use Grav\Plugin\Admin\Popularity;
|
||||
use Grav\Plugin\Admin\Router;
|
||||
use Grav\Plugin\Admin\Themes;
|
||||
use Grav\Plugin\Admin\AdminController;
|
||||
use Grav\Plugin\Admin\SafeUpgradeManager;
|
||||
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
|
||||
use Grav\Plugin\Admin\WhiteLabel;
|
||||
use Grav\Plugin\Form\Form;
|
||||
@@ -383,6 +384,21 @@ class AdminPlugin extends Plugin
|
||||
'reports' => [['admin.super'], 'PLUGIN_ADMIN.REPORTS'],
|
||||
'direct-install' => [['admin.super'], 'PLUGIN_ADMIN.DIRECT_INSTALL'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$manifestFiles = glob(GRAV_ROOT . '/user/data/upgrades/*.json') ?: [];
|
||||
|
||||
if (!$manifestFiles) {
|
||||
$manager = new SafeUpgradeManager(Grav::instance());
|
||||
$manifestFiles = $manager->hasSnapshots() ? [true] : [];
|
||||
}
|
||||
|
||||
if ($manifestFiles) {
|
||||
$event['tools']['restore-grav'] = [['admin.super'], 'PLUGIN_ADMIN.RESTORE_GRAV'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore availability errors, snapshots tool will simply stay hidden
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -341,6 +341,31 @@ class Admin
|
||||
return array_unique($perms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id:string, source_version:?string, target_version:?string, created_at:int, created_at_iso:?string, backup_path:?string, package_path:?string}>
|
||||
*/
|
||||
public function safeUpgradeSnapshots(): array
|
||||
{
|
||||
try {
|
||||
$manager = new SafeUpgradeManager();
|
||||
|
||||
return $manager->listSnapshots();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function safeUpgradeHasSnapshots(): bool
|
||||
{
|
||||
try {
|
||||
$manager = new SafeUpgradeManager();
|
||||
|
||||
return $manager->hasSnapshots();
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the languages available in the site
|
||||
*
|
||||
|
||||
@@ -933,6 +933,102 @@ class AdminController extends AdminBaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a safe-upgrade snapshot via Tools.
|
||||
*
|
||||
* Route: POST /tools/restore-grav?task:safeUpgradeRestore
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function taskSafeUpgradeRestore()
|
||||
{
|
||||
if (!$this->authorizeTask('install grav', ['admin.super'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post = $this->getPost($_POST ?? []);
|
||||
$snapshotId = isset($post['snapshot']) ? (string)$post['snapshot'] : '';
|
||||
|
||||
if ($snapshotId === '') {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error');
|
||||
$this->setRedirect('/tools/restore-grav');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$manager = $this->getSafeUpgradeManager();
|
||||
$result = $manager->restoreSnapshot($snapshotId);
|
||||
|
||||
if (($result['status'] ?? 'error') === 'success') {
|
||||
$manifest = $result['manifest'] ?? [];
|
||||
$version = $manifest['source_version'] ?? $manifest['target_version'] ?? 'unknown';
|
||||
$this->admin->setMessage(
|
||||
sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_SUCCESS'), $snapshotId, $version),
|
||||
'info'
|
||||
);
|
||||
} else {
|
||||
$message = $result['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_FAILED');
|
||||
$this->admin->setMessage($message, 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect('/tools/restore-grav');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one or more safe-upgrade snapshots via Tools.
|
||||
*
|
||||
* Route: POST /tools/restore-grav?task:safeUpgradeDelete
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function taskSafeUpgradeDelete()
|
||||
{
|
||||
if (!$this->authorizeTask('install grav', ['admin.super'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post = $this->getPost($_POST ?? []);
|
||||
$snapshots = $post['snapshots'] ?? [];
|
||||
if (is_string($snapshots)) {
|
||||
$snapshots = [$snapshots];
|
||||
}
|
||||
|
||||
if (empty($snapshots)) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_INVALID'), 'error');
|
||||
$this->setRedirect('/tools/restore-grav');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$manager = $this->getSafeUpgradeManager();
|
||||
$results = $manager->deleteSnapshots($snapshots);
|
||||
|
||||
$success = array_filter($results, static function ($item) {
|
||||
return ($item['status'] ?? 'error') === 'success';
|
||||
});
|
||||
$failed = array_filter($results, static function ($item) {
|
||||
return ($item['status'] ?? 'error') !== 'success';
|
||||
});
|
||||
|
||||
if ($success) {
|
||||
$this->admin->setMessage(
|
||||
sprintf($this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_DELETE_SUCCESS'), count($success)),
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($failed as $entry) {
|
||||
$message = $entry['message'] ?? $this->admin::translate('PLUGIN_ADMIN.RESTORE_GRAV_DELETE_FAILED');
|
||||
$this->admin->setMessage($message, 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect('/tools/restore-grav');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles uninstalling plugins and themes
|
||||
*
|
||||
|
||||
@@ -120,6 +120,164 @@ class SafeUpgradeManager
|
||||
$this->setJobId(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, source_version:?string, target_version:?string, created_at:int, created_at_iso:?string, backup_path:?string, package_path:?string}>
|
||||
*/
|
||||
public function listSnapshots(): array
|
||||
{
|
||||
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
|
||||
if (!is_dir($manifestDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob($manifestDir . '/*.json') ?: [];
|
||||
rsort($files);
|
||||
|
||||
$snapshots = [];
|
||||
foreach ($files as $file) {
|
||||
$decoded = json_decode(file_get_contents($file) ?: '', true);
|
||||
if (!is_array($decoded) || empty($decoded['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$createdAt = isset($decoded['created_at']) ? (int)$decoded['created_at'] : 0;
|
||||
|
||||
$snapshots[] = [
|
||||
'id' => (string)$decoded['id'],
|
||||
'source_version' => $decoded['source_version'] ?? null,
|
||||
'target_version' => $decoded['target_version'] ?? null,
|
||||
'created_at' => $createdAt,
|
||||
'created_at_iso' => $createdAt > 0 ? date('c', $createdAt) : null,
|
||||
'backup_path' => $decoded['backup_path'] ?? null,
|
||||
'package_path' => $decoded['package_path'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
public function hasSnapshots(): bool
|
||||
{
|
||||
return !empty($this->listSnapshots());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $snapshotId
|
||||
* @return array{status:string,message:?string,manifest:array|null}
|
||||
*/
|
||||
public function restoreSnapshot(string $snapshotId): array
|
||||
{
|
||||
try {
|
||||
$safeUpgrade = $this->getSafeUpgradeService();
|
||||
$manifest = $safeUpgrade->rollback($snapshotId);
|
||||
} catch (RuntimeException $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'manifest' => null,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'manifest' => null,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$manifest) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => sprintf('Snapshot %s not found.', $snapshotId),
|
||||
'manifest' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => null,
|
||||
'manifest' => $manifest,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $snapshotIds
|
||||
* @return array<int, array{id:string,status:string,message:?string}>
|
||||
*/
|
||||
public function deleteSnapshots(array $snapshotIds): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('strval', $snapshotIds))));
|
||||
$results = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$results[] = $this->deleteSnapshot($id);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $snapshotId
|
||||
* @return array{id:string,status:string,message:?string}
|
||||
*/
|
||||
protected function deleteSnapshot(string $snapshotId): array
|
||||
{
|
||||
$manifestDir = GRAV_ROOT . '/user/data/upgrades';
|
||||
$manifestPath = $manifestDir . '/' . $snapshotId . '.json';
|
||||
|
||||
if (!is_file($manifestPath)) {
|
||||
return [
|
||||
'id' => $snapshotId,
|
||||
'status' => 'error',
|
||||
'message' => sprintf('Snapshot %s not found.', $snapshotId),
|
||||
];
|
||||
}
|
||||
|
||||
$manifest = json_decode(file_get_contents($manifestPath) ?: '', true);
|
||||
if (!is_array($manifest)) {
|
||||
return [
|
||||
'id' => $snapshotId,
|
||||
'status' => 'error',
|
||||
'message' => sprintf('Snapshot %s manifest is corrupted.', $snapshotId),
|
||||
];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach (['package_path', 'backup_path'] as $key) {
|
||||
$path = isset($manifest[$key]) ? (string)$manifest[$key] : '';
|
||||
if ($path === '' || !file_exists($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (is_dir($path)) {
|
||||
Folder::delete($path);
|
||||
} else {
|
||||
@unlink($path);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!@unlink($manifestPath)) {
|
||||
$errors[] = sprintf('Unable to delete manifest file %s.', $manifestPath);
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
return [
|
||||
'id' => $snapshotId,
|
||||
'status' => 'error',
|
||||
'message' => implode(' ', $errors),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $snapshotId,
|
||||
'status' => 'success',
|
||||
'message' => sprintf('Snapshot %s removed.', $snapshotId),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getJobDir(string $jobId): string
|
||||
{
|
||||
return $this->jobsDir . '/' . $jobId;
|
||||
|
||||
@@ -540,8 +540,8 @@ PLUGIN_ADMIN:
|
||||
SAFE_UPGRADE_STAGE_COMPLETE: "Upgrade complete"
|
||||
SAFE_UPGRADE_STAGE_ERROR: "Upgrade encountered an error"
|
||||
SAFE_UPGRADE_RESULT_SUCCESS: "Grav upgraded to v%s"
|
||||
SAFE_UPGRADE_RESULT_MANIFEST: "Snapshot reference: %s"
|
||||
SAFE_UPGRADE_RESULT_ROLLBACK: "Rollback snapshot stored at: %s"
|
||||
SAFE_UPGRADE_RESULT_MANIFEST: "Snapshot reference: <code>%s</code>"
|
||||
SAFE_UPGRADE_RESULT_HINT: "Restore snapshots from Tools → Restore Grav."
|
||||
SAFE_UPGRADE_RESULT_NOOP: "Grav is already up to date."
|
||||
SAFE_UPGRADE_RESULT_FAILURE: "Safe upgrade failed"
|
||||
OF_THIS: "of this"
|
||||
@@ -840,6 +840,21 @@ PLUGIN_ADMIN:
|
||||
TOOLS_DIRECT_INSTALL_URL_TITLE: "Install Package via Remote URL Reference"
|
||||
TOOLS_DIRECT_INSTALL_URL_DESC: "Alternatively, you can also reference a full URL to the package ZIP file and install it via this remote URL."
|
||||
TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Upload and install"
|
||||
RESTORE_GRAV: "Restore Grav"
|
||||
RESTORE_GRAV_DESC: "Select a snapshot created by Safe Upgrade to restore your site or remove snapshots you no longer need."
|
||||
RESTORE_GRAV_TABLE_SNAPSHOT: "Snapshot"
|
||||
RESTORE_GRAV_TABLE_VERSION: "Version"
|
||||
RESTORE_GRAV_TABLE_CREATED: "Created"
|
||||
RESTORE_GRAV_TABLE_LOCATION: "Snapshot path"
|
||||
RESTORE_GRAV_TABLE_ACTIONS: "Actions"
|
||||
RESTORE_GRAV_RESTORE_BUTTON: "Restore"
|
||||
RESTORE_GRAV_DELETE_SELECTED: "Delete Selected"
|
||||
RESTORE_GRAV_NONE: "No safe upgrade snapshots are currently available."
|
||||
RESTORE_GRAV_INVALID: "Select at least one snapshot before continuing."
|
||||
RESTORE_GRAV_SUCCESS: "Snapshot %s restored (Grav %s)."
|
||||
RESTORE_GRAV_FAILED: "Unable to restore the selected snapshot."
|
||||
RESTORE_GRAV_DELETE_SUCCESS: "%d snapshot(s) deleted."
|
||||
RESTORE_GRAV_DELETE_FAILED: "Failed to delete one or more snapshots."
|
||||
ROUTE_OVERRIDES: "Route Overrides"
|
||||
ROUTE_DEFAULT: "Default Route"
|
||||
ROUTE_CANONICAL: "Canonical Route"
|
||||
|
||||
@@ -859,14 +859,13 @@ export default class SafeUpgrade {
|
||||
if (status === 'success' || status === 'finalized') {
|
||||
const manifest = result.manifest || {};
|
||||
const target = result.version || manifest.target_version || '';
|
||||
const backup = manifest.backup_path || '';
|
||||
const identifier = manifest.id || '';
|
||||
|
||||
this.steps.result.html(`
|
||||
<div class="safe-upgrade-result success">
|
||||
<h3>${r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s')}</h3>
|
||||
${identifier ? `<p>${r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s')}</p>` : ''}
|
||||
${backup ? `<p>${r('SAFE_UPGRADE_RESULT_ROLLBACK', backup, 'Rollback snapshot stored at: %s')}</p>` : ''}
|
||||
${identifier ? `<p>${r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: <code>%s</code>')}</p>` : ''}
|
||||
<p>${t('SAFE_UPGRADE_RESULT_HINT', 'Restore snapshots from Tools → Restore Grav.')}</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
||||
3
themes/grav/js/admin.min.js
vendored
3
themes/grav/js/admin.min.js
vendored
@@ -5258,9 +5258,8 @@ var SafeUpgrade = /*#__PURE__*/function () {
|
||||
if (status === 'success' || status === 'finalized') {
|
||||
var manifest = result.manifest || {};
|
||||
var target = result.version || manifest.target_version || '';
|
||||
var backup = manifest.backup_path || '';
|
||||
var identifier = manifest.id || '';
|
||||
this.steps.result.html("\n <div class=\"safe-upgrade-result success\">\n <h3>".concat(r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s'), "</h3>\n ").concat(identifier ? "<p>".concat(r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: %s'), "</p>") : '', "\n ").concat(backup ? "<p>".concat(r('SAFE_UPGRADE_RESULT_ROLLBACK', backup, 'Rollback snapshot stored at: %s'), "</p>") : '', "\n </div>\n "));
|
||||
this.steps.result.html("\n <div class=\"safe-upgrade-result success\">\n <h3>".concat(r('SAFE_UPGRADE_RESULT_SUCCESS', target, 'Grav upgraded to v%s'), "</h3>\n ").concat(identifier ? "<p>".concat(r('SAFE_UPGRADE_RESULT_MANIFEST', identifier, 'Snapshot reference: <code>%s</code>'), "</p>") : '', "\n <p>").concat(t('SAFE_UPGRADE_RESULT_HINT', 'Restore snapshots from Tools → Restore Grav.'), "</p>\n </div>\n "));
|
||||
this.switchStep('result');
|
||||
external_jQuery_default()('[data-gpm-grav]').remove();
|
||||
if (target) {
|
||||
|
||||
74
themes/grav/templates/partials/tools-restore-grav.html.twig
Normal file
74
themes/grav/templates/partials/tools-restore-grav.html.twig
Normal file
@@ -0,0 +1,74 @@
|
||||
<h1>{{ "PLUGIN_ADMIN.RESTORE_GRAV"|t }}</h1>
|
||||
|
||||
<div class="restore-grav-content">
|
||||
{% set snapshots = admin.safeUpgradeSnapshots() %}
|
||||
|
||||
{% if snapshots %}
|
||||
<p class="restore-grav-intro">
|
||||
{{ "PLUGIN_ADMIN.RESTORE_GRAV_DESC"|t }}
|
||||
</p>
|
||||
|
||||
<form id="snapshot-delete-form" action="{{ admin_route('/tools/restore-grav') }}" method="post" class="snapshot-delete-form">
|
||||
<input type="hidden" name="task" value="safeUpgradeDelete">
|
||||
{{ nonce_field('admin-form', 'admin-nonce')|raw }}
|
||||
</form>
|
||||
|
||||
<table class="restore-grav-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox-cell"></th>
|
||||
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_SNAPSHOT"|t }}</th>
|
||||
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_VERSION"|t }}</th>
|
||||
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_CREATED"|t }}</th>
|
||||
<th>{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_LOCATION"|t }}</th>
|
||||
<th class="actions-cell">{{ "PLUGIN_ADMIN.RESTORE_GRAV_TABLE_ACTIONS"|t }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snapshot in snapshots %}
|
||||
{% set version = snapshot.source_version ?: snapshot.target_version %}
|
||||
<tr>
|
||||
<td class="checkbox-cell">
|
||||
<input type="checkbox" name="snapshots[]" value="{{ snapshot.id }}" form="snapshot-delete-form" />
|
||||
</td>
|
||||
<td><code>{{ snapshot.id }}</code></td>
|
||||
<td>{{ version ?: "PLUGIN_ADMIN.UNKNOWN"|t }}</td>
|
||||
<td>
|
||||
{% if snapshot.created_at %}
|
||||
{{ snapshot.created_at|date('Y-m-d H:i:s') }}
|
||||
<span class="hint">{{ snapshot.created_at|nicetime(false, false) }}</span>
|
||||
{% else %}
|
||||
{{ "PLUGIN_ADMIN.UNKNOWN"|t }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if snapshot.backup_path %}
|
||||
<code class="snapshot-path">{{ snapshot.backup_path }}</code>
|
||||
{% else %}
|
||||
{{ "PLUGIN_ADMIN.UNKNOWN"|t }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<form action="{{ admin_route('/tools/restore-grav') }}" method="post" class="inline-form">
|
||||
<input type="hidden" name="task" value="safeUpgradeRestore">
|
||||
<input type="hidden" name="snapshot" value="{{ snapshot.id }}">
|
||||
{{ nonce_field('admin-form', 'admin-nonce')|raw }}
|
||||
<button type="submit" class="button">{{ "PLUGIN_ADMIN.RESTORE_GRAV_RESTORE_BUTTON"|t }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="button-bar">
|
||||
<button type="submit" form="snapshot-delete-form" class="button danger" name="task" value="safeUpgradeDelete">
|
||||
{{ "PLUGIN_ADMIN.RESTORE_GRAV_DELETE_SELECTED"|t }}
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="notice secondary">
|
||||
<p>{{ "PLUGIN_ADMIN.RESTORE_GRAV_NONE"|t }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
{% set tools_slug = uri.basename %}
|
||||
{% if tools_slug == 'tools' %}{% set tools_slug = 'backups' %}{% endif %}
|
||||
{% set title = "PLUGIN_ADMIN.TOOLS"|t ~ ": " ~ ("PLUGIN_ADMIN." ~ tools_slug|underscorize|upper)|t %}
|
||||
{% set tools = admin.tools() %}
|
||||
{% if tools[tools_slug] is not defined %}
|
||||
{% set tools_slug = tools|keys|first %}
|
||||
{% endif %}
|
||||
{% set title = "PLUGIN_ADMIN.TOOLS"|t ~ ": " ~ ("PLUGIN_ADMIN." ~ tools_slug|underscorize|upper)|t %}
|
||||
|
||||
{% set titlebar -%}
|
||||
{% include 'partials/tools-' ~ tools_slug ~ '-titlebar.html.twig' ignore missing %}
|
||||
@@ -47,4 +50,3 @@
|
||||
<h1>Unauthorized</h1>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user