# CrystalParadise — Deployment Runbook

## F1 — Admin authorization gate

### What changed

- `users` table gains `is_admin BOOLEAN NOT NULL DEFAULT FALSE` column.
- `User` model implements `FilamentUser::canAccessPanel()` returning `$this->is_admin`.
- All existing users (including the Block A admin row) have `is_admin = 0` after the
  migration. The admin panel is inaccessible to everyone until step 2 below.

---

### ⚠️ LOCKOUT WARNING — read before deploying

**Deploying the code without running step 2 in the same maintenance window locks
everyone — including operators — out of `/admin` with no recovery path through the
application.** There is no admin-in-panel way to recover; the only paths are a direct
DB update (`UPDATE users SET is_admin = 1 WHERE email = '...'`) or re-running the
artisan command from the server CLI. Both require server access. Do not split these
steps across windows.

---

### Deployment sequence (all steps in one maintenance window)

**Step 1 — Run the migration**

```bash
php artisan migrate --no-interaction
```

After this step: `is_admin = false` for every user row. The panel is locked.

**Step 2 — Promote the admin user**

```bash
php artisan user:make-admin admin@crystalparadise.in
```

Replace `admin@crystalparadise.in` with the email of the Block A admin account (the
one created via `php artisan filament:user` during initial setup). The command is
idempotent — re-running it is safe.

**Step 3 — Verify**

1. Log in to `/admin` as the promoted admin — confirm the dashboard loads.
2. Log in (or use a separate session) as an approved B2B user — confirm `/admin`
   returns 403 Forbidden.
3. Confirm the user list, approve/reject actions, and bulk actions still work as
   before (F1 does not change any of that logic).

---

### Rollback

If the migration needs to be rolled back:

```bash
php artisan migrate:rollback --step=1 --no-interaction
```

This drops the `is_admin` column. The `User` model will still reference it in code,
so deploy the prior code version before rolling back the migration, or do both
atomically.

---

### Post-deploy: adding additional admins

To grant panel access to another operator:

```bash
php artisan user:make-admin other@crystalparadise.in
```

There is no in-panel way to promote users — this is by design. Promoting an admin
requires server (CLI) access, which is the intended security boundary.

---

## F4 — Launch hardening

### SSL

**Application-side prerequisite (committed to repo):**
`AppServiceProvider::boot()` calls `URL::forceScheme('https')` when
`APP_ENV=production`. Set `APP_URL=https://crystalparadise.in` in `.env`.

**Operator step — certbot (standalone Nginx/Apache):**
```bash
sudo certbot --nginx -d crystalparadise.in -d www.crystalparadise.in
```

**Operator step — Laravel Forge:**
In the Forge dashboard → site → SSL → Let's Encrypt → provision.
Enable "Redirect HTTP to HTTPS" on the Forge site settings.

---

### Queue worker (supervisor)

The `OrderConfirmed` notification (D20) uses `ShouldQueue`. Without a running
worker, order confirmation emails are never sent.

**Supervisor configuration** (`/etc/supervisor/conf.d/crystalparadise-worker.conf`):
```ini
[program:crystalparadise-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/crystalparadise/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/crystalparadise-worker.log
stopwaitsecs=3600
```

```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start crystalparadise-worker:*
```

**Laravel Forge:** Configure the queue worker in the Forge dashboard under
the site → Queue tab. Set driver=redis, connection=default, queue=default,
processes=2, max-time=3600.

---

### Scheduler cron

The `ReleaseStaleRazorpayOrders` command (D15) runs every minute via the
scheduler. Without this entry, stale Razorpay orders never release their
held stock.

Add to crontab (`crontab -e` as www-data or deploy user):
```
* * * * * cd /var/www/crystalparadise && php artisan schedule:run >> /dev/null 2>&1
```

---

### Backups

Install and configure `spatie/laravel-backup`:
```bash
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
```

Configure `config/backup.php`:
- `backup.name` → `'crystalparadise'`
- `backup.source.databases` → `['mysql']`
- `backup.destination.disks` → `['s3']` (configure S3 or equivalent off-server storage)
- `backup.cleanup.strategy` → keep daily backups for 30 days, weekly for 8 weeks

Schedule in `routes/console.php` (already wired via the scheduler):
```php
Schedule::command('backup:clean')->daily()->at('01:00');
Schedule::command('backup:run')->daily()->at('01:30');
```

**Restore drill (run before launch):**
```bash
# 1. List available backups
php artisan backup:list

# 2. Download the latest backup zip from S3
# 3. Unzip — contains MySQL dump (.sql.gz) and storage/ files
# 4. Restore database:
gunzip < crystalparadise-YYYY-MM-DD.sql.gz | mysql -u root -p crystalparadise

# 5. Restore storage files:
rsync -av storage-backup/ /var/www/crystalparadise/storage/
```

---

### 2FA bootstrap — first admin enrolment after deploy

⚠️ After the F4 migration runs, all existing admins have `totp_secret = NULL`.
Every admin will be redirected to `/two-factor/setup` on their next `/admin` login.
This is by design — see D27.

**Deploy sequence:**

**Step 1 — Run migrations**
```bash
php artisan migrate --no-interaction
```

New columns added: `users.totp_secret` (encrypted). No existing data is changed.
The panel gate now also requires 2FA; all current admins will see the setup redirect.

**Step 2 — Each admin enrols on first login**

1. Admin visits `/admin`, is redirected to `/two-factor/setup`.
2. Opens authenticator app, scans QR or enters the manual key.
3. Enters the 6-digit confirmation code.
4. Recovery codes are displayed — **admin must save these now** (shown once only).
5. Admin is redirected to `/admin` with 2FA verified for this session.

**Step 3 — Verify all admins enrolled**
```sql
-- Run on the production DB: every admin row must have a non-null totp_secret.
SELECT email, is_admin, (totp_secret IS NOT NULL) AS has_2fa
FROM users
WHERE is_admin = 1;
```

---

### Health check

A `/up` endpoint is available at `GET /up` — returns `{"status":"ok"}` with HTTP 200.
Configure your uptime monitor (UptimeRobot, Better Uptime, Forge's monitoring) to
hit `https://crystalparadise.in/up` every minute.

---

## Launch checklist

All items must be verified before going live:

- [ ] `APP_DEBUG=false` in production `.env`
- [ ] `APP_ENV=production` in production `.env`
- [ ] `APP_URL=https://crystalparadise.in` (HTTPS)
- [ ] `QUEUE_CONNECTION=redis` in production `.env`
- [ ] `CACHE_STORE=redis` in production `.env`
- [ ] SSL certificate active and HTTP → HTTPS redirect live
- [ ] `URL::forceScheme('https')` active (tied to `APP_ENV=production`)
- [ ] Queue worker supervised and running (`supervisorctl status`)
- [ ] Scheduler cron entry active (`crontab -l`)
- [ ] Backups running and off-server destination verified
- [ ] Razorpay webhook URL registered in Razorpay dashboard; secret matches `.env`
- [ ] Shiprocket credentials verified (test a real API call)
- [ ] GST config filled in (`GST_SUPPLIER_GSTIN` etc.)
- [ ] Mail config pointing to production SMTP; send a test email
- [ ] All tests green on the deploy branch (`php artisan test --compact`)
- [ ] Every admin user has enrolled in 2FA (verify via SQL query above)
- [ ] `/up` health endpoint returning 200; uptime monitor configured
- [ ] Restore drill completed (confirmed backup can be restored)
