diff --git a/backend/middleware.py b/backend/middleware.py
index c6f08d189..221c5427e 100644
--- a/backend/middleware.py
+++ b/backend/middleware.py
@@ -12,9 +12,11 @@
DEFAULT_ALLOWED_CONTENT_TYPES,
validate_content_type as _validate_content_type_header,
)
+ from .validators import validate_password_strength
except ImportError:
from error_responses import invalid_json_error
from security_parsers import DEFAULT_ALLOWED_CONTENT_TYPES, validate_content_type as _validate_content_type_header
+ from validators import validate_password_strength
logger = logging.getLogger(__name__)
@@ -177,6 +179,82 @@ def decorated_function(*args, **kwargs):
return decorated_function
return decorator
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# PASSWORD STRENGTH ENFORCEMENT (Issue #790)
+# ─────────────────────────────────────────────────────────────────────────────
+
+def require_strong_password(f):
+ """
+ Decorator that enforces strong password policy on any endpoint that
+ receives a 'password' field in its JSON body (e.g. /api/v1/register).
+
+ This acts as a second layer of defence — RegisterRequest in validators.py
+ already runs the same check via Pydantic. Having it here means the rule
+ is enforced even if the route is called without the Pydantic schema, and
+ it returns a consistent, descriptive 400 response before any business
+ logic runs.
+
+ Usage:
+ @app.route('/api/v1/register', methods=['POST'])
+ @require_strong_password
+ def register():
+ ...
+
+ Response on failure (HTTP 400):
+ {
+ "success": false,
+ "error": "Password does not meet security requirements",
+ "requirements": [
+ "Minimum 8 characters required",
+ "At least one uppercase letter (A–Z) required"
+ ]
+ }
+ """
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ # Only check POST / PUT / PATCH — skip GET, HEAD, DELETE
+ if request.method not in ('POST', 'PUT', 'PATCH'):
+ return f(*args, **kwargs)
+
+ data = request.get_json(silent=True) or {}
+ password = data.get('password', '')
+
+ if not password:
+ logger.warning(
+ f"[require_strong_password] Missing password field "
+ f"on {request.method} {request.path} from {request.remote_addr}"
+ )
+ return jsonify({
+ 'success': False,
+ 'error': 'Password is required',
+ 'requirements': []
+ }), 400
+
+ is_valid, errors = validate_password_strength(password)
+
+ if not is_valid:
+ logger.warning(
+ f"[require_strong_password] Weak password rejected "
+ f"on {request.path} from {request.remote_addr} — "
+ f"failed rules: {errors}"
+ )
+ return jsonify({
+ 'success': False,
+ 'error': 'Password does not meet security requirements',
+ 'requirements': errors
+ }), 400
+
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+# ─────────────────────────────────────────────────────────────────────────────
+# END PASSWORD STRENGTH ENFORCEMENT
+# ─────────────────────────────────────────────────────────────────────────────
+
+
def csrf_token_required(f):
"""
=============================================================================
@@ -230,4 +308,4 @@ def decorated_function(*args, **kwargs):
"message": "An internal security error occurred."
}), 500
- return decorated_function
+ return decorated_function
\ No newline at end of file
diff --git a/backend/validators.py b/backend/validators.py
index 879c00a7a..26aff7a0c 100644
--- a/backend/validators.py
+++ b/backend/validators.py
@@ -28,6 +28,46 @@ def validate_google_books_id(google_id: str) -> bool:
return bool(GOOGLE_BOOKS_ID_PATTERN.fullmatch(str(google_id).strip()))
+# ─────────────────────────────────────────────────────────────────────────────
+# PASSWORD STRENGTH VALIDATION (Issue #790)
+# ─────────────────────────────────────────────────────────────────────────────
+
+# Individual rule regexes — kept separate so RegisterRequest's field_validator
+# can also iterate over them to produce per-rule error messages.
+_PASSWORD_RULES: List[Dict[str, Any]] = [
+ {"regex": re.compile(r'.{8,}'), "msg": "Minimum 8 characters required"},
+ {"regex": re.compile(r'[A-Z]'), "msg": "At least one uppercase letter (A–Z) required"},
+ {"regex": re.compile(r'[a-z]'), "msg": "At least one lowercase letter (a–z) required"},
+ {"regex": re.compile(r'\d'), "msg": "At least one digit (0–9) required"},
+ {"regex": re.compile(r'[@#$!%&*]'), "msg": "At least one special character (@, #, $, !, %, &, *) required"},
+]
+
+
+def validate_password_strength(password: str) -> tuple[bool, list[str]]:
+ """
+ Check password against all strength rules.
+
+ Returns:
+ (is_valid, errors) — errors is an empty list when is_valid is True.
+
+ Usage (standalone):
+ ok, errors = validate_password_strength("Secure@123")
+ """
+ if not isinstance(password, str):
+ return False, ["Password must be a string"]
+
+ errors = [
+ rule["msg"]
+ for rule in _PASSWORD_RULES
+ if not rule["regex"].search(password)
+ ]
+ return len(errors) == 0, errors
+
+# ─────────────────────────────────────────────────────────────────────────────
+# END PASSWORD STRENGTH VALIDATION
+# ─────────────────────────────────────────────────────────────────────────────
+
+
class ShelfType(str, Enum):
"""Valid shelf types for library items."""
WANT = "want"
@@ -203,7 +243,7 @@ class RegisterRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=50, description="Username (3-50 characters)")
email: EmailStr = Field(..., description="Valid email address")
password: str = Field(..., min_length=8, max_length=100, description="Password (minimum 8 characters)")
-
+
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
@@ -213,6 +253,21 @@ def username_alphanumeric(cls, v: str) -> str:
raise ValueError('Username must contain only letters, numbers, and underscores.')
return v
+ @field_validator('password')
+ @classmethod
+ def password_strong_enough(cls, v: str) -> str:
+ """
+ Enforce strong password policy (Issue #790).
+ Runs validate_password_strength() and raises ValueError
+ with all failed rules so the API response is descriptive.
+ """
+ is_valid, errors = validate_password_strength(v)
+ if not is_valid:
+ # Join all failure reasons into one message so Pydantic surfaces them
+ raise ValueError("Password does not meet requirements: " + " | ".join(errors))
+ return v # never sanitize / modify passwords — return as-is
+
+
class LoginRequest(BaseModel):
"""Request schema for POST /api/v1/login endpoint."""
username: str = Field(..., min_length=1, description="Username or email")
diff --git a/frontend/css/style.css b/frontend/css/style.css
index 72625834a..de87ca7ad 100644
--- a/frontend/css/style.css
+++ b/frontend/css/style.css
@@ -7112,6 +7112,198 @@ body.night-mode .tooltip-author {
border-color: var(--text-main);
}
+/* Password Strength Styles */
+.password-strength-bar {
+ height: 5px;
+ background: #e0e0e0;
+ border-radius: 3px;
+ margin: 6px 0 4px;
+ overflow: hidden;
+}
+.strength-fill {
+ height: 100%;
+ width: 0%;
+ border-radius: 3px;
+ transition: width 0.3s ease, background 0.3s ease;
+}
+.strength-label { font-size: 12px; margin: 0 0 8px; min-height: 16px; }
+.pw-checklist { list-style: none; padding: 0; margin: 0 0 12px; font-size: 13px; }
+.pw-checklist li { padding: 2px 0; color: #888; transition: color 0.2s; }
+.pw-checklist li.pass { color: #1D9E75; }
+.pw-checklist li.fail { color: #E24B4A; }
+.pw-checklist li.pass::before { content: '✓ '; }
+.pw-checklist li.fail::before { content: '✗ '; }
+
+/* Purani CSS ke sath yeh hissa add/update karein */
+
+.avatar-icon {
+ font-size: 18px;
+ color: #555;
+ background: #e0e0e0;
+ padding: 8px;
+ border-radius: 50%;
+ width: 35px;
+ height: 35px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.like-btn {
+ background: none;
+ border: 1px solid #ff4d4d;
+ color: #ff4d4d;
+ padding: 6px 12px;
+ border-radius: 20px;
+ cursor: pointer;
+ margin-bottom: 15px;
+ font-weight: bold;
+ transition: 0.2s;
+}
+
+/* Hover karne par button thoda bharega */
+.like-btn:hover {
+ background: #ff4d4d;
+ color: white;
+}
+
+.comment {
+ font-size: 14px;
+ margin-bottom: 8px;
+ color: #333;
+}
+
+.comment i {
+ color: #007bff; /* Comment icon ka color blue */
+ margin-right: 5px;
+}
+
+/* ==========================================================================
+ Book Club Discussion Forum Styles
+ ========================================================================== */
+
+.discussion-container {
+ max-width: 800px;
+ margin: 40px auto;
+}
+
+.create-post-section {
+ background: var(--card-bg, rgba(255, 255, 255, 0.03));
+ border: 1px solid var(--border-color, rgba(255, 255, 255, 0.05));
+ padding: 25px;
+ border-radius: 12px;
+ margin-bottom: 40px;
+ backdrop-filter: blur(10px);
+}
+
+.styled-input {
+ width: 100%;
+ padding: 12px 15px;
+ margin-bottom: 15px;
+ background: rgba(0, 0, 0, 0.2);
+ border: 1px solid var(--control-border, rgba(255, 255, 255, 0.1));
+ border-radius: 8px;
+ color: var(--text-main, #fff);
+ font-family: inherit;
+ transition: 0.3s;
+}
+
+.styled-input:focus {
+ outline: none;
+ border-color: var(--accent-gold, #d4af37);
+ background: rgba(0, 0, 0, 0.3);
+}
+
+textarea.styled-input {
+ min-height: 100px;
+ resize: vertical;
+}
+
+/* Post Cards */
+.post-card {
+ background: var(--card-bg, rgba(255, 255, 255, 0.02));
+ border: 1px solid var(--border-color, rgba(255, 255, 255, 0.05));
+ padding: 20px;
+ border-radius: 10px;
+ margin-bottom: 25px;
+ transition: transform 0.2s ease, border-color 0.2s ease;
+}
+
+.post-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--accent-gold, rgba(212, 175, 55, 0.5));
+}
+
+.post-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+ color: var(--text-muted, #a0a0a0);
+}
+
+.avatar-icon {
+ width: 35px;
+ height: 35px;
+ background: var(--wood-dark, #3E2723);
+ color: var(--accent-gold, #d4af37);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ border: 1px solid rgba(255,255,255,0.1);
+}
+
+.post-title {
+ font-family: 'Playfair Display', serif;
+ font-size: 1.4rem;
+ margin-bottom: 10px;
+ color: var(--text-main, #fff);
+}
+
+.post-text {
+ line-height: 1.6;
+ color: var(--text-muted, #ccc);
+ margin-bottom: 20px;
+}
+
+/* Comments Section */
+.comments-section {
+ background: rgba(0, 0, 0, 0.15);
+ padding: 15px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.02);
+}
+
+.comments-section h4 {
+ color: var(--text-main, #fff);
+ margin-bottom: 10px;
+ font-size: 0.95rem;
+}
+
+.comment {
+ font-size: 0.9rem;
+ color: var(--text-muted, #bbb);
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.comment:last-child {
+ border-bottom: none;
+}
+
+.comment i {
+ color: var(--accent-gold, #d4af37);
+ margin-right: 8px;
+}
+
+.add-comment {
+ display: flex;
+ gap: 10px;
+ margin-top: 15px;
+}
/* ── No-results empty state ── */
#no-results-state {
justify-content: center;
diff --git a/frontend/js/app.js b/frontend/js/app.js
index bc22b829b..31fd0fdef 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -720,7 +720,10 @@ class BookRenderer {
-
+
+
+
+
@@ -949,6 +952,10 @@ class BookRenderer {
const addBtn = document.getElementById('modal-add-btn');
const shareBtn = document.getElementById('modal-share-btn');
+ const discussBtn = document.getElementById('modal-discuss-btn');
+if (discussBtn) {
+ discussBtn.href = `discuss.html?title=${encodeURIComponent(book.volumeInfo.title)}`;
+}
const isInLibrary = this.libraryManager && typeof this.libraryManager.findBook === 'function' && this.libraryManager.findBook(book.id);
if (addBtn) {
diff --git a/frontend/js/authValidator.js b/frontend/js/authValidator.js
new file mode 100644
index 000000000..465a0b2a7
--- /dev/null
+++ b/frontend/js/authValidator.js
@@ -0,0 +1,162 @@
+/**
+ * authValidator.js — Issue #790
+ * Wires up real-time password strength UI for the BiblioDrift auth page.
+ * Depends on passwordValidator.js (imported first in auth.html).
+ */
+
+import { validatePassword, PASSWORD_RULES } from './passwordValidator.js';
+
+// ─── Element references ───────────────────────────────────────────────────────
+const passwordInput = document.getElementById('password');
+const strengthFill = document.getElementById('strength-fill');
+const strengthLabel = document.getElementById('strength-label');
+const submitBtn = document.getElementById('submitBtn');
+const authForm = document.getElementById('authForm');
+
+// Map rule id (from passwordValidator.js) → id in auth.html
+const RULE_TO_LI = {
+ 'len': 'chk-len',
+ 'up': 'chk-up',
+ 'low': 'chk-low',
+ 'num': 'chk-num',
+ 'spec': 'chk-spec',
+};
+
+const STRENGTH_COLORS = {
+ weak: '#E24B4A',
+ fair: '#EF9F27',
+ good: '#EF9F27',
+ strong: '#1D9E75',
+};
+
+// ─── Show / hide checklist based on form mode ─────────────────────────────────
+function isRegisterMode() {
+ return authForm && authForm.dataset.mode === 'register';
+}
+
+function setChecklistVisible(visible) {
+ const bar = document.querySelector('.password-strength-bar');
+ const lbl = strengthLabel;
+ const ul = document.getElementById('pw-checklist');
+ if (bar) bar.style.display = visible ? '' : 'none';
+ if (lbl) lbl.style.display = visible ? '' : 'none';
+ if (ul) ul.style.display = visible ? '' : 'none';
+}
+
+// ─── Core update function ─────────────────────────────────────────────────────
+function updateStrengthUI() {
+ const val = passwordInput ? passwordInput.value : '';
+
+ // Only show strength UI when in register mode
+ if (!isRegisterMode()) {
+ setChecklistVisible(false);
+ return;
+ }
+
+ setChecklistVisible(true);
+
+ if (!val) {
+ // Reset everything when field is empty
+ if (strengthFill) { strengthFill.style.width = '0%'; strengthFill.style.background = ''; }
+ if (strengthLabel) strengthLabel.textContent = '';
+ Object.values(RULE_TO_LI).forEach(liId => {
+ const li = document.getElementById(liId);
+ if (li) { li.classList.remove('pass', 'fail'); }
+ });
+ if (submitBtn) { submitBtn.disabled = true; submitBtn.style.opacity = '0.45'; submitBtn.style.cursor = 'not-allowed'; }
+ return;
+ }
+
+ const { isValid, results, strength } = validatePassword(val);
+
+ // Update strength bar
+ const pct = (results.filter(r => r.passed).length / PASSWORD_RULES.length) * 100;
+ if (strengthFill) {
+ strengthFill.style.width = `${pct}%`;
+ strengthFill.style.background = STRENGTH_COLORS[strength] || '#E24B4A';
+ }
+ if (strengthLabel) {
+ strengthLabel.textContent = strength
+ ? strength.charAt(0).toUpperCase() + strength.slice(1)
+ : '';
+ strengthLabel.style.color = STRENGTH_COLORS[strength] || '#888';
+ }
+
+ // Update checklist items
+ results.forEach(({ id, passed }) => {
+ const liId = RULE_TO_LI[id];
+ if (!liId) return;
+ const li = document.getElementById(liId);
+ if (!li) return;
+ li.classList.toggle('pass', passed);
+ li.classList.toggle('fail', !passed);
+ });
+
+ // Enable / disable submit button
+ if (submitBtn) {
+ submitBtn.disabled = !isValid;
+ submitBtn.style.opacity = isValid ? '1' : '0.45';
+ submitBtn.style.cursor = isValid ? 'pointer' : 'not-allowed';
+ }
+}
+
+// ─── Listen to password input ─────────────────────────────────────────────────
+if (passwordInput) {
+ passwordInput.addEventListener('input', updateStrengthUI);
+}
+
+// ─── Reset UI when toggling between login ↔ register ────────────────────────
+// auth.html's inline script toggles authForm.dataset.mode — we observe that.
+if (authForm) {
+ const observer = new MutationObserver(() => {
+ updateStrengthUI();
+ // In login mode, always re-enable submit (no strength requirement)
+ if (!isRegisterMode() && submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.style.opacity = '1';
+ submitBtn.style.cursor = 'pointer';
+ }
+ });
+ observer.observe(authForm, { attributes: true, attributeFilter: ['data-mode'] });
+}
+
+// ─── Block form submission if password is weak (register mode) ───────────────
+if (authForm) {
+ authForm.addEventListener('submit', (e) => {
+ if (!isRegisterMode()) return; // login — no strength check needed
+
+ const val = passwordInput ? passwordInput.value : '';
+ const { isValid, errors } = validatePassword(val);
+
+ if (!isValid) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ // Shake the checklist to draw attention
+ const ul = document.getElementById('pw-checklist');
+ if (ul) {
+ ul.style.animation = 'none';
+ ul.offsetHeight; // reflow
+ ul.style.animation = 'shake 0.4s ease';
+ }
+
+ // Force-update UI so user sees which rules failed
+ updateStrengthUI();
+ }
+ }, true); // capture phase — runs before handleAuth()
+}
+
+// ─── Simple shake keyframe (injected once) ───────────────────────────────────
+const style = document.createElement('style');
+style.textContent = `
+@keyframes shake {
+ 0%,100% { transform: translateX(0); }
+ 20% { transform: translateX(-6px); }
+ 40% { transform: translateX(6px); }
+ 60% { transform: translateX(-4px); }
+ 80% { transform: translateX(4px); }
+}`;
+document.head.appendChild(style);
+
+// ─── Init: hide checklist on page load (starts in login mode) ────────────────
+setChecklistVisible(false);
\ No newline at end of file
diff --git a/frontend/js/discuss.js b/frontend/js/discuss.js
new file mode 100644
index 000000000..9ca3c26d3
--- /dev/null
+++ b/frontend/js/discuss.js
@@ -0,0 +1,89 @@
+// URL se book ka title nikal kar input field mein auto-fill karna
+const urlParams = new URLSearchParams(window.location.search);
+const bookTitleParam = urlParams.get('title');
+if (bookTitleParam) {
+ document.getElementById('post-title').value = bookTitleParam;
+}
+let posts = JSON.parse(localStorage.getItem('biblioDrift_posts')) || [];
+
+const postForm = document.getElementById('post-form');
+const postsContainer = document.getElementById('posts-container');
+
+function savePosts() {
+ localStorage.setItem('biblioDrift_posts', JSON.stringify(posts));
+}
+
+function renderPosts() {
+ postsContainer.innerHTML = '';
+
+ posts.forEach((post, postIndex) => {
+ const postCard = document.createElement('div');
+ postCard.classList.add('post-card');
+
+ postCard.innerHTML = `
+
+ ${post.title}
+ ${post.content}
+
+
+
+
+
+
+ `;
+ postsContainer.appendChild(postCard);
+ });
+}
+
+postForm.addEventListener('submit', function(e) {
+ e.preventDefault();
+
+ const title = document.getElementById('post-title').value;
+ const content = document.getElementById('post-content').value;
+
+ const newPost = {
+ username: "GuestReader",
+ title: title,
+ content: content,
+ likes: 0,
+ comments: []
+ };
+
+ posts.unshift(newPost);
+ savePosts();
+ renderPosts();
+ postForm.reset();
+});
+
+window.likePost = function(index) {
+ posts[index].likes += 1;
+ savePosts();
+ renderPosts();
+}
+
+window.addComment = function(index) {
+ const commentInput = document.getElementById(`comment-input-${index}`);
+ const commentText = commentInput.value.trim();
+
+ if(commentText !== "") {
+ posts[index].comments.push(commentText);
+ savePosts();
+ renderPosts();
+ }
+}
+
+renderPosts();
\ No newline at end of file
diff --git a/frontend/js/passwordValidator.js b/frontend/js/passwordValidator.js
new file mode 100644
index 000000000..fc40be631
--- /dev/null
+++ b/frontend/js/passwordValidator.js
@@ -0,0 +1,19 @@
+// frontend/js/passwordValidator.js
+
+export const PASSWORD_RULES = [
+ { id: 'len', regex: /.{8,}/, msg: 'At least 8 characters' },
+ { id: 'up', regex: /[A-Z]/, msg: 'One uppercase letter (A–Z)' },
+ { id: 'low', regex: /[a-z]/, msg: 'One lowercase letter (a–z)' },
+ { id: 'num', regex: /[0-9]/, msg: 'One digit (0–9)' },
+ { id: 'spec', regex: /[@#$!%&*]/, msg: 'One special character (@#$!%&*)' },
+];
+
+export function validatePassword(password) {
+ const results = PASSWORD_RULES.map(rule => ({
+ ...rule,
+ passed: rule.regex.test(password),
+ }));
+ const passedCount = results.filter(r => r.passed).length;
+ const strength = ['', 'weak', 'weak', 'fair', 'good', 'strong'][passedCount];
+ return { isValid: passedCount === 5, results, strength };
+}
\ No newline at end of file
diff --git a/frontend/js/security.js b/frontend/js/security.js
index 9c9691e73..e6508ee94 100644
--- a/frontend/js/security.js
+++ b/frontend/js/security.js
@@ -19,6 +19,134 @@ const SAFE_HREF_PROTOCOLS = ['http:', 'https:', 'mailto:'];
const SAFE_HREF_PATTERN = /^(?:(?:https?:|mailto:)[^\s]*|#.*|\/(?!\/)[^\s]*|[^:/\s][^:\s]*)$/i;
let domPurifyHooksRegistered = false;
+// ─────────────────────────────────────────────────────────────────────────────
+// PASSWORD VALIDATION (Issue #790)
+// ─────────────────────────────────────────────────────────────────────────────
+
+/**
+ * Rules that every new password must satisfy.
+ * Each rule has a unique id (used to target the checklist ),
+ * a regex to test against, and the human-readable message shown to the user.
+ */
+const PASSWORD_RULES = [
+ { id: 'pw-len', regex: /.{8,}/, msg: 'At least 8 characters' },
+ { id: 'pw-up', regex: /[A-Z]/, msg: 'One uppercase letter (A–Z)' },
+ { id: 'pw-low', regex: /[a-z]/, msg: 'One lowercase letter (a–z)' },
+ { id: 'pw-num', regex: /[0-9]/, msg: 'One digit (0–9)' },
+ { id: 'pw-spec', regex: /[@#$!%&*]/, msg: 'One special character (@, #, $, !, %, &, *)' },
+];
+
+/**
+ * Validate password strength against PASSWORD_RULES.
+ *
+ * @param {string} password - The password to test
+ * @returns {{ isValid: boolean, errors: string[], strength: string, results: Array }}
+ * - isValid : true only when ALL rules pass
+ * - errors : array of failure messages (empty when valid)
+ * - strength : 'weak' | 'fair' | 'good' | 'strong'
+ * - results : per-rule objects { id, msg, passed } for building UI checklists
+ */
+function validatePassword(password) {
+ if (typeof password !== 'string') {
+ return { isValid: false, errors: ['Password must be a string'], strength: 'weak', results: [] };
+ }
+
+ const results = PASSWORD_RULES.map(rule => ({
+ id: rule.id,
+ msg: rule.msg,
+ passed: rule.regex.test(password),
+ }));
+
+ const passedCount = results.filter(r => r.passed).length;
+ const errors = results.filter(r => !r.passed).map(r => r.msg);
+
+ // Map 1–5 passed rules to a label; 0 rules = no label yet
+ const strengthMap = { 1: 'weak', 2: 'weak', 3: 'fair', 4: 'good', 5: 'strong' };
+ const strength = passedCount === 0 ? '' : (strengthMap[passedCount] || 'weak');
+
+ return {
+ isValid: passedCount === PASSWORD_RULES.length,
+ errors,
+ strength,
+ results,
+ };
+}
+
+/**
+ * Wire up real-time password strength UI.
+ *
+ * Expects this HTML structure inside your registration form:
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * - At least 8 characters
+ * - One uppercase letter (A–Z)
+ * - One lowercase letter (a–z)
+ * - One digit (0–9)
+ * - One special character (@, #, $, !, %, &, *)
+ *
+ *
+ * @param {string} inputId - id of the element
+ * @param {string} fillId - id of the strength bar fill
+ * @param {string} labelId - id of the strength label
+ * @param {string} submitId - id of the submit/register
+
+
+
+
+
+ - At least 8 characters
+ - One uppercase letter
+ - One lowercase letter
+ - One digit (0–9)
+ - One special character
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Start a Discussion
+
+
+
+
+
+
+
+
+
+
+
+
+
Comments
+${comment}
`).join('')} +