From 677b13c298d2cf07bf3df381f006dbfcb9e2d7cd Mon Sep 17 00:00:00 2001 From: hyzen Date: Thu, 4 Jun 2026 12:05:17 +0530 Subject: [PATCH] Add: login & sign up pages --- SERVER_SETUP.md | 190 +++++++++++++++++++++++++++++++++ api/auth.php | 195 ++++++++++++++++++++++++++++++++++ content/login/index.md | 2 +- content/signup/index.md | 2 +- docs/CNAME | 1 - docs/css/auth-additions.css | 195 ++++++++++++++++++++++++++++++++++ docs/index.html | 2 +- docs/index.xml | 4 +- docs/login/index.html | 10 +- docs/signup/index.html | 12 ++- docs/sitemap.xml | 2 +- layouts/login/single.html | 120 ++++++++++++++++++++- layouts/signup/single.html | 168 ++++++++++++++++++++++++++++- static/css/auth-additions.css | 195 ++++++++++++++++++++++++++++++++++ 14 files changed, 1083 insertions(+), 15 deletions(-) create mode 100644 SERVER_SETUP.md create mode 100644 api/auth.php delete mode 100644 docs/CNAME create mode 100644 docs/css/auth-additions.css create mode 100644 static/css/auth-additions.css diff --git a/SERVER_SETUP.md b/SERVER_SETUP.md new file mode 100644 index 0000000..67f49da --- /dev/null +++ b/SERVER_SETUP.md @@ -0,0 +1,190 @@ +# 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 new file mode 100644 index 0000000..3605652 --- /dev/null +++ b/api/auth.php @@ -0,0 +1,195 @@ + 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/content/login/index.md b/content/login/index.md index 2b99f9e..6c65619 100644 --- a/content/login/index.md +++ b/content/login/index.md @@ -1,5 +1,5 @@ --- -title: "Coming soon!" +title: "Login" description: "Login to Freedoms4." --- diff --git a/content/signup/index.md b/content/signup/index.md index c7bba40..b9e173e 100644 --- a/content/signup/index.md +++ b/content/signup/index.md @@ -1,5 +1,5 @@ --- -title: "Coming soon!" +title: "Sign Up" description: "Sign up for Freedoms4." --- diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index cf895be..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -freedoms4.org diff --git a/docs/css/auth-additions.css b/docs/css/auth-additions.css new file mode 100644 index 0000000..21d726d --- /dev/null +++ b/docs/css/auth-additions.css @@ -0,0 +1,195 @@ + +/* ── Auth pages (login / signup) ───────────────────────────────────────── */ + +.auth-page { + max-width: 420px; + margin: 0 auto; + padding: 1rem 0 3rem; +} + +.auth-page__title { + margin-bottom: 1.5rem; +} + +.auth-card { + border: 1px solid var(--background-color1, #ccc); + border-radius: 10px; + padding: 2rem 1.75rem; + background: var(--background-color); +} + +/* Message banner */ +.auth-message { + border-radius: 6px; + padding: 0.65rem 0.9rem; + font-size: 0.88rem; + margin-bottom: 1.2rem; + line-height: 1.4; +} + +.auth-message--error { + background: rgba(220, 53, 69, 0.12); + border: 1px solid rgba(220, 53, 69, 0.4); + color: #c0392b; +} + +[data-theme='dark'] .auth-message--error { + color: #ff6b6b; + background: rgba(220, 53, 69, 0.18); + border-color: rgba(220, 53, 69, 0.5); +} + +.auth-message--success { + background: rgba(39, 174, 96, 0.12); + border: 1px solid rgba(39, 174, 96, 0.4); + color: #1e8449; +} + +[data-theme='dark'] .auth-message--success { + color: #58d68d; + background: rgba(39, 174, 96, 0.18); + border-color: rgba(39, 174, 96, 0.5); +} + +/* Form fields */ +.auth-form { + display: flex; + flex-direction: column; + gap: 1.1rem; +} + +.auth-form__group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.auth-form__label { + font-size: 0.85rem; + font-weight: 600; + color: var(--foreground-color); +} + +.auth-form__input-wrap { + position: relative; + display: flex; + align-items: center; +} + +.auth-form__input-wrap .auth-form__input { + padding-right: 2.5rem; +} + +.auth-form__input { + width: 100%; + box-sizing: border-box; + padding: 0.5rem 0.75rem; + border: 1px solid var(--background-color1, #ccc); + border-radius: 5px; + font-size: 0.9rem; + background: var(--background-color); + color: var(--foreground-color); + transition: border-color 0.18s ease, box-shadow 0.18s ease; + outline: none; + font-family: inherit; +} + +.auth-form__input:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb, 17, 51, 233), 0.15); +} + +.auth-form__input::placeholder { + color: var(--foreground-color3, #888); + opacity: 0.7; +} + +.auth-form__eye { + position: absolute; + right: 0.6rem; + background: none; + border: none; + cursor: pointer; + color: var(--foreground-color3, #888); + display: inline-flex; + align-items: center; + padding: 0.2rem; + line-height: 1; + transition: color 0.15s ease; +} + +.auth-form__eye:hover { + color: var(--foreground-color); +} + +.auth-form__hint { + font-size: 0.75rem; + color: var(--foreground-color3, #888); +} + +/* Submit button */ +.auth-form__submit { + margin-top: 0.5rem; + width: 100%; + padding: 0.6rem 1rem; + border: none; + border-radius: 5px; + background: var(--accent-color); + color: var(--background-color, #fff); + font-size: 0.92rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + transition: opacity 0.18s ease; +} + +.auth-form__submit:hover:not(:disabled) { + opacity: 0.85; +} + +.auth-form__submit:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.auth-form__submit-loader { + display: inline-flex; + align-items: center; +} + +/* Spinner animation */ +@keyframes auth-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.spin { + animation: auth-spin 0.8s linear infinite; +} + +/* Footer link */ +.auth-card__footer { + margin-top: 1.4rem; + text-align: center; + font-size: 0.85rem; + color: var(--foreground-color3, #888); +} + +.auth-card__link { + color: var(--accent-color); + text-decoration: none; + font-weight: 600; +} + +.auth-card__link:hover { + text-decoration: underline; +} + +/* Light theme override for submit button text */ +[data-theme='light'] .auth-form__submit { + color: #fff; +} diff --git a/docs/index.html b/docs/index.html index c950828..f7099fa 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,4 +1,4 @@ -Freedoms4
Freedoms4 logo

