404
The page you visited was not found.
diff --git a/SERVER_SETUP.md b/SERVER_SETUP.md deleted file mode 100644 index 67f49da..0000000 --- a/SERVER_SETUP.md +++ /dev/null @@ -1,190 +0,0 @@ -# Server Setup: PostgreSQL + PHP Auth for freedoms4 - -## Overview - -``` -/var/www/freedoms4/ ← Hugo's published docs/ folder (static files) -/var/www/freedoms4/api/ ← PHP backend (auth.php lives here) -``` - -Nginx serves the static Hugo site and passes `/api/` requests to PHP-FPM. - ---- - -## 1 · Install PostgreSQL - -```bash -sudo apt update -sudo apt install -y postgresql postgresql-contrib -sudo systemctl enable --now postgresql -``` - ---- - -## 2 · Create the database and user - -```bash -sudo -u postgres psql -``` - -Inside the psql shell: - -```sql -CREATE USER freedoms4_user WITH PASSWORD 'CHANGE_THIS_PASSWORD'; -CREATE DATABASE freedoms4 OWNER freedoms4_user; -\c freedoms4 - -CREATE TABLE users ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(32) NOT NULL UNIQUE, - email VARCHAR(254) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX idx_users_username ON users (username); -CREATE INDEX idx_users_email ON users (email); - --- Optional: allow only this user to access the table -REVOKE ALL ON TABLE users FROM PUBLIC; -GRANT SELECT, INSERT ON TABLE users TO freedoms4_user; -GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO freedoms4_user; - -\q -``` - -> **Important:** use the same password you set in `DB_PASS` inside `auth.php`. - ---- - -## 3 · Install the PHP PostgreSQL extension - -```bash -# Find your PHP version first: -php -v - -# Install the pgsql extension (replace 8.x with your version, e.g. 8.3): -sudo apt install -y php8.3-pgsql - -# Restart PHP-FPM (replace 8.3 with your version): -sudo systemctl restart php8.3-fpm -``` - -Verify it loaded: - -```bash -php -m | grep pgsql # should print: pgsql -``` - ---- - -## 4 · Deploy the PHP file - -```bash -sudo mkdir -p /var/www/freedoms4/api -sudo cp /path/to/auth.php /var/www/freedoms4/api/auth.php -sudo chown -R www-data:www-data /var/www/freedoms4/api -sudo chmod 640 /var/www/freedoms4/api/auth.php -``` - -Edit the config constants at the top of `auth.php`: - -```php -define('DB_PASS', 'CHANGE_THIS_PASSWORD'); // ← your actual password -``` - ---- - -## 5 · Configure Nginx - -Open your site config (e.g. `/etc/nginx/sites-available/freedoms4`): - -```nginx -server { - listen 443 ssl http2; - server_name freedoms4.org www.freedoms4.org; - - root /var/www/freedoms4; - index index.html; - - # ── Static Hugo files ─────────────────────────────────────────────── - location / { - try_files $uri $uri/ $uri/index.html =404; - } - - # ── PHP API ───────────────────────────────────────────────────────── - location /api/ { - # Only allow POST (OPTIONS for CORS preflight) - limit_except POST OPTIONS { - deny all; - } - - # Pass to PHP-FPM (adjust socket path to match your PHP version) - fastcgi_pass unix:/run/php/php8.3-fpm.sock; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - } - - # ── Block direct access to .php files outside /api/ ───────────────── - location ~* \.php$ { - deny all; - } - - # SSL certs (already configured, adjust paths if needed) - ssl_certificate /etc/letsencrypt/live/freedoms4.org/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/freedoms4.org/privkey.pem; -} -``` - -Test and reload: - -```bash -sudo nginx -t -sudo systemctl reload nginx -``` - ---- - -## 6 · Deploy the Hugo frontend changes - -In your local Hugo project, apply the three file changes from the delivery package, then rebuild and sync: - -```bash -# From inside the freedoms4 project directory: -hugo --minify - -# Sync to server (adjust user/host): -rsync -avz --delete docs/ user@your-vps:/var/www/freedoms4/ -``` - -Or if you use git+CI, commit and push; your pipeline handles the rest. - ---- - -## 7 · Test - -```bash -# Sign up -curl -s -X POST https://freedoms4.org/api/auth.php \ - -H 'Content-Type: application/json' \ - -d '{"action":"signup","username":"testuser","email":"test@example.com","password":"hunter2hunter2"}' | jq . - -# Log in -curl -s -X POST https://freedoms4.org/api/auth.php \ - -H 'Content-Type: application/json' \ - -d '{"action":"login","username":"testuser","password":"hunter2hunter2"}' | jq . -``` - -Both should return `{"success":true, ...}`. - ---- - -## Security notes - -- All passwords are stored as bcrypt hashes (cost 12). Plain-text passwords are never written to disk or logs. -- Session cookies are `HttpOnly`, `Secure`, and `SameSite=Strict`. -- A simple per-IP rate limit (20 requests per 15 min) is enforced server-side via PHP sessions. -- For production, consider adding `fail2ban` rules on your Nginx access log to block repeated 429s at the firewall level. -- Keep `SESSION_SECURE = true` (requires HTTPS, which you already have). diff --git a/api/auth.php b/api/auth.php deleted file mode 100644 index 3605652..0000000 --- a/api/auth.php +++ /dev/null @@ -1,195 +0,0 @@ - false, 'message' => 'Forbidden.']); - exit; -} - -if ($origin) { - header('Access-Control-Allow-Origin: ' . $origin); - header('Access-Control-Allow-Methods: POST, OPTIONS'); - header('Access-Control-Allow-Headers: Content-Type'); - header('Access-Control-Allow-Credentials: true'); -} - -if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { - http_response_code(204); - exit; -} - -// ── Helpers ────────────────────────────────────────────────────────────────── -function json_out(array $data, int $status = 200): never { - http_response_code($status); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($data); - exit; -} - -function start_session(): void { - if (session_status() === PHP_SESSION_NONE) { - session_name(SESSION_NAME); - session_set_cookie_params([ - 'lifetime' => 0, - 'path' => '/', - 'secure' => SESSION_SECURE, - 'httponly' => true, - 'samesite' => SESSION_SAMESITE, - ]); - session_start(); - } -} - -function db_connect(): PDO { - static $pdo = null; - if ($pdo !== null) return $pdo; - - $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', DB_HOST, DB_PORT, DB_NAME); - - try { - $pdo = new PDO($dsn, DB_USER, DB_PASS, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - ]); - } catch (PDOException $e) { - error_log('DB connection failed: ' . $e->getMessage()); - json_out(['success' => false, 'message' => 'Database unavailable. Please try again later.'], 503); - } - - return $pdo; -} - -// ── Only accept POST ────────────────────────────────────────────────────────── -if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - json_out(['success' => false, 'message' => 'Method not allowed.'], 405); -} - -// ── Parse JSON body ─────────────────────────────────────────────────────────── -$body = json_decode(file_get_contents('php://input'), true); - -if (!is_array($body)) { - json_out(['success' => false, 'message' => 'Invalid request body.'], 400); -} - -$action = $body['action'] ?? ''; - -// ── Rate limiting (per-IP via session) ──────────────────────────────────────── -start_session(); -$now = time(); -$rl_key = 'rl_' . hash('sha256', $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); -$rl = $_SESSION[$rl_key] ?? ['count' => 0, 'window_start' => $now]; - -if ($now - $rl['window_start'] > 900) { - $rl = ['count' => 0, 'window_start' => $now]; -} - -$rl['count']++; -$_SESSION[$rl_key] = $rl; - -if ($rl['count'] > 20) { - json_out(['success' => false, 'message' => 'Too many requests. Please wait a few minutes.'], 429); -} - -// ════════════════════════════════════════════════════════════════════════════ -// ACTION: login -// ════════════════════════════════════════════════════════════════════════════ -if ($action === 'login') { - $username = trim($body['username'] ?? ''); - $password = $body['password'] ?? ''; - - if ($username === '' || $password === '') { - json_out(['success' => false, 'message' => 'Username and password are required.']); - } - - $pdo = db_connect(); - $stmt = $pdo->prepare('SELECT id, username, password_hash FROM users WHERE username = :u LIMIT 1'); - $stmt->execute([':u' => $username]); - $user = $stmt->fetch(); - - $hash = $user['password_hash'] ?? '$2y$12$invalidhashpadding000000000000000000000000000000000000000'; - if (!$user || !password_verify($password, $hash)) { - json_out(['success' => false, 'message' => 'Invalid username or password.']); - } - - session_regenerate_id(true); - $_SESSION['user_id'] = $user['id']; - $_SESSION['username'] = $user['username']; - - json_out(['success' => true, 'redirect' => '/']); -} - -// ════════════════════════════════════════════════════════════════════════════ -// ACTION: signup -// ════════════════════════════════════════════════════════════════════════════ -if ($action === 'signup') { - $username = trim($body['username'] ?? ''); - $email = trim($body['email'] ?? ''); - $password = $body['password'] ?? ''; - - if ($username === '') { - json_out(['success' => false, 'message' => 'Username is required.']); - } - if (!preg_match('/^[a-zA-Z0-9_\-]{3,32}$/', $username)) { - json_out(['success' => false, 'message' => 'Username must be 3–32 characters: letters, numbers, _ or -.']); - } - if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { - json_out(['success' => false, 'message' => 'A valid email address is required.']); - } - if (strlen($password) < 8) { - json_out(['success' => false, 'message' => 'Password must be at least 8 characters.']); - } - - $pdo = db_connect(); - $stmt = $pdo->prepare('SELECT 1 FROM users WHERE username = :u OR email = :e LIMIT 1'); - $stmt->execute([':u' => $username, ':e' => $email]); - - if ($stmt->fetch()) { - json_out(['success' => false, 'message' => 'Username or email is already taken.']); - } - - $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); - $stmt = $pdo->prepare( - 'INSERT INTO users (username, email, password_hash, created_at) VALUES (:u, :e, :h, NOW())' - ); - $stmt->execute([':u' => $username, ':e' => $email, ':h' => $hash]); - - json_out(['success' => true]); -} - -// ════════════════════════════════════════════════════════════════════════════ -// ACTION: logout -// ════════════════════════════════════════════════════════════════════════════ -if ($action === 'logout') { - $_SESSION = []; - if (ini_get('session.use_cookies')) { - $p = session_get_cookie_params(); - setcookie(session_name(), '', time() - 42000, - $p['path'], $p['domain'], $p['secure'], $p['httponly']); - } - session_destroy(); - json_out(['success' => true, 'redirect' => '/']); -} - -json_out(['success' => false, 'message' => 'Unknown action.'], 400); diff --git a/docs/404.html b/docs/404.html index 1468b90..a5bfc87 100644 --- a/docs/404.html +++ b/docs/404.html @@ -7,4 +7,4 @@
The page you visited was not found.