diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..1735dd7
Binary files /dev/null and b/.DS_Store differ
diff --git a/.env b/.env
new file mode 100644
index 0000000..714b528
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+MASTER_KEY=2c19e5c2-eb30-410b-807e-411643d5d730
+DB_HOST=localhost
+DB_NAME=paste
+DB_USER=root
+DB_PASS=sH1382
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..0700f5e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,8 @@
+MASTER_KEY=change_me_to_a_random_secret
+DB_HOST=localhost
+DB_NAME=paste
+DB_USER=root
+DB_PASS=
+REDIS_HOST=127.0.0.1
+REDIS_PORT=6379
+APP_URL=
diff --git a/.gitignore b/.gitignore
index 2eea525..6c23564 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-.env
\ No newline at end of file
+.env.DS_Store
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..037b3c5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+# SafePaste
+
+A minimal, encrypted pastebin built with PHP, MySQL, and Redis.
+
+## Features
+
+- **AES-256-CBC encryption** — all pastes are encrypted at rest using a master key
+- **Optional password protection** — bcrypt-hashed passwords per paste
+- **TTL expiry via Redis** — short-lived pastes live only in Redis; permanent pastes use MySQL
+- **Clean dark UI** — responsive, accessible, no external dependencies
+
+## Project Structure
+
+```
+safe-paste/
+├── app/
+│ ├── config/config.php # Loads .env settings
+│ ├── core/
+│ │ ├── db.php # PDO connection
+│ │ ├── redis.php # Redis singleton
+│ │ └── security.php # Encrypt/decrypt helpers
+│ ├── controllers/
+│ │ ├── SaveController.php # POST handler: create paste
+│ │ └── ViewController.php # GET/POST handler: view paste
+│ └── models/Paste.php # Save/get paste (Redis + MySQL)
+└── public/
+ ├── index.php # Home page + save action
+ ├── view.php # View paste page
+ ├── error.php # Error page
+ ├── .htaccess # URL routing
+ └── assets/
+ ├── css/style.css
+ └── js/app.js
+```
+
+## Setup
+
+1. Copy `.env.example` to `.env` and fill in your values
+2. Create the MySQL database and table:
+
+```sql
+CREATE DATABASE paste CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE paste;
+CREATE TABLE pastes (
+ id CHAR(32) PRIMARY KEY,
+ encrypted_text TEXT NOT NULL,
+ iv VARCHAR(64) NOT NULL,
+ expire_time INT DEFAULT NULL,
+ password_hash VARCHAR(255) DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+3. Point your web server document root to `public/`
+4. Ensure `mod_rewrite` is enabled (Apache) or configure your nginx equivalent
+
+## .env
+
+```ini
+MASTER_KEY=your-random-secret-key
+DB_HOST=localhost
+DB_NAME=paste
+DB_USER=root
+DB_PASS=secret
+REDIS_HOST=127.0.0.1
+REDIS_PORT=6379
+APP_URL=https://yourdomain.com
+```
diff --git a/app/config/config.php b/app/config/config.php
index fd93941..20f0d84 100644
--- a/app/config/config.php
+++ b/app/config/config.php
@@ -1,15 +1,22 @@
[
- 'host' => $env['DB_HOST'] ?? 'localhost',
- 'name' => $env['DB_NAME'] ?? 'paste',
- 'user' => $env['DB_USER'] ?? 'root',
- 'pass' => $env['DB_PASS'] ?? ''
+ 'host' => $env['DB_HOST'] ?? 'localhost',
+ 'name' => $env['DB_NAME'] ?? 'paste',
+ 'user' => $env['DB_USER'] ?? 'root',
+ 'pass' => $env['DB_PASS'] ?? '',
+ 'charset' => 'utf8mb4',
],
'redis' => [
- 'host' => '127.0.0.1',
- 'port' => 6379
+ 'host' => $env['REDIS_HOST'] ?? '127.0.0.1',
+ 'port' => (int)($env['REDIS_PORT'] ?? 6379),
+ ],
+ 'app' => [
+ 'master_key' => $env['MASTER_KEY'] ?? 'change_me_master_key',
+ 'base_url' => $env['APP_URL'] ?? '',
+ 'cipher' => 'AES-256-CBC',
],
- 'master_key' => $env['MASTER_KEY'] ?? 'change_me_master_key'
];
diff --git a/app/controllers/SaveController.php b/app/controllers/SaveController.php
index b895ce4..3330479 100644
--- a/app/controllers/SaveController.php
+++ b/app/controllers/SaveController.php
@@ -1,31 +1,31 @@
false, 'message' => 'Text cannot be empty.'], 422);
}
-$id = generateId();
-$enc = encryptText($text, $config['master_key']);
+
+$id = generateId();
+$enc = encryptText($text, $config['app']['master_key']);
$password_hash = $password !== '' ? password_hash($password, PASSWORD_DEFAULT) : null;
-$expire_time = $expire > 0 ? time() + $expire : null;
+$expire_time = $expire > 0 ? time() + $expire : null;
+
$paste = new Paste($pdo);
+$paste->save($id, $enc['cipher'], $enc['iv'], $expire_time, $password_hash);
-$paste->save($id, $enc['cipher'], $enc['iv'], $expire_time,$password_hash);
+$base = rtrim($config['app']['base_url'] ?: ('http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . $_SERVER['HTTP_HOST']), '/');
+$url = $base . '/view/' . $id;
-
-$url = "http://" . $_SERVER['HTTP_HOST'] . "/view.php?id=" . $id;
-
-echo json_encode([
- "success" => true,
- "url" => $url
-]);
-exit;
\ No newline at end of file
+jsonResponse(['success' => true, 'url' => $url]);
diff --git a/app/controllers/ViewController.php b/app/controllers/ViewController.php
index 7128c82..bfa3f76 100644
--- a/app/controllers/ViewController.php
+++ b/app/controllers/ViewController.php
@@ -1,59 +1,56 @@
get($id);
+$data = $paste->get($id);
if (!$data) {
- die('Paste not found.');
+ $errorCode = 404;
+ $errorMessage = 'Paste not found.';
+ require __DIR__ . '/../../public/error.php';
+ exit;
}
if ($data['expire_time'] !== null && time() > (int)$data['expire_time']) {
- die('Paste has expired.');
+ $errorCode = 410;
+ $errorMessage = 'This paste has expired.';
+ require __DIR__ . '/../../public/error.php';
+ exit;
}
-if ($data['password_hash']) {
- if (!isset($_POST['password'])) {
- echo "";
- echo "
";
+$needsPassword = (bool)$data['password_hash'];
+$wrongPassword = false;
+$decrypted = null;
+
+if ($needsPassword) {
+ $submitted = $_POST['password'] ?? null;
+ if ($submitted !== null) {
+ if (password_verify($submitted, $data['password_hash'])) {
+ $needsPassword = false;
+ } else {
+ $wrongPassword = true;
+ }
+ }
+}
+
+if (!$needsPassword) {
+ $decrypted = decryptText($data['encrypted_text'], $data['iv'], $config['app']['master_key']);
+ if ($decrypted === false) {
+ $errorCode = 500;
+ $errorMessage = 'Decryption failed. The paste may be corrupted.';
+ require __DIR__ . '/../../public/error.php';
exit;
}
-
- if (!password_verify($_POST['password'], $data['password_hash'])) {
- die('Wrong password.');
- }
}
-
-$decrypted = decryptText($data['encrypted_text'], $data['iv'], $config['master_key']);
-
-if ($decrypted === false) {
- die('Decryption failed.');
-}
-?>
-
-
-
-
-
- View Paste
-
-
-
-
-
-
Your Paste
-
-
-
= htmlspecialchars($decrypted) ?>
-
-
-
-
\ No newline at end of file
diff --git a/app/core/db.php b/app/core/db.php
index 7fc0cd1..4a41208 100644
--- a/app/core/db.php
+++ b/app/core/db.php
@@ -1,13 +1,21 @@
PDO::ERRMODE_EXCEPTION]
+ $dsn = sprintf(
+ 'mysql:host=%s;dbname=%s;charset=%s',
+ $config['db']['host'],
+ $config['db']['name'],
+ $config['db']['charset']
);
-} catch (Exception $e) {
- die('Database connection error');
+ $pdo = new PDO($dsn, $config['db']['user'], $config['db']['pass'], [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ]);
+} catch (PDOException $e) {
+ http_response_code(503);
+ die(json_encode(['success' => false, 'message' => 'Database connection error.']));
}
+
return $pdo;
diff --git a/app/core/redis.php b/app/core/redis.php
index 613cb18..f848d93 100644
--- a/app/core/redis.php
+++ b/app/core/redis.php
@@ -1,13 +1,14 @@
connect($config['redis']['host'], $config['redis']['port']);
+ $redis = new Redis();
+ if (!@$redis->connect($config['redis']['host'], $config['redis']['port'])) {
+ throw new RuntimeException('Redis connection failed.');
+ }
}
return $redis;
}
diff --git a/app/core/security.php b/app/core/security.php
index f3c6238..4ea3b50 100644
--- a/app/core/security.php
+++ b/app/core/security.php
@@ -1,15 +1,32 @@
$cipher,
- 'iv' => base64_encode($iv)
+ 'iv' => base64_encode($iv),
];
}
-function decryptText($cipher, $iv, $key) {
+
+function decryptText(string $cipher, string $iv, string $key): string|false
+{
return openssl_decrypt($cipher, 'AES-256-CBC', $key, 0, base64_decode($iv));
}
-function generateId() {
+
+function generateId(): string
+{
return bin2hex(random_bytes(16));
}
+
+function jsonResponse(array $data, int $status = 200): void
+{
+ http_response_code($status);
+ header('Content-Type: application/json');
+ echo json_encode($data);
+ exit;
+}
diff --git a/app/models/Paste.php b/app/models/Paste.php
index 01e4e50..ecf512b 100644
--- a/app/models/Paste.php
+++ b/app/models/Paste.php
@@ -1,62 +1,58 @@
pdo = $pdo;
}
- public function save($id, $encrypted_text, $iv, $expire_time, $password_hash)
- {
- if ($expire_time === null) {
- $stmt = $this->pdo->prepare("INSERT INTO pastes(id, encrypted_text, iv, expire_time, password_hash)
- VALUES (?, ?, ?, NULL, ?)");
- return $stmt->execute([$id, $encrypted_text, $iv, $password_hash]);
+ public function save(
+ string $id,
+ string $encrypted_text,
+ string $iv,
+ ?int $expire_time,
+ ?string $password_hash
+ ): bool {
+ if ($expire_time !== null) {
+ $ttl = max(1, $expire_time - time());
+ $redis = redisClient();
+ $redis->setex("paste:{$id}", $ttl, json_encode([
+ 'encrypted_text' => $encrypted_text,
+ 'iv' => $iv,
+ 'password_hash' => $password_hash,
+ ]));
+ return true;
}
- $redis = redisClient();
-
- $expire_time = (int)$expire_time;
- $ttl = max(1, $expire_time - time());
-
-
- $redis->setex(
- "paste:$id",
- $ttl,
- json_encode([
- 'encrypted_text' => $encrypted_text,
- 'iv' => $iv,
- 'password_hash' => $password_hash
- ])
+ $stmt = $this->pdo->prepare(
+ 'INSERT INTO pastes (id, encrypted_text, iv, expire_time, password_hash)
+ VALUES (?, ?, ?, NULL, ?)'
);
-
-
- return true;
+ return $stmt->execute([$id, $encrypted_text, $iv, $password_hash]);
}
-
- public function get($id)
+ public function get(string $id): array|false
{
$redis = redisClient();
+ $raw = $redis->get("paste:{$id}");
- $data = $redis->get("paste:$id");
- if ($data !== false) {
- $json = json_decode($data, true);
+ if ($raw !== false) {
+ $json = json_decode($raw, true);
return [
'encrypted_text' => $json['encrypted_text'],
- 'iv' => $json['iv'],
- 'password_hash' => $json['password_hash'],
- 'expire_time' => time() + $redis->ttl("paste:$id")
+ 'iv' => $json['iv'],
+ 'password_hash' => $json['password_hash'],
+ 'expire_time' => time() + $redis->ttl("paste:{$id}"),
];
}
- $stmt = $this->pdo->prepare("SELECT * FROM pastes WHERE id = ?");
+ $stmt = $this->pdo->prepare('SELECT * FROM pastes WHERE id = ? LIMIT 1');
$stmt->execute([$id]);
- return $stmt->fetch(PDO::FETCH_ASSOC);
+ return $stmt->fetch();
}
}
diff --git a/public/.htaccess b/public/.htaccess
index e69de29..f678b92 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -0,0 +1,12 @@
+Options -Indexes
+DirectoryIndex index.php
+
+RewriteEngine On
+
+# Route /view/{id} -> view.php?id={id} (GET and POST)
+RewriteCond %{REQUEST_METHOD} GET [OR]
+RewriteCond %{REQUEST_METHOD} POST
+RewriteRule ^view/([a-f0-9]+)/?$ view.php?id=$1 [L,QSA]
+
+# Block direct access to .env and sensitive files
+RewriteRule ^(\.env|\.git) - [F,L]
diff --git a/public/assets/css/style.css b/public/assets/css/style.css
index 695dd8e..2842f22 100644
--- a/public/assets/css/style.css
+++ b/public/assets/css/style.css
@@ -1,83 +1,512 @@
-* {
+/* ─── Reset & Base ─── */
+*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
- font-family: monospace;
}
+:root {
+ --bg: #080e1a;
+ --surface: #0d1526;
+ --surface-2: #111d33;
+ --border: #1e2f4a;
+ --border-soft: #172137;
+ --accent: #3b9eff;
+ --accent-glow: rgba(59,158,255,0.18);
+ --accent-dark: #1a6fd4;
+ --text: #dce8f8;
+ --text-muted: #5f7da0;
+ --text-dim: #3a5272;
+ --success: #22c55e;
+ --error: #f43f5e;
+ --error-bg: rgba(244,63,94,0.1);
+ --radius: 12px;
+ --radius-sm: 8px;
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
+ --font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ --shadow: 0 4px 24px rgba(0,0,0,0.4);
+ --shadow-lg: 0 8px 48px rgba(0,0,0,0.6);
+}
+
+html { font-size: 15px; }
+
body {
- background: #0f172a;
- color: #e2e8f0;
- padding: 40px;
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ min-height: 100vh;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
}
-/* container */
-.container {
- max-width: 900px;
- margin: auto;
+/* ─── Page Layout ─── */
+.page-wrapper {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ max-width: 760px;
+ margin: 0 auto;
+ padding: 0 1.25rem;
}
-/* title */
-h1 {
- margin-bottom: 20px;
- font-size: 28px;
+/* ─── Header ─── */
+.site-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1.4rem 0;
+ border-bottom: 1px solid var(--border-soft);
+ margin-bottom: 2.5rem;
}
-/* textarea */
-textarea {
- width: 100%;
- min-height: 300px;
- background: #020617;
- border: 1px solid #334155;
- color: #e2e8f0;
- padding: 12px;
- resize: vertical;
- font-size: 14px;
- border-radius: 6px;
-}
-
-.usepassword,
-select {
- background: #020617;
- border: 1px solid #334155;
- color: #e2e8f0;
- padding: 8px;
- border-radius: 6px;
- margin-top: 10px;
-}
-
-/* button */
-button {
- background: #38bdf8;
- border: none;
- color: #020617;
- padding: 10px 16px;
- margin-top: 15px;
- border-radius: 6px;
- cursor: pointer;
- font-weight: bold;
-}
-
-button:hover {
- background: #0ea5e9;
-}
-
-/* paste display */
-.paste-box {
- background: #020617;
- border: 1px solid #334155;
- padding: 15px;
- margin-top: 20px;
- border-radius: 6px;
- overflow-x: auto;
-}
-
-/* link */
-a {
- color: #38bdf8;
+.logo {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 700;
+ font-size: 1.05rem;
+ letter-spacing: -0.01em;
+ color: var(--text);
text-decoration: none;
}
-a:hover {
- text-decoration: underline;
+.logo svg { color: var(--accent); }
+
+.header-badge {
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--accent);
+ background: var(--accent-glow);
+ border: 1px solid rgba(59,158,255,0.25);
+ padding: 0.28rem 0.65rem;
+ border-radius: 999px;
+}
+
+.btn-new-paste {
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: var(--text);
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ padding: 0.45rem 1rem;
+ border-radius: var(--radius-sm);
+ text-decoration: none;
+ transition: border-color 0.18s, background 0.18s;
+}
+
+.btn-new-paste:hover {
+ border-color: var(--accent);
+ background: var(--accent-glow);
+ text-decoration: none;
+}
+
+/* ─── Main Card ─── */
+.main-card {
+ flex: 1;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 2rem 2.25rem;
+ box-shadow: var(--shadow);
+}
+
+.card-header {
+ margin-bottom: 1.75rem;
+}
+
+.card-header h1 {
+ font-size: 1.45rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: var(--text);
+ margin-bottom: 0.3rem;
+}
+
+.card-header p {
+ font-size: 0.875rem;
+ color: var(--text-muted);
+}
+
+/* ─── Form Fields ─── */
+.field { margin-bottom: 1rem; }
+
+.textarea-wrapper { position: relative; }
+
+textarea {
+ width: 100%;
+ min-height: 280px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font-family: var(--font-mono);
+ font-size: 0.875rem;
+ line-height: 1.7;
+ padding: 1rem 1.1rem 2.5rem;
+ resize: vertical;
+ outline: none;
+ transition: border-color 0.18s, box-shadow 0.18s;
+ tab-size: 4;
+}
+
+textarea:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+textarea::placeholder { color: var(--text-dim); }
+
+.char-counter {
+ position: absolute;
+ bottom: 0.6rem;
+ right: 0.85rem;
+ font-size: 0.72rem;
+ color: var(--text-dim);
+ font-family: var(--font-mono);
+ pointer-events: none;
+}
+
+/* ─── Options Row ─── */
+.options-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ margin-bottom: 1.25rem;
+}
+
+.field-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.45rem;
+}
+
+.field-group label {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.78rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--text-muted);
+}
+
+select,
+input[type="password"],
+input[type="text"] {
+ width: 100%;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: 0.875rem;
+ padding: 0.6rem 0.85rem;
+ outline: none;
+ transition: border-color 0.18s, box-shadow 0.18s;
+ appearance: none;
+ -webkit-appearance: none;
+}
+
+select {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235f7da0' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2.2rem;
+}
+
+select:focus,
+input[type="password"]:focus,
+input[type="text"]:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+select option { background: var(--surface-2); }
+
+/* ─── Buttons ─── */
+.btn-primary {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ border-radius: var(--radius-sm);
+ padding: 0.7rem 1.4rem;
+ font-family: var(--font-sans);
+ font-size: 0.9rem;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: none;
+ transition: background 0.18s, transform 0.1s, box-shadow 0.18s;
+}
+
+.btn-primary:hover {
+ background: var(--accent-dark);
+ box-shadow: 0 0 20px var(--accent-glow);
+ text-decoration: none;
+}
+
+.btn-primary:active { transform: scale(0.98); }
+
+.btn-primary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-icon {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: var(--surface-2);
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 0.4rem 0.85rem;
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: color 0.18s, border-color 0.18s, background 0.18s;
+}
+
+.btn-icon:hover {
+ color: var(--accent);
+ border-color: var(--accent);
+ background: var(--accent-glow);
+}
+
+/* ─── Result Box ─── */
+.result-box {
+ margin-top: 1.5rem;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-left: 3px solid var(--success);
+ border-radius: var(--radius-sm);
+ padding: 1.25rem 1.4rem;
+}
+
+.result-box[hidden] { display: none; }
+
+.result-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--success);
+ margin-bottom: 1rem;
+}
+
+.result-link-row {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 0.85rem;
+}
+
+.result-link-row input[type="text"] {
+ flex: 1;
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ cursor: text;
+}
+
+.btn-copy {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: var(--surface);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 0.5rem 1rem;
+ font-size: 0.82rem;
+ font-weight: 600;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.18s, border-color 0.18s, color 0.18s;
+}
+
+.btn-copy:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.btn-copy.copied {
+ color: var(--success);
+ border-color: var(--success);
+}
+
+.result-open-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ text-decoration: none;
+ transition: color 0.18s;
+}
+
+.result-open-link:hover {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+/* ─── Paste View ─── */
+.paste-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1.25rem;
+}
+
+.meta-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--text-muted);
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ padding: 0.28rem 0.65rem;
+ border-radius: 999px;
+}
+
+.paste-box {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+}
+
+.paste-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.6rem 0.85rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface-2);
+}
+
+.paste-toolbar-title {
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--text-dim);
+}
+
+#pasteContent {
+ font-family: var(--font-mono);
+ font-size: 0.875rem;
+ line-height: 1.75;
+ color: var(--text);
+ padding: 1.25rem 1.4rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+ overflow-x: auto;
+ margin: 0;
+}
+
+/* ─── Password Prompt ─── */
+.lock-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 56px;
+ height: 56px;
+ background: var(--accent-glow);
+ border: 1px solid rgba(59,158,255,0.3);
+ border-radius: 14px;
+ margin-bottom: 1.25rem;
+ color: var(--accent);
+}
+
+.password-form { max-width: 380px; }
+
+.input-password {
+ width: 100%;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font-size: 0.9rem;
+ padding: 0.7rem 0.95rem;
+ outline: none;
+ margin-bottom: 1rem;
+ transition: border-color 0.18s, box-shadow 0.18s;
+}
+
+.input-password:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+/* ─── Alerts ─── */
+.alert {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+ font-weight: 500;
+ margin-bottom: 1rem;
+}
+
+.alert-error {
+ background: var(--error-bg);
+ border: 1px solid rgba(244,63,94,0.3);
+ color: var(--error);
+}
+
+/* ─── Error Page ─── */
+.error-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 4rem 2rem;
+ min-height: 320px;
+}
+
+.error-code {
+ font-size: 5rem;
+ font-weight: 800;
+ letter-spacing: -0.04em;
+ color: var(--border);
+ line-height: 1;
+ margin-bottom: 0.75rem;
+}
+
+.error-message {
+ font-size: 1rem;
+ color: var(--text-muted);
+ max-width: 340px;
+}
+
+/* ─── Footer ─── */
+.site-footer {
+ padding: 1.5rem 0;
+ margin-top: 2rem;
+ text-align: center;
+ font-size: 0.75rem;
+ color: var(--text-dim);
+ border-top: 1px solid var(--border-soft);
+}
+
+/* ─── Submit button loading state ─── */
+.btn-primary.loading { opacity: 0.7; pointer-events: none; }
+
+/* ─── Responsive ─── */
+@media (max-width: 560px) {
+ .main-card { padding: 1.5rem 1.25rem; }
+ .options-row { grid-template-columns: 1fr; }
+ .result-link-row { flex-direction: column; }
+ .btn-copy { width: 100%; justify-content: center; }
}
diff --git a/public/assets/js/app.js b/public/assets/js/app.js
index fb1c30f..af98321 100644
--- a/public/assets/js/app.js
+++ b/public/assets/js/app.js
@@ -1,38 +1,106 @@
-document.addEventListener("DOMContentLoaded", () => {
+document.addEventListener('DOMContentLoaded', () => {
- const form = document.getElementById("pasteForm");
- const resultBox = document.getElementById("result");
+ /* ── Create-paste page ── */
+ const pasteForm = document.getElementById('pasteForm');
+ const resultBox = document.getElementById('result');
+ const pasteText = document.getElementById('pasteText');
+ const charCount = document.getElementById('charCount');
+ const submitBtn = document.getElementById('submitBtn');
- form.addEventListener("submit", async (e) => {
- e.preventDefault();
+ if (pasteText && charCount) {
+ const update = () => {
+ charCount.textContent = pasteText.value.length.toLocaleString();
+ pasteText.style.height = 'auto';
+ pasteText.style.height = Math.max(280, pasteText.scrollHeight) + 'px';
+ };
+ pasteText.addEventListener('input', update);
+ update();
+ }
- const formData = new FormData(form);
+ if (pasteForm) {
+ pasteForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
- const response = await fetch("/index.php?action=save", {
- method: "POST",
- body: formData
+ if (!pasteText || pasteText.value.trim() === '') {
+ pasteText.focus();
+ pasteText.style.borderColor = 'var(--error)';
+ setTimeout(() => pasteText.style.borderColor = '', 1200);
+ return;
+ }
+
+ submitBtn.classList.add('loading');
+ submitBtn.querySelector('span').textContent = 'Creating...';
+
+ try {
+ const res = await fetch('/index.php?action=save', {
+ method: 'POST',
+ body: new FormData(pasteForm),
+ });
+ const data = await res.json();
+
+ if (!data.success) {
+ showError(data.message || 'Something went wrong.');
+ return;
+ }
+
+ const linkInput = document.getElementById('pasteLink');
+ const viewLink = document.getElementById('viewLink');
+ linkInput.value = data.url;
+ viewLink.href = data.url;
+ resultBox.removeAttribute('hidden');
+ resultBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+
+ pasteText.value = '';
+ if (charCount) charCount.textContent = '0';
+
+ } catch {
+ showError('Network error. Please try again.');
+ } finally {
+ submitBtn.classList.remove('loading');
+ submitBtn.querySelector('span').textContent = 'Create Secure Link';
+ }
});
- const data = await response.json();
-
- if (!data.success) {
- resultBox.innerHTML = "" + data.message + "
";
- return;
+ function showError(msg) {
+ resultBox.removeAttribute('hidden');
+ resultBox.style.borderLeftColor = 'var(--error)';
+ resultBox.innerHTML = `
+
+
+ ${msg}
+
`;
}
+ }
- resultBox.innerHTML = `
- Paste Created
-
-
-
-
- `;
-
- document.getElementById("copyBtn").addEventListener("click", () => {
- const input = document.getElementById("pasteLink");
- navigator.clipboard.writeText(input.value);
- alert("Link copied!");
- });
+ /* ── Copy link button (result box) ── */
+ document.addEventListener('click', (e) => {
+ const btn = e.target.closest('#copyLinkBtn');
+ if (!btn) return;
+ const val = document.getElementById('pasteLink')?.value;
+ if (!val) return;
+ copyToClipboard(val, btn, 'Copied!', 'Copy');
});
+ /* ── View-paste page: copy content ── */
+ const copyBtn = document.getElementById('copyBtn');
+ if (copyBtn) {
+ copyBtn.addEventListener('click', () => {
+ const content = document.getElementById('pasteContent')?.innerText;
+ if (!content) return;
+ copyToClipboard(content, copyBtn, 'Copied!', 'Copy');
+ });
+ }
+
+ /* ── Shared helper ── */
+ function copyToClipboard(text, btn, successLabel, defaultLabel) {
+ navigator.clipboard.writeText(text).then(() => {
+ const span = btn.querySelector('span') || btn;
+ btn.classList.add('copied');
+ span.textContent = successLabel;
+ setTimeout(() => {
+ btn.classList.remove('copied');
+ span.textContent = defaultLabel;
+ }, 2000);
+ });
+ }
});
diff --git a/public/assets/js/main.js b/public/assets/js/main.js
index 6b9953d..598c59f 100644
--- a/public/assets/js/main.js
+++ b/public/assets/js/main.js
@@ -1,40 +1 @@
-document.addEventListener("DOMContentLoaded", () => {
-
- const textarea = document.querySelector("textarea");
- const form = document.querySelector("form");
- const copyBtn = document.getElementById("copyBtn");
-
-
- if (textarea) {
- textarea.addEventListener("input", () => {
- textarea.style.height = "auto";
- textarea.style.height = textarea.scrollHeight + "px";
- });
- }
-
- if (form && textarea) {
- form.addEventListener("submit", (e) => {
- if (textarea.value.trim() === "") {
- e.preventDefault();
- alert("Paste cannot be empty");
- }
- });
- }
-
- if (copyBtn) {
- copyBtn.addEventListener("click", () => {
- const paste = document.getElementById("pasteContent");
-
- if (!paste) return;
-
- navigator.clipboard.writeText(paste.innerText)
- .then(() => {
- copyBtn.textContent = "Copied!";
- setTimeout(() => {
- copyBtn.textContent = "Copy";
- }, 2000);
- });
- });
- }
-
-});
+/* merged into app.js */
diff --git a/public/error.php b/public/error.php
new file mode 100644
index 0000000..daaabd0
--- /dev/null
+++ b/public/error.php
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ SafePaste — = $errorCode ?>
+
+
+
+
+
+
+ = $errorCode ?>
+ = htmlspecialchars($errorMessage) ?>
+
+
+ Back to Home
+
+
+
+
+
+
diff --git a/public/index.php b/public/index.php
index 7dad121..7694c57 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,28 +1,108 @@
-
-
-
+
+
+
+
+
+ SafePaste — Secure Text Sharing
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/router.php b/public/router.php
new file mode 100644
index 0000000..7fc9ee5
--- /dev/null
+++ b/public/router.php
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ SafePaste — View
+
+
+
+
+
-require __DIR__ . '/../app/controllers/ViewController.php';
+
+
-?>
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($decrypted) ?>
+
+
+
+
+
+
+
+
+
+
+