fix: rename setInterval to setBilling, add deploy.sh

- pricing.php: rename setInterval() to setBilling() to stop shadowing
  the native window.setInterval — the billing toggle was silently broken
- deploy.sh: full automated deploy script for nixpanel.io
  - DocumentRoot set to public/ (fixes /pricing, /login 404s)
  - AllowOverride All so public/.htaccess handles all routing
  - PHP 8.3-FPM via Apache mod_proxy_fcgi
  - composer install, DB migrations, .env setup, SSL via certbot
This commit is contained in:
NixReaper
2026-04-25 05:37:31 -05:00
parent d0424577a9
commit 29c18c5af8
2 changed files with 299 additions and 4 deletions
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────────
# NixPanel Website — Deploy Script
# Deploys nixpanel.io (PHP frontend + customer portal) to a server.
# Run as root on the target server.
# ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'
YELLOW='\033[1;33m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}[ OK ]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
error() { echo -e "${RED}[ERR ]${RESET} $*" >&2; exit 1; }
step() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${RESET}"; }
[[ $EUID -ne 0 ]] && error "Must be run as root"
SITE_DIR="/var/www/nixpanel-website"
GITHUB_REPO="NixReaper/nixpanel-website"
SERVER_NAME="${1:-nixpanel.io}"
echo ""
echo -e "${BOLD}${CYAN} NixPanel Website — Deployer${RESET}"
echo -e " Domain: ${SERVER_NAME}"
echo ""
# ── 1. System deps ────────────────────────────────────────────────────────────
step "Installing system dependencies"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# ondrej/php PPA for latest PHP on Ubuntu 24.04
apt-get install -y -qq ca-certificates gnupg software-properties-common
if ! grep -rq "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null; then
add-apt-repository -y ppa:ondrej/php
apt-get update -qq
fi
apt-get install -y -qq \
apache2 \
php8.3 php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl \
php8.3-mbstring php8.3-xml php8.3-zip php8.3-gd php8.3-intl \
mariadb-server mariadb-client \
certbot python3-certbot-apache \
curl wget git unzip
# Install Composer
if ! command -v composer &>/dev/null; then
info "Installing Composer…"
curl -fsSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
fi
success "Dependencies installed"
# ── 2. MariaDB ────────────────────────────────────────────────────────────────
step "Configuring MariaDB"
systemctl enable --now mariadb
if ! mysql -u root -e "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='nixpanel_website'" 2>/dev/null | grep -q nixpanel_website; then
DB_PASS=$(openssl rand -hex 24)
mysql -u root <<SQL
CREATE DATABASE IF NOT EXISTS nixpanel_website CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'nixpanel_web'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON nixpanel_website.* TO 'nixpanel_web'@'127.0.0.1';
FLUSH PRIVILEGES;
SQL
echo "$DB_PASS" > /root/.nixpanel_website_db_pass
chmod 600 /root/.nixpanel_website_db_pass
success "Database created (password saved to /root/.nixpanel_website_db_pass)"
else
DB_PASS=$(cat /root/.nixpanel_website_db_pass 2>/dev/null || echo "")
info "Database already exists"
fi
# ── 3. Clone / update source ──────────────────────────────────────────────────
step "Deploying source code"
if [[ -d "$SITE_DIR/.git" ]]; then
info "Updating existing deployment…"
git -C "$SITE_DIR" pull --ff-only
else
git clone "https://github.com/${GITHUB_REPO}.git" "$SITE_DIR"
fi
success "Source at $SITE_DIR"
# ── 4. Composer dependencies ──────────────────────────────────────────────────
step "Installing PHP dependencies"
cd "$SITE_DIR"
composer install --no-dev --optimize-autoloader --quiet
success "Composer packages installed"
# ── 5. Run database migrations ────────────────────────────────────────────────
step "Running database migrations"
if [[ -f "$SITE_DIR/migrations/001_schema.sql" ]]; then
mysql -u nixpanel_web -p"${DB_PASS}" nixpanel_website < "$SITE_DIR/migrations/001_schema.sql"
success "Migrations applied"
else
warn "No migration file found at migrations/001_schema.sql"
fi
# ── 6. Environment file ───────────────────────────────────────────────────────
step "Configuring environment"
if [[ ! -f "$SITE_DIR/.env" ]]; then
echo ""
info "Setting up .env — press Enter to leave values blank (configure later)"
echo ""
read -rp " Stripe secret key (sk_live_...): " STRIPE_SK; STRIPE_SK="${STRIPE_SK:-}"
read -rp " Stripe publishable key (pk_live_...): " STRIPE_PK; STRIPE_PK="${STRIPE_PK:-}"
read -rp " Stripe webhook secret (whsec_...): " STRIPE_WH; STRIPE_WH="${STRIPE_WH:-}"
echo ""
info "Stripe Price IDs (leave blank to fill in later):"
read -rp " STRIPE_PRICE_SOLO_MONTHLY: " P_SOLO_M; P_SOLO_M="${P_SOLO_M:-}"
read -rp " STRIPE_PRICE_SOLO_ANNUAL: " P_SOLO_A; P_SOLO_A="${P_SOLO_A:-}"
read -rp " STRIPE_PRICE_HOST_MONTHLY: " P_HOST_M; P_HOST_M="${P_HOST_M:-}"
read -rp " STRIPE_PRICE_HOST_ANNUAL: " P_HOST_A; P_HOST_A="${P_HOST_A:-}"
read -rp " STRIPE_PRICE_AGENCY_MONTHLY: " P_AGCY_M; P_AGCY_M="${P_AGCY_M:-}"
read -rp " STRIPE_PRICE_AGENCY_ANNUAL: " P_AGCY_A; P_AGCY_A="${P_AGCY_A:-}"
echo ""
read -rp " SMTP host [smtp.mailgun.org]: " MAIL_HOST; MAIL_HOST="${MAIL_HOST:-smtp.mailgun.org}"
read -rp " SMTP user: " MAIL_USER; MAIL_USER="${MAIL_USER:-}"
read -rsp " SMTP password: " MAIL_PASS; MAIL_PASS="${MAIL_PASS:-}"; echo ""
echo ""
read -rp " Licensing server URL [https://license.nixpanel.io]: " LIC_URL
LIC_URL="${LIC_URL:-https://license.nixpanel.io}"
read -rp " Licensing server API key: " LIC_KEY; LIC_KEY="${LIC_KEY:-}"
SESSION_SECRET=$(openssl rand -hex 64)
cat > "$SITE_DIR/.env" <<EOF
APP_URL=https://${SERVER_NAME}
APP_ENV=production
# Database
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=nixpanel_website
DB_USER=nixpanel_web
DB_PASSWORD=${DB_PASS}
# Session
SESSION_SECRET=${SESSION_SECRET}
# Stripe
STRIPE_SECRET_KEY=${STRIPE_SK}
STRIPE_PUBLISHABLE_KEY=${STRIPE_PK}
STRIPE_WEBHOOK_SECRET=${STRIPE_WH}
STRIPE_PRICE_SOLO_MONTHLY=${P_SOLO_M}
STRIPE_PRICE_SOLO_ANNUAL=${P_SOLO_A}
STRIPE_PRICE_HOST_MONTHLY=${P_HOST_M}
STRIPE_PRICE_HOST_ANNUAL=${P_HOST_A}
STRIPE_PRICE_AGENCY_MONTHLY=${P_AGCY_M}
STRIPE_PRICE_AGENCY_ANNUAL=${P_AGCY_A}
# Mail (SMTP)
MAIL_HOST=${MAIL_HOST}
MAIL_PORT=587
MAIL_USER=${MAIL_USER}
MAIL_PASSWORD=${MAIL_PASS}
MAIL_FROM=noreply@${SERVER_NAME}
MAIL_FROM_NAME=NixPanel
# Internal licensing server
LICENSING_SERVER_URL=${LIC_URL}
LICENSING_SERVER_API_KEY=${LIC_KEY}
EOF
chmod 600 "$SITE_DIR/.env"
success ".env written"
else
info ".env already exists — skipping (edit manually: $SITE_DIR/.env)"
fi
# ── 7. File permissions ───────────────────────────────────────────────────────
step "Setting permissions"
chown -R www-data:www-data "$SITE_DIR"
chmod 750 "$SITE_DIR"
chmod 640 "$SITE_DIR/.env"
# Protect vendor and src from direct web access (handled by DocumentRoot=public/)
find "$SITE_DIR/public" -type d -exec chmod 755 {} \;
find "$SITE_DIR/public" -type f -exec chmod 644 {} \;
success "Permissions set"
# ── 8. Apache vhost ───────────────────────────────────────────────────────────
step "Configuring Apache"
a2enmod rewrite proxy proxy_http proxy_fcgi ssl headers expires deflate
a2enconf php8.3-fpm
# Remove default site
a2dissite 000-default 2>/dev/null || true
cat > /etc/apache2/sites-available/nixpanel-website.conf <<APACHEEOF
<VirtualHost *:80>
ServerName ${SERVER_NAME}
ServerAlias www.${SERVER_NAME}
# DocumentRoot points directly to public/ — only one .htaccess needed
DocumentRoot ${SITE_DIR}/public
<Directory ${SITE_DIR}/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Pass PHP to PHP-FPM
<FilesMatch "\.php$">
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
ErrorLog /var/log/apache2/nixpanel-website-error.log
CustomLog /var/log/apache2/nixpanel-website-access.log combined
</VirtualHost>
# HTTPS — populated by certbot
<VirtualHost *:443>
ServerName ${SERVER_NAME}
ServerAlias www.${SERVER_NAME}
DocumentRoot ${SITE_DIR}/public
<Directory ${SITE_DIR}/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
<FilesMatch "\.php$">
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
ErrorLog /var/log/apache2/nixpanel-website-error.log
CustomLog /var/log/apache2/nixpanel-website-access.log combined
# SSL — certbot will fill these in
# SSLEngine on
# SSLCertificateFile ...
# SSLCertificateKeyFile ...
</VirtualHost>
APACHEEOF
a2ensite nixpanel-website
systemctl enable --now apache2 php8.3-fpm
apache2ctl configtest && systemctl reload apache2
success "Apache configured"
# ── 9. SSL ────────────────────────────────────────────────────────────────────
step "SSL certificate (Let's Encrypt)"
echo ""
echo -e " ${BOLD}Obtain SSL cert now?${RESET} (requires DNS pointing ${SERVER_NAME} → this server)"
read -rp " Get SSL cert? [Y/n]: " DO_SSL
DO_SSL="${DO_SSL:-Y}"
if [[ "${DO_SSL,,}" == "y" ]]; then
read -rp " Email for Let's Encrypt notices: " LE_EMAIL
certbot --apache -d "$SERVER_NAME" -d "www.${SERVER_NAME}" \
--non-interactive --agree-tos -m "$LE_EMAIL" --redirect \
&& success "SSL certificate installed" \
|| warn "certbot failed — run manually: certbot --apache -d ${SERVER_NAME}"
else
info "Skipped — run later: certbot --apache -d ${SERVER_NAME}"
fi
# ── Done ──────────────────────────────────────────────────────────────────────
SERVER_IP=$(hostname -I | awk '{print $1}')
echo ""
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD}${GREEN} NixPanel Website Deployed!${RESET}"
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo ""
echo -e " ${BOLD}URL:${RESET} https://${SERVER_NAME}"
echo -e " ${BOLD}Root:${RESET} ${SITE_DIR}"
echo -e " ${BOLD}Config:${RESET} ${SITE_DIR}/.env"
echo -e " ${BOLD}Logs:${RESET} /var/log/apache2/nixpanel-website-*.log"
echo ""
echo -e " ${BOLD}To redeploy after code changes:${RESET}"
echo -e " git -C ${SITE_DIR} pull && composer install -d ${SITE_DIR} --no-dev -o"
echo ""
if [[ -z "$(grep 'STRIPE_SECRET_KEY=sk_' "$SITE_DIR/.env" 2>/dev/null || true)" ]]; then
echo -e " ${YELLOW}⚠ Stripe keys not yet configured — edit ${SITE_DIR}/.env${RESET}"
fi
echo ""
+4 -4
View File
@@ -25,11 +25,11 @@ $plans = [
<p class="text-gray-400">Tied to your server IP. No per-user fees. Cancel anytime.</p> <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"> <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')" <button id="btn-monthly" onclick="setBilling('monthly')"
class="px-4 py-2 rounded-md text-sm font-medium transition-colors bg-brand-600 text-white"> class="px-4 py-2 rounded-md text-sm font-medium transition-colors bg-brand-600 text-white">
Monthly Monthly
</button> </button>
<button id="btn-annual" onclick="setInterval('annual')" <button id="btn-annual" onclick="setBilling('annual')"
class="px-4 py-2 rounded-md text-sm font-medium transition-colors text-gray-400 hover:text-white"> class="px-4 py-2 rounded-md text-sm font-medium transition-colors text-gray-400 hover:text-white">
Annual 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> <span class="ml-2 text-xs bg-green-500/20 text-green-400 px-1.5 py-0.5 rounded">2 months free</span>
@@ -111,13 +111,13 @@ $plans = [
</div> </div>
<script> <script>
function setInterval(val) { function setBilling(val) {
document.querySelectorAll('.price-monthly').forEach(el => el.classList.toggle('hidden', val !== 'monthly')); 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('.price-annual').forEach(el => el.classList.toggle('hidden', val !== 'annual'));
document.querySelectorAll('[id^="interval-"]').forEach(el => el.value = val); 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-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'); 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 // Hide the duplicate free $0 span
document.querySelectorAll('.price-monthly[data-plan="free"]').forEach((el, i) => { if (i > 0) el.remove(); }); document.querySelectorAll('.price-monthly[data-plan="free"]').forEach((el, i) => { if (i > 0) el.remove(); });
</script> </script>