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]>
122 lines
5.3 KiB
PHP
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>
|