Freedoms4

Login

+
+
\ No newline at end of file diff --git a/docs/signup/index.html b/docs/signup/index.html index bd77c31..bc74a85 100644 --- a/docs/signup/index.html +++ b/docs/signup/index.html @@ -1,4 +1,4 @@ -Coming soon! | Freedoms4
Freedoms4 logo

Freedoms4

Coming soon!

The feature is not available yet.

\ No newline at end of file +Subscribe

Sign Up

+ +3–32 characters; letters, numbers, _ and - only.
+
+
At least 8 characters.
\ No newline at end of file diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 58b62f0..3231a12 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -1 +1 @@ -https://freedoms4.org/tags/academics/2026-03-01T18:11:14+00:00https://freedoms4.org/blog/2026-03-01T18:11:14+00:00https://freedoms4.org/categories/2026-03-01T18:11:14+00:00https://freedoms4.org/tags/education/2026-03-01T18:11:14+00:00https://freedoms4.org/2026-03-01T18:11:14+00:00https://freedoms4.org/categories/philosophy/2026-03-01T18:11:14+00:00https://freedoms4.org/tags/system/2026-03-01T18:11:14+00:00https://freedoms4.org/tags/2026-03-01T18:11:14+00:00https://freedoms4.org/blog/what-is-education/2026-03-01T18:11:14+00:00https://freedoms4.org/uninotes/2025-11-11T23:15:44+05:30https://freedoms4.org/uninotes/s1/bo-dcm1109/https://freedoms4.org/changelog/https://freedoms4.org/coming-soon/https://freedoms4.org/login/https://freedoms4.org/signup/https://freedoms4.org/contact/https://freedoms4.org/uninotes/s1/et-dcm1107/https://freedoms4.org/uninotes/s1/fa-dcm1108/https://freedoms4.org/services/file-share/https://freedoms4.org/uninotes/s1/ge-dcm1106/https://freedoms4.org/uninotes/s1/pbm-dcm1110/https://freedoms4.org/uninotes/s1/https://freedoms4.org/uninotes/s2/https://freedoms4.org/uninotes/s3/https://freedoms4.org/uninotes/s4/https://freedoms4.org/uninotes/s5/https://freedoms4.org/uninotes/s6/https://freedoms4.org/semester/https://freedoms4.org/services/https://freedoms4.org/subjectcode/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit1/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit1/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit1/https://freedoms4.org/uninotes/s1/et-dcm1107/unit1/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/https://freedoms4.org/uninotes/s1/ge-dcm1106/unit1/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/self/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit11/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit11/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit12/https://freedoms4.org/uninotes/s1/et-dcm1107/unit12/self/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit2/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit2/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit2/https://freedoms4.org/uninotes/s1/et-dcm1107/unit2/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit2/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit2/self/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit3/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit3/self/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit3/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit3/self/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit4/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit4/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit5/https://freedoms4.org/uninotes/s1/et-dcm1107/unit5/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit6/https://freedoms4.org/uninotes/s1/et-dcm1107/unit6/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit7/https://freedoms4.org/uninotes/s1/et-dcm1107/unit7/self/https://freedoms4.org/services/xmpp-account/ \ No newline at end of file +https://freedoms4.org/tags/academics/2026-03-01T18:11:14+00:00https://freedoms4.org/blog/2026-03-01T18:11:14+00:00https://freedoms4.org/categories/2026-03-01T18:11:14+00:00https://freedoms4.org/tags/education/2026-03-01T18:11:14+00:00https://freedoms4.org/2026-03-01T18:11:14+00:00https://freedoms4.org/categories/philosophy/2026-03-01T18:11:14+00:00https://freedoms4.org/tags/system/2026-03-01T18:11:14+00:00https://freedoms4.org/tags/2026-03-01T18:11:14+00:00https://freedoms4.org/blog/what-is-education/2026-03-01T18:11:14+00:00https://freedoms4.org/uninotes/2025-11-11T23:15:44+05:30https://freedoms4.org/uninotes/s1/bo-dcm1109/https://freedoms4.org/changelog/https://freedoms4.org/coming-soon/https://freedoms4.org/contact/https://freedoms4.org/uninotes/s1/et-dcm1107/https://freedoms4.org/uninotes/s1/fa-dcm1108/https://freedoms4.org/services/file-share/https://freedoms4.org/uninotes/s1/ge-dcm1106/https://freedoms4.org/login/https://freedoms4.org/uninotes/s1/pbm-dcm1110/https://freedoms4.org/uninotes/s1/https://freedoms4.org/uninotes/s2/https://freedoms4.org/uninotes/s3/https://freedoms4.org/uninotes/s4/https://freedoms4.org/uninotes/s5/https://freedoms4.org/uninotes/s6/https://freedoms4.org/semester/https://freedoms4.org/services/https://freedoms4.org/signup/https://freedoms4.org/subjectcode/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit1/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit1/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit1/https://freedoms4.org/uninotes/s1/et-dcm1107/unit1/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/https://freedoms4.org/uninotes/s1/ge-dcm1106/unit1/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit1/self/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit11/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit11/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit12/https://freedoms4.org/uninotes/s1/et-dcm1107/unit12/self/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit2/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit2/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit2/https://freedoms4.org/uninotes/s1/et-dcm1107/unit2/live/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit2/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit2/self/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit3/https://freedoms4.org/uninotes/s1/bo-dcm1109/unit3/self/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit3/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit3/self/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit4/https://freedoms4.org/uninotes/s1/pbm-dcm1110/unit4/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit5/https://freedoms4.org/uninotes/s1/et-dcm1107/unit5/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit6/https://freedoms4.org/uninotes/s1/et-dcm1107/unit6/self/https://freedoms4.org/uninotes/s1/et-dcm1107/unit7/https://freedoms4.org/uninotes/s1/et-dcm1107/unit7/self/https://freedoms4.org/services/xmpp-account/ \ No newline at end of file diff --git a/layouts/login/single.html b/layouts/login/single.html index 17b274b..9d72f3a 100644 --- a/layouts/login/single.html +++ b/layouts/login/single.html @@ -1,4 +1,120 @@ {{ define "main" }} -

