Files
NixReaperandClaude Sonnet 4.6 faf0818dde 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]>
2026-04-05 11:29:58 -05:00

122 lines
5.3 KiB
PHP

<?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>