Benjamin Harris 9f1ae4afdc Security hardening: XSS fixes, hide errors, protect auth tokens hai 2 semanas
..
.htaccess 1aeef14fb0 Restructure repo: move files into internal/ and contracts/ subfolders hai 2 semanas
README.md 1aeef14fb0 Restructure repo: move files into internal/ and contracts/ subfolders hai 2 semanas
contracts-admin.php 9f1ae4afdc Security hardening: XSS fixes, hide errors, protect auth tokens hai 2 semanas
schema.mysql.sql 1aeef14fb0 Restructure repo: move files into internal/ and contracts/ subfolders hai 2 semanas

README.md

Contracts Admin MVP

This is a single-file PHP admin and JSON API that lets you:

  • List all .md contracts stored in a /contracts folder
  • Edit a contract in a modal and save changes back to disk
  • Email a signing link to a client and mark the item as "sent"
  • Mark contracts as "signed" automatically from your existing contract.php page, or manually from the UI

Quick start

  1. Copy contracts-admin.php to a folder that is not public or is protected by HTTP auth.
  2. Make sure your contracts live in a folder named contracts alongside this file. Each file is clientid.md.
  3. Open the admin in a browser and log in with admin / changeme.
  4. Click Edit to update an .md. Click Email link to send the client a link like:

    https://modulosdesign.com.au/contracts/contract.php?clientid=3043
    

The "sent" tick updates automatically on success.

Configuration

Open the top of contracts-admin.php and adjust:

  • CONTRACTS_DIR absolute path to your contracts folder
  • BASE_URL base URL where the public contract.php lives
  • ADMIN_SHARED_SECRET shared token used by contract.php to mark items as signed
  • ADMIN_USER and ADMIN_PASS for Basic Auth
  • Database: by default uses SQLite contracts.sqlite. You can override with DB_DSN, DB_USER, DB_PASS for MySQL or Postgres

If you already have a config.php with $pdo or constants, keep it next to the file. It will be included automatically.

Database

The app auto-creates a table named contract_status.

CREATE TABLE IF NOT EXISTS contract_status (
  clientid        TEXT PRIMARY KEY,
  sent            INTEGER DEFAULT 0,
  sent_at         TEXT NULL,
  signed          INTEGER DEFAULT 0,
  signed_at       TEXT NULL,
  last_email_to   TEXT NULL,
  pdf_path        TEXT NULL,
  signer_name     TEXT NULL,
  signer_ip       TEXT NULL
);

On MySQL, use VARCHAR(64) for clientid and DATETIME for timestamps.

Hooking into your existing contract.php

After the client signs and the PDF has been generated and emailed, call the admin endpoint to mark it signed.

Add this snippet near the end of your contract.php after a successful send:

// Mark as signed in the admin database
$clientid = $_GET['clientid'] ?? '';
$signerName = $clientName ?? null; // replace with your variable if available
$pdfPath = $savedPdfPath ?? null;  // replace with your PDF path if you store it

$ch = curl_init('https://your-admin-url/contracts-admin.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
  'action' => 'mark_signed',
  'secret' => ADMIN_SHARED_SECRET, // define the same secret in both places
  'clientid' => $clientid,
  'name' => $signerName,
  'pdf' => $pdfPath,
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);

If you prefer direct DB access from contract.php, insert or update contract_status with signed=1 instead.

Email sending

The MVP uses PHP mail() by default. If PHPMailer is present and SMTP constants are defined (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE), it will send via SMTP.

The email body is simple and friendly. Tweak it in send_contract_email().

Security

  • Protect this admin with HTTP Basic Auth or an allowlist in your reverse proxy
  • The signing webhook uses a shared secret
  • Filenames are validated to prevent path traversal

Embedding

You can embed the admin as an iframe inside Grav's admin route accessible only to you. Example shortcode:

<iframe src="/protected/contracts-admin.php" width="100%" height="900" style="border:0"></iframe>

For better integration, use an Nginx rule to restrict by your IP or VPN.

Styling and UX

  • Sticky header for the table
  • Search box filters by client id or last sent email
  • Buttons for edit, email, copy link, and open

You can brand it by adding your logo and colors in the <style> block.