Implements a fully self-built comment system for the blog: Data layer: - comments table with BIGSERIAL PK, parent_id self-reference (ON DELETE SET NULL), depth tracking (max 20), status workflow (pending/approved/spam/trash), content hashing for dedup, GDPR consent tracking, IP/UA storage with auto-purge - 5 partial indexes optimized for read patterns - updated_at auto-trigger API (9 Dioxus server functions): - Public: get_comments, get_comment_count, create_comment - Admin: get_pending_comments, get_pending_count, get_all_comments, approve_comment (with ancestor auto-approval), spam_comment, trash_comment, batch_update_comment_status Security: - Function-level rate limiting (1/sec, burst 5) via FullstackContext IP extraction - Input validation (name, email, URL scheme, content length, consent) - Parent chain validation (must be approved, same post) - Strict comment Markdown renderer (headings→strong, no img/id/data URIs, nofollow links) - Honeypot anti-spam field - 5-minute dedup window via SHA-256 content hash Frontend: - CommentSection with SuspenseBoundary isolation - Flat-list rendering with depth-based CSS indentation (responsive) - Gravatar via cravatar.cn (server-computed, email never exposed) - Inline reply forms (one-at-a-time via Signal) - Admin action buttons (approve/spam/delete) visible per-comment - CommentForm with privacy consent, Markdown hint, loading states Admin: - /admin/comments page with status tabs, batch operations, pagination - Pending count badge on admin dashboard Infrastructure: - Shared get_current_admin_user moved from posts/helpers to auth module - COMMENT_LIMITER rate limiter tier - Moka caches (60s TTL for comments, 10s for pending count) - IP/UA purge background task (daily, 90-day retention)
59 lines
2.0 KiB
PL/PgSQL
59 lines
2.0 KiB
PL/PgSQL
CREATE TABLE IF NOT EXISTS comments (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
post_id INT NOT NULL REFERENCES posts(id) ON DELETE RESTRICT,
|
|
parent_id BIGINT REFERENCES comments(id) ON DELETE SET NULL,
|
|
depth INT NOT NULL DEFAULT 0,
|
|
author_name VARCHAR(50) NOT NULL,
|
|
author_email VARCHAR(255) NOT NULL,
|
|
author_url VARCHAR(500),
|
|
content_md TEXT NOT NULL,
|
|
content_html TEXT,
|
|
content_hash VARCHAR(64),
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
ip_address VARCHAR(45),
|
|
user_agent VARCHAR(500),
|
|
consented_at TIMESTAMPTZ,
|
|
approved_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
CONSTRAINT comments_status_check
|
|
CHECK (status IN ('pending', 'approved', 'spam', 'trash')),
|
|
CONSTRAINT comments_depth_check
|
|
CHECK (depth >= 0 AND depth <= 20),
|
|
CONSTRAINT comments_content_not_empty
|
|
CHECK (length(trim(content_md)) >= 1),
|
|
CONSTRAINT comments_name_not_empty
|
|
CHECK (length(trim(author_name)) >= 1)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_comments_post_approved
|
|
ON comments(post_id, created_at) WHERE status = 'approved' AND deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_comments_top_level
|
|
ON comments(post_id, created_at)
|
|
WHERE parent_id IS NULL AND status = 'approved' AND deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_comments_pending
|
|
ON comments(created_at DESC) WHERE status = 'pending' AND deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_comments_admin_list
|
|
ON comments(status, created_at DESC) WHERE deleted_at IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_comments_parent
|
|
ON comments(parent_id) WHERE parent_id IS NOT NULL;
|
|
|
|
CREATE OR REPLACE FUNCTION update_comments_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_comments_updated_at
|
|
BEFORE UPDATE ON comments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_comments_updated_at();
|