{{ .Title }}

-{{ .Content }} +
+

{{ .Title }}

+ +
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ + +
+ + +
+
+ + {{ end }} diff --git a/layouts/signup/single.html b/layouts/signup/single.html index 17b274b..f83d821 100644 --- a/layouts/signup/single.html +++ b/layouts/signup/single.html @@ -1,4 +1,168 @@ {{ define "main" }} -

{{ .Title }}

-{{ .Content }} +
+

{{ .Title }}

+ +
+ + +
+
+ + + 3–32 characters; letters, numbers, _ and - only. +
+ +
+ + +
+ +
+ +
+ + +
+ At least 8 characters. +
+ +
+ +
+ +
+
+ + +
+ + +
+
+ + {{ end }} diff --git a/static/css/auth-additions.css b/static/css/auth-additions.css new file mode 100644 index 0000000..21d726d --- /dev/null +++ b/static/css/auth-additions.css @@ -0,0 +1,195 @@ + +/* ── Auth pages (login / signup) ───────────────────────────────────────── */ + +.auth-page { + max-width: 420px; + margin: 0 auto; + padding: 1rem 0 3rem; +} + +.auth-page__title { + margin-bottom: 1.5rem; +} + +.auth-card { + border: 1px solid var(--background-color1, #ccc); + border-radius: 10px; + padding: 2rem 1.75rem; + background: var(--background-color); +} + +/* Message banner */ +.auth-message { + border-radius: 6px; + padding: 0.65rem 0.9rem; + font-size: 0.88rem; + margin-bottom: 1.2rem; + line-height: 1.4; +} + +.auth-message--error { + background: rgba(220, 53, 69, 0.12); + border: 1px solid rgba(220, 53, 69, 0.4); + color: #c0392b; +} + +[data-theme='dark'] .auth-message--error { + color: #ff6b6b; + background: rgba(220, 53, 69, 0.18); + border-color: rgba(220, 53, 69, 0.5); +} + +.auth-message--success { + background: rgba(39, 174, 96, 0.12); + border: 1px solid rgba(39, 174, 96, 0.4); + color: #1e8449; +} + +[data-theme='dark'] .auth-message--success { + color: #58d68d; + background: rgba(39, 174, 96, 0.18); + border-color: rgba(39, 174, 96, 0.5); +} + +/* Form fields */ +.auth-form { + display: flex; + flex-direction: column; + gap: 1.1rem; +} + +.auth-form__group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.auth-form__label { + font-size: 0.85rem; + font-weight: 600; + color: var(--foreground-color); +} + +.auth-form__input-wrap { + position: relative; + display: flex; + align-items: center; +} + +.auth-form__input-wrap .auth-form__input { + padding-right: 2.5rem; +} + +.auth-form__input { + width: 100%; + box-sizing: border-box; + padding: 0.5rem 0.75rem; + border: 1px solid var(--background-color1, #ccc); + border-radius: 5px; + font-size: 0.9rem; + background: var(--background-color); + color: var(--foreground-color); + transition: border-color 0.18s ease, box-shadow 0.18s ease; + outline: none; + font-family: inherit; +} + +.auth-form__input:focus { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb, 17, 51, 233), 0.15); +} + +.auth-form__input::placeholder { + color: var(--foreground-color3, #888); + opacity: 0.7; +} + +.auth-form__eye { + position: absolute; + right: 0.6rem; + background: none; + border: none; + cursor: pointer; + color: var(--foreground-color3, #888); + display: inline-flex; + align-items: center; + padding: 0.2rem; + line-height: 1; + transition: color 0.15s ease; +} + +.auth-form__eye:hover { + color: var(--foreground-color); +} + +.auth-form__hint { + font-size: 0.75rem; + color: var(--foreground-color3, #888); +} + +/* Submit button */ +.auth-form__submit { + margin-top: 0.5rem; + width: 100%; + padding: 0.6rem 1rem; + border: none; + border-radius: 5px; + background: var(--accent-color); + color: var(--background-color, #fff); + font-size: 0.92rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + transition: opacity 0.18s ease; +} + +.auth-form__submit:hover:not(:disabled) { + opacity: 0.85; +} + +.auth-form__submit:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.auth-form__submit-loader { + display: inline-flex; + align-items: center; +} + +/* Spinner animation */ +@keyframes auth-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.spin { + animation: auth-spin 0.8s linear infinite; +} + +/* Footer link */ +.auth-card__footer { + margin-top: 1.4rem; + text-align: center; + font-size: 0.85rem; + color: var(--foreground-color3, #888); +} + +.auth-card__link { + color: var(--accent-color); + text-decoration: none; + font-weight: 600; +} + +.auth-card__link:hover { + text-decoration: underline; +} + +/* Light theme override for submit button text */ +[data-theme='light'] .auth-form__submit { + color: #fff; +}