feat: rewrite website in PHP, drop Node.js stack
Replace React/Vite frontend + Fastify API with a single PHP application. - Front controller router in public/index.php (clean URLs via .htaccess) - Session-based auth (no JWT) — register, login, verify email, forgot/reset password, logout - Stripe checkout sessions, billing portal, webhook handler (checkout.session.completed, subscription.updated/deleted) - License IP change via internal licensing server API - PHP classes: DB (PDO), Auth, Mailer (PHPMailer), StripeClient - Views: home, pricing, install, login, register, dashboard, verify-email, forgot-password, reset-password, 404 - Tailwind CSS via CDN — no build step required - Composer dependencies: stripe/stripe-php, phpmailer/phpmailer, vlucas/phpdotenv, ramsey/uuid - CSRF protection on all POST forms - PRG pattern with session flash messages Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
72f5cc1060
commit
faf0818dde
@@ -0,0 +1,10 @@
|
||||
<?php $pageTitle = '404 — NixPanel'; ?>
|
||||
|
||||
<div class="min-h-[80vh] flex items-center justify-center px-4 text-center">
|
||||
<div>
|
||||
<p class="text-8xl font-extrabold text-brand-600 mb-4">404</p>
|
||||
<h1 class="text-2xl font-bold text-white mb-3">Page not found</h1>
|
||||
<p class="text-gray-400 mb-8">The page you're looking for doesn't exist.</p>
|
||||
<a href="/" class="btn-primary px-8 py-3">Go home</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php $pageTitle = 'Dashboard — NixPanel'; ?>
|
||||
<?php
|
||||
$planLabels = ['free' => 'Free', 'solo' => 'Solo', 'host' => 'Host', 'agency' => 'Agency'];
|
||||
?>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h1 class="text-2xl font-bold text-white mb-8">Dashboard</h1>
|
||||
|
||||
<?php if (isset($_GET['checkout']) && $_GET['checkout'] === 'success'): ?>
|
||||
<div class="bg-emerald-900/30 border border-emerald-500/30 text-emerald-300 rounded-xl px-5 py-4 mb-6 text-sm">
|
||||
🎉 Subscription activated! Your license has been provisioned.
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Account -->
|
||||
<div class="card mb-6">
|
||||
<h2 class="font-semibold text-white mb-4">Account</h2>
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Name</p>
|
||||
<p class="text-white"><?= e($customer['full_name']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Email</p>
|
||||
<p class="text-white"><?= e($customer['email']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscription -->
|
||||
<?php if ($subscription): ?>
|
||||
<div class="card mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="font-semibold text-white">Subscription</h2>
|
||||
<span class="text-xs font-medium px-2.5 py-1 rounded-full <?= $subscription['status'] === 'active' ? 'bg-emerald-900/40 text-emerald-400' : 'bg-yellow-900/40 text-yellow-400' ?>">
|
||||
<?= e($subscription['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-3 gap-4 text-sm mb-6">
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Plan</p>
|
||||
<p class="text-white font-medium"><?= e($planLabels[$subscription['plan_key']] ?? $subscription['plan_key']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Billing</p>
|
||||
<p class="text-white capitalize"><?= e($subscription['billing_interval']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Renews</p>
|
||||
<p class="text-white"><?= date('M j, Y', strtotime($subscription['current_period_end'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($subscription['cancel_at_period_end']): ?>
|
||||
<p class="text-yellow-400 text-sm mb-4">
|
||||
⚠ Cancels on <?= date('M j, Y', strtotime($subscription['current_period_end'])) ?>
|
||||
</p>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<form method="POST" action="/billing/portal">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="btn-secondary text-sm">Manage billing</button>
|
||||
</form>
|
||||
<?php if (!$subscription['cancel_at_period_end']): ?>
|
||||
<form method="POST" action="/billing/cancel"
|
||||
onsubmit="return confirm('Cancel at end of billing period?')">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-red-500/30 text-red-400 hover:bg-red-900/20 text-sm transition-colors">
|
||||
Cancel plan
|
||||
</button>
|
||||
</form>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card mb-6 text-center py-10">
|
||||
<p class="text-gray-400 mb-4">No active subscription.</p>
|
||||
<a href="/pricing" class="btn-primary px-6 py-2.5">View plans</a>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- License -->
|
||||
<?php if ($license): ?>
|
||||
<div class="card mb-6">
|
||||
<h2 class="font-semibold text-white mb-4">License</h2>
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-sm mb-6">
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">License ID</p>
|
||||
<p class="text-white font-mono text-xs truncate"><?= e($license['id']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Server IP</p>
|
||||
<p class="text-white font-mono"><?= e($license['server_ip'] ?? '—') ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Status</p>
|
||||
<p class="text-emerald-400 capitalize"><?= e($license['status'] ?? 'active') ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 mb-0.5">Last verified</p>
|
||||
<p class="text-white"><?= $license['last_verified'] ? date('M j, Y g:ia', strtotime($license['last_verified'])) : 'Never' ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-800 pt-4">
|
||||
<h3 class="text-sm font-medium text-white mb-3">Change server IP</h3>
|
||||
<form method="POST" action="/license/change-ip" class="flex gap-3 items-start">
|
||||
<?= csrf_field() ?>
|
||||
<input type="text" name="server_ip" placeholder="New IPv4 address (e.g. 1.2.3.4)"
|
||||
pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||
title="Valid IPv4 address" required
|
||||
class="input flex-1 text-sm">
|
||||
<button type="submit" class="btn-secondary text-sm flex-shrink-0">Update IP</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php $pageTitle = 'Forgot Password — NixPanel'; ?>
|
||||
|
||||
<div class="min-h-[80vh] flex items-center justify-center px-4 py-16">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-white">Reset your password</h1>
|
||||
<p class="text-gray-400 mt-2">We'll send a reset link to your email</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="POST" action="/forgot-password" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Email address</label>
|
||||
<input type="email" name="email" required autofocus
|
||||
class="input" placeholder="[email protected]">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary w-full justify-center py-3">Send reset link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-6">
|
||||
<a href="/login" class="text-brand-400 hover:underline">Back to sign in</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php $pageTitle = 'NixPanel — Modern Web Hosting Control Panel'; ?>
|
||||
<?php
|
||||
$features = [
|
||||
['icon' => '⚡', 'title' => 'Lightning Fast', 'body' => 'Built for performance. Nginx support, PHP-FPM pools, and optimized defaults out of the box.'],
|
||||
['icon' => '🔒', 'title' => 'Security First', 'body' => "Hardened defaults, Let's Encrypt SSL automation, Fail2ban, and two-factor auth."],
|
||||
['icon' => '🌐', 'title' => 'Full DNS Control', 'body' => 'Manage BIND9 zones and records directly from the panel. No third-party DNS required.'],
|
||||
['icon' => '📊', 'title' => 'Complete Control', 'body' => 'Manage email, databases, cron jobs, FTP accounts, and subdomains in one clean dashboard.'],
|
||||
['icon' => '🔄', 'title' => 'Easy Migration', 'body' => 'Moving from cPanel? Import your accounts with a single command using our migration tool.'],
|
||||
['icon' => '💳', 'title' => 'Simple Licensing', 'body' => 'Tied to your server IP. No per-user fees, no surprises. Upgrade or cancel anytime.'],
|
||||
];
|
||||
?>
|
||||
|
||||
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-20 text-center">
|
||||
<div class="inline-flex items-center gap-2 bg-brand-900/40 border border-brand-500/30 text-brand-400 text-xs font-medium px-3 py-1.5 rounded-full mb-6">
|
||||
Now in public beta
|
||||
</div>
|
||||
<h1 class="text-5xl sm:text-6xl font-extrabold text-white leading-tight tracking-tight">
|
||||
The hosting control panel<br>
|
||||
<span class="text-brand-500">you actually want to use.</span>
|
||||
</h1>
|
||||
<p class="mt-6 text-lg text-gray-400 max-w-2xl mx-auto">
|
||||
NixPanel is a modern, open-source alternative to cPanel. Manage your Linux server,
|
||||
host sites, and control everything from a clean, fast dashboard.
|
||||
</p>
|
||||
<div class="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/register" class="btn-primary text-base px-7 py-3">Get started free</a>
|
||||
<a href="/install" class="btn-outline text-base px-7 py-3">Install guide</a>
|
||||
<a href="/pricing" class="btn-secondary text-base px-7 py-3">View pricing</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<h2 class="text-3xl font-bold text-white text-center mb-12">Everything you need to run a server</h2>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<?php foreach ($features as $f): ?>
|
||||
<div class="card">
|
||||
<div class="text-3xl mb-3"><?= $f['icon'] ?></div>
|
||||
<h3 class="font-semibold text-white mb-2"><?= e($f['title']) ?></h3>
|
||||
<p class="text-sm text-gray-400"><?= e($f['body']) ?></p>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div class="bg-brand-900/20 border border-brand-500/20 rounded-2xl p-12 text-center">
|
||||
<h2 class="text-3xl font-bold text-white mb-4">Ready to take control?</h2>
|
||||
<p class="text-gray-400 mb-8">Start free. Upgrade when you need more.</p>
|
||||
<a href="/register" class="btn-primary text-base px-8 py-3">Create your account</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php $pageTitle = 'Install NixPanel — Server Installation Guide'; ?>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
|
||||
<div class="mb-12">
|
||||
<div class="inline-flex items-center gap-2 bg-brand-900/40 border border-brand-500/30 text-brand-400 text-xs font-medium px-3 py-1.5 rounded-full mb-4">
|
||||
Installation Guide
|
||||
</div>
|
||||
<h1 class="text-4xl font-extrabold text-white mb-4">Install NixPanel</h1>
|
||||
<p class="text-gray-400 text-lg max-w-2xl">
|
||||
Get your NixServer admin panel and NixClient user panel running on any supported Linux server in under five minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$requirements = [
|
||||
'Ubuntu 20.04 / 22.04 / 24.04', 'Debian 11 / 12',
|
||||
'AlmaLinux 8 / 9', 'Rocky Linux 8 / 9', 'RHEL 8 / 9',
|
||||
];
|
||||
$minReqs = [
|
||||
'Root access (sudo)', 'x86_64 or aarch64 architecture',
|
||||
'1 GB RAM (2 GB+ recommended)', '10 GB disk space',
|
||||
'Fresh / minimal OS install', 'Internet access from the server',
|
||||
];
|
||||
?>
|
||||
|
||||
<!-- Requirements -->
|
||||
<div class="mb-12 grid sm:grid-cols-2 gap-6">
|
||||
<div class="card">
|
||||
<h2 class="text-white font-semibold mb-4">Supported OS</h2>
|
||||
<ul class="space-y-2">
|
||||
<?php foreach ($requirements as $os): ?>
|
||||
<li class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<span class="text-emerald-500">✓</span> <?= e($os) ?>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-white font-semibold mb-4">Minimum Requirements</h2>
|
||||
<ul class="space-y-2">
|
||||
<?php foreach ($minReqs as $req): ?>
|
||||
<li class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<span class="text-emerald-500">✓</span> <?= e($req) ?>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick install -->
|
||||
<div class="mb-14">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Quick Install</h2>
|
||||
<p class="text-gray-400 text-sm mb-1">Run this single command as root on a fresh server.</p>
|
||||
<?php
|
||||
$oneLiner = 'curl -fsSL https://raw.githubusercontent.com/NixReaper/nixpanel-server/main/scripts/install.sh | sudo bash';
|
||||
code_block($oneLiner);
|
||||
?>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<?php foreach (['Auto-detects OS', 'Generates random secrets', 'Configures firewall', 'Creates systemd service'] as $b): ?>
|
||||
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"><?= e($b) ?></span>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<p class="mt-4 text-sm text-gray-500">With custom credentials:</p>
|
||||
<?php code_block("curl -fsSL https://raw.githubusercontent.com/NixReaper/nixpanel-server/main/scripts/install.sh | sudo bash -s -- \\\n --admin-user admin \\\n --admin-pass \"YourSecurePassword\" \\\n --admin-email \"[email protected]\"") ?>
|
||||
</div>
|
||||
|
||||
<!-- After install -->
|
||||
<div class="mb-14 bg-brand-900/20 border border-brand-500/20 rounded-xl p-6">
|
||||
<h2 class="text-white font-semibold text-lg mb-3">After Installation</h2>
|
||||
<p class="text-gray-400 text-sm mb-4">
|
||||
Credentials are printed on screen and saved to
|
||||
<code class="text-brand-400 bg-gray-900 px-1.5 py-0.5 rounded text-xs">/root/.nixpanel_credentials</code>.
|
||||
</p>
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<div class="bg-gray-900/60 border border-gray-800 rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 mb-1 uppercase tracking-wide font-medium">NixServer — Admin Panel</p>
|
||||
<p class="text-white font-mono text-sm">http://<your-ip>:2087</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/60 border border-gray-800 rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 mb-1 uppercase tracking-wide font-medium">NixClient — User Panel</p>
|
||||
<p class="text-white font-mono text-sm">http://<your-ip>:2083</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual install -->
|
||||
<div class="mb-14">
|
||||
<h2 class="text-2xl font-bold text-white mb-1">Manual Installation</h2>
|
||||
<p class="text-gray-400 text-sm mb-8">Prefer full control? Install system dependencies first (Node.js 22, MariaDB, Nginx, etc.), then follow these steps.</p>
|
||||
|
||||
<?php
|
||||
$steps = [
|
||||
['Clone the repository', "git clone https://github.com/NixReaper/nixpanel-server.git /opt/nixpanel\ncd /opt/nixpanel"],
|
||||
['Configure environment variables', "cp .env.example .env\nnano .env # set your passwords and secrets"],
|
||||
['Install, migrate, and build', "npm install --workspaces --include-workspace-root\ncd server && npx prisma generate && npx prisma migrate deploy\nADMIN_USERNAME=admin ADMIN_PASSWORD=yourpass npx tsx src/db/seed.ts\ncd ..\nnpm run build --workspace=nixserver\nnpm run build --workspace=nixclient"],
|
||||
['Create the systemd service', "cp scripts/nixpanel.service /etc/systemd/system/nixpanel.service\nsystemctl daemon-reload\nsystemctl enable --now nixpanel"],
|
||||
];
|
||||
foreach ($steps as $i => [$title, $cmd]):
|
||||
?>
|
||||
<div class="flex gap-5 mb-8">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-brand-600 flex items-center justify-center text-white text-sm font-bold"><?= $i + 1 ?></div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-white font-semibold text-lg mb-2"><?= e($title) ?></h3>
|
||||
<?php code_block($cmd) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<!-- What gets installed -->
|
||||
<?php
|
||||
$software = [
|
||||
['Node.js 22 LTS', 'Application runtime'],
|
||||
['MariaDB', 'Panel database'],
|
||||
['Nginx', 'Web server & reverse proxy'],
|
||||
['PHP 8.3 + FPM', 'PHP runtime with common extensions'],
|
||||
['Postfix + Dovecot', 'SMTP & IMAP mail server'],
|
||||
['vsftpd', 'FTP server'],
|
||||
['BIND9', 'DNS server'],
|
||||
['Certbot', "Let's Encrypt SSL automation"],
|
||||
['Fail2ban', 'Brute-force protection'],
|
||||
['UFW / firewalld', 'Firewall (auto-configured)'],
|
||||
];
|
||||
?>
|
||||
<div class="mb-14">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">What Gets Installed</h2>
|
||||
<div class="card overflow-hidden p-0">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="border-b border-gray-800">
|
||||
<th class="text-left text-gray-500 font-medium px-5 py-3">Package</th>
|
||||
<th class="text-left text-gray-500 font-medium px-5 py-3">Purpose</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($software as $i => [$pkg, $desc]): ?>
|
||||
<tr class="border-b border-gray-800 last:border-0 <?= $i % 2 !== 0 ? 'bg-gray-900/30' : '' ?>">
|
||||
<td class="px-5 py-3 text-white font-mono text-xs"><?= e($pkg) ?></td>
|
||||
<td class="px-5 py-3 text-gray-400"><?= e($desc) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ports -->
|
||||
<?php
|
||||
$ports = [
|
||||
['2087', 'NixServer', 'Admin panel (NixServer)'],
|
||||
['2083', 'NixClient', 'User panel (NixClient)'],
|
||||
['80', 'HTTP', 'Web traffic + ACME challenges'],
|
||||
['443', 'HTTPS', 'SSL web traffic'],
|
||||
['21', 'FTP', 'FTP control'],
|
||||
['25', 'SMTP', 'Inbound mail'],
|
||||
['587', 'Submission', 'Outbound mail (authenticated)'],
|
||||
['993', 'IMAPS', 'Secure IMAP'],
|
||||
['53', 'DNS', 'DNS resolution (TCP + UDP)'],
|
||||
];
|
||||
?>
|
||||
<div class="mb-14">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Firewall Ports</h2>
|
||||
<p class="text-gray-400 text-sm mb-4">Auto-configured via UFW (Debian/Ubuntu) or firewalld (RHEL/AlmaLinux/Rocky).</p>
|
||||
<div class="card overflow-hidden p-0">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="border-b border-gray-800">
|
||||
<th class="text-left text-gray-500 font-medium px-5 py-3">Port</th>
|
||||
<th class="text-left text-gray-500 font-medium px-5 py-3">Name</th>
|
||||
<th class="text-left text-gray-500 font-medium px-5 py-3">Description</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($ports as $i => [$port, $name, $desc]): ?>
|
||||
<tr class="border-b border-gray-800 last:border-0 <?= $i % 2 !== 0 ? 'bg-gray-900/30' : '' ?>">
|
||||
<td class="px-5 py-3 font-mono text-brand-400 text-xs"><?= e($port) ?></td>
|
||||
<td class="px-5 py-3 text-white"><?= e($name) ?></td>
|
||||
<td class="px-5 py-3 text-gray-400"><?= e($desc) ?></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Updating -->
|
||||
<div class="mb-14">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Updating NixPanel</h2>
|
||||
<?php code_block('sudo bash /opt/nixpanel/scripts/update.sh') ?>
|
||||
</div>
|
||||
|
||||
<!-- Service management -->
|
||||
<div class="mb-14">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Managing the Service</h2>
|
||||
<div class="grid sm:grid-cols-2 gap-3">
|
||||
<?php
|
||||
$cmds = [
|
||||
'Check status' => 'systemctl status nixpanel',
|
||||
'View live logs'=> 'journalctl -u nixpanel -f',
|
||||
'Restart' => 'systemctl restart nixpanel',
|
||||
'Stop' => 'systemctl stop nixpanel',
|
||||
];
|
||||
foreach ($cmds as $label => $cmd):
|
||||
?>
|
||||
<div class="card py-3">
|
||||
<p class="text-xs text-gray-500 mb-1"><?= e($label) ?></p>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<code class="text-sm text-gray-300 font-mono"><?= e($cmd) ?></code>
|
||||
<button onclick="navigator.clipboard.writeText('<?= e($cmd) ?>')" class="text-xs px-2.5 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 flex-shrink-0 transition-colors">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uninstall -->
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Uninstalling</h2>
|
||||
<p class="text-gray-400 text-sm mb-1">Removes the service, nginx config, install directory, and database. Does not remove Nginx, PHP, or other system packages.</p>
|
||||
<?php code_block('sudo bash /opt/nixpanel/scripts/uninstall.sh') ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
function code_block(string $code): void {
|
||||
echo '<div class="relative mt-3 rounded-lg bg-gray-950 border border-gray-800 overflow-hidden">';
|
||||
echo '<div class="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-900/60">';
|
||||
echo '<span class="text-xs text-gray-500 font-mono">bash</span>';
|
||||
echo '<button onclick="navigator.clipboard.writeText(this.dataset.code)" data-code="' . htmlspecialchars($code, ENT_QUOTES) . '" class="text-xs px-2.5 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 transition-colors">Copy</button>';
|
||||
echo '</div>';
|
||||
echo '<pre class="px-4 py-4 text-sm text-gray-300 font-mono overflow-x-auto whitespace-pre leading-relaxed">' . htmlspecialchars($code, ENT_QUOTES) . '</pre>';
|
||||
echo '</div>';
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,16 @@
|
||||
</main>
|
||||
<footer class="border-t border-gray-800 bg-gray-950 py-10 mt-16">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-gray-500">
|
||||
<span>© <?= date('Y') ?> NixPanel. All rights reserved.</span>
|
||||
<div class="flex gap-6">
|
||||
<a href="/pricing" class="hover:text-gray-300 transition-colors">Pricing</a>
|
||||
<a href="/install" class="hover:text-gray-300 transition-colors">Install</a>
|
||||
<a href="https://docs.nixpanel.io" target="_blank" rel="noreferrer" class="hover:text-gray-300 transition-colors">Docs</a>
|
||||
<a href="/login" class="hover:text-gray-300 transition-colors">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="bg-gray-950">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= e($pageTitle ?? 'NixPanel — Modern Web Hosting Control Panel') ?></title>
|
||||
<meta name="description" content="<?= e($pageDesc ?? 'NixPanel is a modern, open-source cPanel alternative. Manage your Linux server with NixServer and NixClient.') ?>">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
900: '#1e3a5f',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer base {
|
||||
body { @apply bg-gray-950 text-gray-100 font-sans antialiased; }
|
||||
.btn-primary { @apply inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-brand-600 hover:bg-brand-700 text-white font-semibold text-sm transition-colors; }
|
||||
.btn-secondary { @apply inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-transparent hover:bg-gray-800 text-gray-300 border border-gray-700 font-semibold text-sm transition-colors; }
|
||||
.btn-outline { @apply inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-transparent hover:bg-brand-900/30 text-brand-400 border border-brand-500/50 font-semibold text-sm transition-colors; }
|
||||
.input { @apply w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-brand-500 transition-colors; }
|
||||
.card { @apply bg-gray-900/50 border border-gray-800 rounded-xl p-6; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col">
|
||||
|
||||
<?php if ($flash): ?>
|
||||
<div id="flash" class="fixed top-4 right-4 z-50 max-w-sm w-full">
|
||||
<div class="flex items-start gap-3 px-4 py-3 rounded-lg border shadow-lg
|
||||
<?= $flash['type'] === 'success' ? 'bg-emerald-900/60 border-emerald-500/30 text-emerald-300' : 'bg-red-900/60 border-red-500/30 text-red-300' ?>">
|
||||
<span class="text-sm flex-1"><?= e($flash['message']) ?></span>
|
||||
<button onclick="document.getElementById('flash').remove()" class="opacity-60 hover:opacity-100 text-lg leading-none">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>setTimeout(() => { const f = document.getElementById('flash'); if (f) f.remove(); }, 5000);</script>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<header class="border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm sticky top-0 z-40">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<span class="text-xl font-bold text-white">Nix<span class="text-brand-500">Panel</span></span>
|
||||
</a>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-6 text-sm text-gray-400">
|
||||
<a href="/" class="hover:text-white transition-colors">Home</a>
|
||||
<a href="/pricing" class="hover:text-white transition-colors">Pricing</a>
|
||||
<a href="/install" class="hover:text-white transition-colors">Install</a>
|
||||
<a href="https://docs.nixpanel.io" target="_blank" rel="noreferrer" class="hover:text-white transition-colors">Docs</a>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<?php if (\NixPanel\Auth::isLoggedIn()): ?>
|
||||
<a href="/dashboard" class="text-sm text-gray-300 hover:text-white transition-colors">Dashboard</a>
|
||||
<a href="/logout" class="text-sm px-4 py-2 rounded-lg border border-gray-700 text-gray-300 hover:border-gray-500 hover:text-white transition-colors">Sign out</a>
|
||||
<?php else: ?>
|
||||
<a href="/login" class="text-sm text-gray-300 hover:text-white transition-colors">Sign in</a>
|
||||
<a href="/register" class="text-sm px-4 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 text-white font-semibold transition-colors">Get started</a>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1">
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php $pageTitle = 'Sign In — NixPanel'; ?>
|
||||
|
||||
<div class="min-h-[80vh] flex items-center justify-center px-4 py-16">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-white">Welcome back</h1>
|
||||
<p class="text-gray-400 mt-2">Sign in to your NixPanel account</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="POST" action="/login" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Email address</label>
|
||||
<input type="email" name="email" value="<?= old('email') ?>" required autofocus
|
||||
class="input" placeholder="[email protected]">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Password</label>
|
||||
<input type="password" name="password" required
|
||||
class="input" placeholder="••••••••">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<a href="/forgot-password" class="text-sm text-brand-400 hover:underline">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full justify-center py-3">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-6">
|
||||
Don't have an account? <a href="/register" class="text-brand-400 hover:underline">Create one</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php $pageTitle = 'Pricing — NixPanel'; ?>
|
||||
<?php
|
||||
$plans = [
|
||||
['key' => 'free', 'name' => 'Free', 'monthly' => 0, 'annual' => 0, 'highlighted' => false,
|
||||
'limits' => ['Domains' => '1', 'Accounts' => '1', 'Emails' => '5', 'Databases' => '2', 'Disk' => '5 GB'],
|
||||
'features' => ['Community support', "Let's Encrypt SSL"]],
|
||||
|
||||
['key' => 'solo', 'name' => 'Solo', 'monthly' => 9.99, 'annual' => 95.90, 'highlighted' => false,
|
||||
'limits' => ['Domains' => '5', 'Accounts' => '5', 'Emails' => '50', 'Databases' => '10', 'Disk' => '50 GB'],
|
||||
'features' => ['Email support', "Let's Encrypt SSL", 'WP one-click install']],
|
||||
|
||||
['key' => 'host', 'name' => 'Host', 'monthly' => 29.99, 'annual' => 287.90, 'highlighted' => true,
|
||||
'limits' => ['Domains' => '50', 'Accounts' => '50', 'Emails' => '500', 'Databases' => '100', 'Disk' => '250 GB'],
|
||||
'features' => ['Priority support', "Let's Encrypt SSL", 'WP one-click install', 'NixServer reseller panel']],
|
||||
|
||||
['key' => 'agency', 'name' => 'Agency', 'monthly' => 79.99, 'annual' => 767.90, 'highlighted' => false,
|
||||
'limits' => ['Domains' => 'Unlimited', 'Accounts' => 'Unlimited', 'Emails' => 'Unlimited', 'Databases' => 'Unlimited', 'Disk' => 'Unlimited'],
|
||||
'features' => ['Priority support', "Let's Encrypt SSL", 'WP one-click install', 'NixServer reseller panel', 'White-label ready']],
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-bold text-white mb-4">Simple, transparent pricing</h1>
|
||||
<p class="text-gray-400">Tied to your server IP. No per-user fees. Cancel anytime.</p>
|
||||
|
||||
<div class="inline-flex items-center gap-1 bg-gray-900 border border-gray-800 rounded-lg p-1 mt-8">
|
||||
<button id="btn-monthly" onclick="setInterval('monthly')"
|
||||
class="px-4 py-2 rounded-md text-sm font-medium transition-colors bg-brand-600 text-white">
|
||||
Monthly
|
||||
</button>
|
||||
<button id="btn-annual" onclick="setInterval('annual')"
|
||||
class="px-4 py-2 rounded-md text-sm font-medium transition-colors text-gray-400 hover:text-white">
|
||||
Annual
|
||||
<span class="ml-2 text-xs bg-green-500/20 text-green-400 px-1.5 py-0.5 rounded">2 months free</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<?php foreach ($plans as $plan): ?>
|
||||
<div class="flex flex-col rounded-xl border p-6 <?= $plan['highlighted'] ? 'bg-brand-900/20 border-brand-500/40' : 'bg-gray-900/50 border-gray-800' ?>">
|
||||
<?php if ($plan['highlighted']): ?>
|
||||
<div class="text-xs font-semibold text-brand-400 bg-brand-500/10 border border-brand-500/20 rounded-full px-3 py-1 w-fit mb-4">Most popular</div>
|
||||
<?php endif ?>
|
||||
|
||||
<h3 class="text-white font-bold text-xl mb-1"><?= e($plan['name']) ?></h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<span class="price-monthly <?= $plan['monthly'] === 0 ? '' : 'hidden' ?>" data-plan="<?= $plan['key'] ?>">
|
||||
<span class="text-4xl font-extrabold text-white">$0</span>
|
||||
<span class="text-gray-400 text-sm">/mo</span>
|
||||
</span>
|
||||
<?php if ($plan['monthly'] > 0): ?>
|
||||
<span class="price-monthly" data-plan="<?= $plan['key'] ?>">
|
||||
<span class="text-4xl font-extrabold text-white">$<?= number_format($plan['monthly'], 2) ?></span>
|
||||
<span class="text-gray-400 text-sm">/mo</span>
|
||||
</span>
|
||||
<span class="price-annual hidden" data-plan="<?= $plan['key'] ?>">
|
||||
<span class="text-4xl font-extrabold text-white">$<?= number_format($plan['annual'] / 12, 2) ?></span>
|
||||
<span class="text-gray-400 text-sm">/mo</span>
|
||||
<div class="text-xs text-gray-500 mt-0.5">$<?= number_format($plan['annual'], 2) ?>/yr</div>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-1.5 mb-5 text-sm text-gray-400">
|
||||
<?php foreach ($plan['limits'] as $label => $value): ?>
|
||||
<li class="flex justify-between">
|
||||
<span><?= e($label) ?></span>
|
||||
<span class="text-white font-medium"><?= e($value) ?></span>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
|
||||
<ul class="space-y-2 mb-6 text-sm text-gray-400 flex-1">
|
||||
<?php foreach ($plan['features'] as $feat): ?>
|
||||
<li class="flex items-center gap-2"><span class="text-emerald-500 flex-shrink-0">✓</span><?= e($feat) ?></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
|
||||
<?php if ($plan['key'] === 'free'): ?>
|
||||
<a href="/register" class="<?= $plan['highlighted'] ? 'btn-primary' : 'btn-secondary' ?> w-full justify-center">Get started free</a>
|
||||
<?php else: ?>
|
||||
<?php if (\NixPanel\Auth::isLoggedIn()): ?>
|
||||
<form method="POST" action="/checkout">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="plan_key" value="<?= e($plan['key']) ?>">
|
||||
<input type="hidden" name="interval" id="interval-<?= $plan['key'] ?>" value="monthly">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="server_ip" placeholder="Your server IP (e.g. 1.2.3.4)"
|
||||
class="input text-xs" pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||
title="Valid IPv4 address" required>
|
||||
</div>
|
||||
<button type="submit" class="<?= $plan['highlighted'] ? 'btn-primary' : 'btn-secondary' ?> w-full justify-center">
|
||||
Get started
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<a href="/register" class="<?= $plan['highlighted'] ? 'btn-primary' : 'btn-secondary' ?> w-full justify-center">Get started</a>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-10">
|
||||
All plans include 1 server IP. Need multiple servers?
|
||||
<a href="mailto:[email protected]" class="text-brand-400 hover:underline">Contact us.</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function setInterval(val) {
|
||||
document.querySelectorAll('.price-monthly').forEach(el => el.classList.toggle('hidden', val !== 'monthly'));
|
||||
document.querySelectorAll('.price-annual').forEach(el => el.classList.toggle('hidden', val !== 'annual'));
|
||||
document.querySelectorAll('[id^="interval-"]').forEach(el => el.value = val);
|
||||
document.getElementById('btn-monthly').className = 'px-4 py-2 rounded-md text-sm font-medium transition-colors ' + (val === 'monthly' ? 'bg-brand-600 text-white' : 'text-gray-400 hover:text-white');
|
||||
document.getElementById('btn-annual').className = 'px-4 py-2 rounded-md text-sm font-medium transition-colors ' + (val === 'annual' ? 'bg-brand-600 text-white' : 'text-gray-400 hover:text-white');
|
||||
}
|
||||
// Fix: hide the extra free $0 span since we output it twice to avoid if/else issues
|
||||
document.querySelectorAll('.price-monthly[data-plan="free"]').forEach((el, i) => { if (i > 0) el.remove(); });
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php $pageTitle = 'Create Account — NixPanel'; ?>
|
||||
|
||||
<div class="min-h-[80vh] flex items-center justify-center px-4 py-16">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-white">Create your account</h1>
|
||||
<p class="text-gray-400 mt-2">Get started with NixPanel for free</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="POST" action="/register" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Full name</label>
|
||||
<input type="text" name="full_name" value="<?= old('full_name') ?>" required
|
||||
class="input" placeholder="Jane Smith">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Email address</label>
|
||||
<input type="email" name="email" value="<?= old('email') ?>" required
|
||||
class="input" placeholder="[email protected]">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Password</label>
|
||||
<input type="password" name="password" required minlength="8"
|
||||
class="input" placeholder="Min 8 characters">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full justify-center py-3">Create account</button>
|
||||
|
||||
<p class="text-xs text-gray-500 text-center">
|
||||
By creating an account you agree to our
|
||||
<a href="/terms" class="text-brand-400 hover:underline">Terms of Service</a>.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-6">
|
||||
Already have an account? <a href="/login" class="text-brand-400 hover:underline">Sign in</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php $pageTitle = 'Set New Password — NixPanel'; ?>
|
||||
|
||||
<div class="min-h-[80vh] flex items-center justify-center px-4 py-16">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-white">Set new password</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="POST" action="/reset-password" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="token" value="<?= e($token) ?>">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">New password</label>
|
||||
<input type="password" name="password" required minlength="8"
|
||||
class="input" placeholder="Min 8 characters" autofocus>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-1.5">Confirm password</label>
|
||||
<input type="password" name="password_confirm" required minlength="8"
|
||||
class="input" placeholder="Repeat password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full justify-center py-3">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php $pageTitle = 'Verify Email — NixPanel'; ?>
|
||||
|
||||
<div class="min-h-[80vh] flex items-center justify-center px-4 py-16">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<?php if ($verified): ?>
|
||||
<div class="text-5xl mb-6">✅</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-3">Email verified!</h1>
|
||||
<p class="text-gray-400 mb-8">Your account is active. You can now sign in.</p>
|
||||
<a href="/login" class="btn-primary px-8 py-3">Sign in</a>
|
||||
<?php else: ?>
|
||||
<div class="text-5xl mb-6">❌</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-3">Invalid link</h1>
|
||||
<p class="text-gray-400 mb-8">This verification link is invalid or has already been used.</p>
|
||||
<a href="/register" class="btn-secondary px-8 py-3">Create a new account</a>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user