Update -> refactor and optimize UI , code ,...
This commit is contained in:
5
.env
Normal file
5
.env
Normal file
@@ -0,0 +1,5 @@
|
||||
MASTER_KEY=2c19e5c2-eb30-410b-807e-411643d5d730
|
||||
DB_HOST=localhost
|
||||
DB_NAME=paste
|
||||
DB_USER=root
|
||||
DB_PASS=sH1382
|
||||
8
.env.example
Normal file
8
.env.example
Normal file
@@ -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=
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
||||
.env
|
||||
.env.DS_Store
|
||||
|
||||
68
README.md
Normal file
68
README.md
Normal file
@@ -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
|
||||
```
|
||||
@@ -1,15 +1,22 @@
|
||||
<?php
|
||||
$env = parse_ini_file(__DIR__ . '/../../.env');
|
||||
$envFile = __DIR__ . '/../../.env';
|
||||
$env = file_exists($envFile) ? parse_ini_file($envFile) : [];
|
||||
|
||||
return [
|
||||
'db' => [
|
||||
'host' => $env['DB_HOST'] ?? 'localhost',
|
||||
'name' => $env['DB_NAME'] ?? 'paste',
|
||||
'user' => $env['DB_USER'] ?? 'root',
|
||||
'pass' => $env['DB_PASS'] ?? ''
|
||||
'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'
|
||||
];
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<?php
|
||||
require __DIR__ . '/../core/security.php';
|
||||
require_once __DIR__ . '/../core/security.php';
|
||||
$pdo = require __DIR__ . '/../core/db.php';
|
||||
require __DIR__ . '/../models/Paste.php';
|
||||
require_once __DIR__ . '/../models/Paste.php';
|
||||
$config = require __DIR__ . '/../config/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /index.php');
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
$text = $_POST['text'] ?? '';
|
||||
|
||||
$text = trim($_POST['text'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$expire = isset($_POST['expire']) ? intval($_POST['expire']) : 0;
|
||||
if (trim($text) === '') {
|
||||
die('Text is required');
|
||||
$expire = isset($_POST['expire']) ? (int)$_POST['expire'] : 0;
|
||||
|
||||
if ($text === '') {
|
||||
jsonResponse(['success' => false, 'message' => 'Text cannot be empty.'], 422);
|
||||
}
|
||||
|
||||
$id = generateId();
|
||||
$enc = encryptText($text, $config['master_key']);
|
||||
$enc = encryptText($text, $config['app']['master_key']);
|
||||
$password_hash = $password !== '' ? password_hash($password, PASSWORD_DEFAULT) : 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;
|
||||
jsonResponse(['success' => true, 'url' => $url]);
|
||||
|
||||
@@ -1,59 +1,56 @@
|
||||
<?php
|
||||
require __DIR__ . '/../core/security.php';
|
||||
require_once __DIR__ . '/../core/security.php';
|
||||
$pdo = require __DIR__ . '/../core/db.php';
|
||||
require __DIR__ . '/../models/Paste.php';
|
||||
require_once __DIR__ . '/../models/Paste.php';
|
||||
$config = require __DIR__ . '/../config/config.php';
|
||||
|
||||
$id = $_GET['id'] ?? '';
|
||||
$id = preg_replace('/[^a-f0-9]/i', '', $_GET['id'] ?? '');
|
||||
|
||||
if ($id === '') {
|
||||
$errorCode = 404;
|
||||
$errorMessage = 'Invalid paste ID.';
|
||||
require __DIR__ . '/../../public/error.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$paste = new Paste($pdo);
|
||||
$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 "<link rel='stylesheet' href='/assets/css/style.css'>";
|
||||
echo "<form method='post'>";
|
||||
echo "<input type='password' class='usepassword' name='password' placeholder='Password'>";
|
||||
echo "<button type='submit'>View</button>";
|
||||
echo "</form>";
|
||||
$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.');
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>View Paste</title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Your Paste</h1>
|
||||
<div class="paste-box">
|
||||
<button id="copyBtn">Copy</button>
|
||||
<pre id="pasteContent"><?= htmlspecialchars($decrypted) ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,21 @@
|
||||
<?php
|
||||
$config = require __DIR__ . '/../config/config.php';
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$config['db']['host']};dbname={$config['db']['name']};charset=utf8mb4",
|
||||
$config['db']['user'],
|
||||
$config['db']['pass'],
|
||||
[PDO::ATTR_ERRMODE => 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;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
function redisClient()
|
||||
function redisClient(): Redis
|
||||
{
|
||||
static $redis = null;
|
||||
if ($redis === null) {
|
||||
$config = require __DIR__ . '/../config/config.php';
|
||||
|
||||
$redis = new Redis();
|
||||
$redis->connect($config['redis']['host'], $config['redis']['port']);
|
||||
if (!@$redis->connect($config['redis']['host'], $config['redis']['port'])) {
|
||||
throw new RuntimeException('Redis connection failed.');
|
||||
}
|
||||
}
|
||||
return $redis;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
<?php
|
||||
function encryptText($text, $key) {
|
||||
|
||||
function encryptText(string $text, string $key): array
|
||||
{
|
||||
$iv = random_bytes(16);
|
||||
$cipher = openssl_encrypt($text, 'AES-256-CBC', $key, 0, $iv);
|
||||
if ($cipher === false) {
|
||||
throw new RuntimeException('Encryption failed.');
|
||||
}
|
||||
return [
|
||||
'cipher' => $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;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,58 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../core/db.php';
|
||||
require_once __DIR__ . '/../core/redis.php';
|
||||
|
||||
require_once __DIR__ . '/../core/redis.php';
|
||||
|
||||
class Paste
|
||||
{
|
||||
private $pdo;
|
||||
public function __construct($pdo)
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->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]);
|
||||
}
|
||||
|
||||
$redis = redisClient();
|
||||
|
||||
$expire_time = (int)$expire_time;
|
||||
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->setex(
|
||||
"paste:$id",
|
||||
$ttl,
|
||||
json_encode([
|
||||
$redis = redisClient();
|
||||
$redis->setex("paste:{$id}", $ttl, json_encode([
|
||||
'encrypted_text' => $encrypted_text,
|
||||
'iv' => $iv,
|
||||
'password_hash' => $password_hash
|
||||
])
|
||||
);
|
||||
|
||||
|
||||
'password_hash' => $password_hash,
|
||||
]));
|
||||
return true;
|
||||
}
|
||||
|
||||
$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 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")
|
||||
'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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
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();
|
||||
}
|
||||
|
||||
if (pasteForm) {
|
||||
pasteForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
const response = await fetch("/index.php?action=save", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
resultBox.innerHTML = "<div class='error'>" + data.message + "</div>";
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
function showError(msg) {
|
||||
resultBox.removeAttribute('hidden');
|
||||
resultBox.style.borderLeftColor = 'var(--error)';
|
||||
resultBox.innerHTML = `
|
||||
<h3>Paste Created</h3>
|
||||
<div class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
${msg}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
<input type="text" id="pasteLink" value="${data.url}" readonly style="width:100%; padding:8px;">
|
||||
|
||||
<button id="copyBtn">Copy Link</button>
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
|
||||
35
public/error.php
Normal file
35
public/error.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
$errorCode = $errorCode ?? 404;
|
||||
$errorMessage = $errorMessage ?? 'Page not found.';
|
||||
http_response_code($errorCode);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SafePaste — <?= $errorCode ?></title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<header class="site-header">
|
||||
<a href="/" class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M5 4h-1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1"/><path d="M9 12h6M9 16h4"/></svg>
|
||||
SafePaste
|
||||
</a>
|
||||
</header>
|
||||
<main class="main-card error-card">
|
||||
<div class="error-code"><?= $errorCode ?></div>
|
||||
<p class="error-message"><?= htmlspecialchars($errorMessage) ?></p>
|
||||
<a href="/" class="btn-primary" style="display:inline-flex;margin-top:2rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
Back to Home
|
||||
</a>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
AES-256-CBC encrypted · Open source · No tracking
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
116
public/index.php
116
public/index.php
@@ -1,28 +1,108 @@
|
||||
<?php
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
if ($action === 'save') {
|
||||
if (($_GET['action'] ?? '') === 'save') {
|
||||
require __DIR__ . '/../app/controllers/SaveController.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
?>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<script src="/assets/js/main.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SafePaste — Secure Text Sharing</title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<header class="site-header">
|
||||
<div class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M5 4h-1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1"/><path d="M9 12h6M9 16h4"/></svg>
|
||||
SafePaste
|
||||
</div>
|
||||
<span class="header-badge">End-to-end encrypted</span>
|
||||
</header>
|
||||
|
||||
<main class="main-card">
|
||||
<div class="card-header">
|
||||
<h1>New Paste</h1>
|
||||
<p>Your text is encrypted before storage. Share safely.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="?action=save" id="pasteForm">
|
||||
<textarea name="text" placeholder="Enter your text..." required></textarea>
|
||||
<input type="text" class="usepassword" name="password" placeholder="Password (Optional)">
|
||||
<select name="expire">
|
||||
<option value="60">1 MIN</option>
|
||||
<option value="3600">1 Hours</option>
|
||||
<option value="86400">2 Day</option>
|
||||
<option value="0">without time</option>
|
||||
<form id="pasteForm" novalidate>
|
||||
<div class="field">
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="pasteText"
|
||||
name="text"
|
||||
placeholder="Paste your text, code, or notes here..."
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
></textarea>
|
||||
<div class="char-counter"><span id="charCount">0</span> chars</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-row">
|
||||
<div class="field-group">
|
||||
<label for="pasteExpire">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
Expires
|
||||
</label>
|
||||
<select id="pasteExpire" name="expire">
|
||||
<option value="300">5 minutes</option>
|
||||
<option value="3600" selected>1 hour</option>
|
||||
<option value="86400">1 day</option>
|
||||
<option value="604800">1 week</option>
|
||||
<option value="0">Never</option>
|
||||
</select>
|
||||
<button type="submit">Create link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="result"></div>
|
||||
<div class="field-group">
|
||||
<label for="pastePassword">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="pastePassword"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Optional password"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" id="submitBtn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
<span>Create Secure Link</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="result" class="result-box" hidden>
|
||||
<div class="result-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Paste created successfully!
|
||||
</div>
|
||||
<div class="result-link-row">
|
||||
<input type="text" id="pasteLink" readonly>
|
||||
<button type="button" id="copyLinkBtn" class="btn-copy">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<a id="viewLink" href="#" class="result-open-link" target="_blank">
|
||||
Open paste
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
AES-256-CBC encrypted · Open source · No tracking
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
21
public/router.php
Normal file
21
public/router.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
$path = parse_url($uri, PHP_URL_PATH);
|
||||
|
||||
// Strip trailing slash
|
||||
$path = rtrim($path, '/') ?: '/';
|
||||
|
||||
// Route: /view/{id}
|
||||
if (preg_match('#^/view/([a-f0-9]+)$#i', $path, $m)) {
|
||||
$_GET['id'] = $m[1];
|
||||
require __DIR__ . '/view.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Route: static assets
|
||||
if (preg_match('#\.(css|js|png|jpg|ico|svg|woff2?)$#', $path)) {
|
||||
return false; // let PHP built-in server serve the file
|
||||
}
|
||||
|
||||
// Route: / or /index.php
|
||||
require __DIR__ . '/index.php';
|
||||
@@ -1,5 +1,95 @@
|
||||
<?php
|
||||
<?php require __DIR__ . '/../app/controllers/ViewController.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SafePaste — View</title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<header class="site-header">
|
||||
<a href="/" class="logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M5 4h-1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1"/><path d="M9 12h6M9 16h4"/></svg>
|
||||
SafePaste
|
||||
</a>
|
||||
<a href="/" class="btn-new-paste">+ New Paste</a>
|
||||
</header>
|
||||
|
||||
require __DIR__ . '/../app/controllers/ViewController.php';
|
||||
<main class="main-card">
|
||||
<?php if ($needsPassword): ?>
|
||||
|
||||
?>
|
||||
<div class="card-header">
|
||||
<div class="lock-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
</div>
|
||||
<h1>Password Required</h1>
|
||||
<p>This paste is protected. Enter the password to view it.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="<?= htmlspecialchars($_SERVER['REQUEST_URI']) ?>" class="password-form">
|
||||
<?php if ($wrongPassword): ?>
|
||||
<div class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
Incorrect password. Please try again.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="field">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="input-password"
|
||||
placeholder="Enter password..."
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Unlock Paste
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="paste-meta">
|
||||
<?php if ($data['expire_time']): ?>
|
||||
<span class="meta-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
Expires <?= htmlspecialchars(date('M j, Y H:i', (int)$data['expire_time'])) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="meta-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
No expiry
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="meta-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
||||
<?= number_format(mb_strlen($decrypted)) ?> chars
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="paste-box">
|
||||
<div class="paste-toolbar">
|
||||
<span class="paste-toolbar-title">Content</span>
|
||||
<button id="copyBtn" class="btn-icon" title="Copy to clipboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre id="pasteContent"><?= htmlspecialchars($decrypted) ?></pre>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
AES-256-CBC encrypted · Open source · No tracking
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user