Tighten auth defaults and register form behavior

This commit is contained in:
Jage9
2026-02-24 23:12:01 -05:00
parent 4fbae1dcc2
commit 853bca824a
5 changed files with 52 additions and 28 deletions

View File

@@ -19,8 +19,7 @@
<label for="authPassword">Password</label> <label for="authPassword">Password</label>
<input id="authPassword" type="password" maxlength="64" autocomplete="current-password" /> <input id="authPassword" type="password" maxlength="64" autocomplete="current-password" />
</div> </div>
<p id="authPolicyHintLogin" class="auth-hint"></p> <button id="showRegisterButton" type="button">Register</button>
<button id="showRegisterButton" type="button">Need an account? Register</button>
</section> </section>
<section id="registerView" class="auth-panel hidden"> <section id="registerView" class="auth-panel hidden">
<h2>Register</h2> <h2>Register</h2>
@@ -32,12 +31,16 @@
<label for="registerPassword">Password</label> <label for="registerPassword">Password</label>
<input id="registerPassword" type="password" maxlength="64" autocomplete="new-password" /> <input id="registerPassword" type="password" maxlength="64" autocomplete="new-password" />
</div> </div>
<div class="auth-row">
<label for="registerPasswordConfirm">Confirm Password</label>
<input id="registerPasswordConfirm" type="password" maxlength="64" autocomplete="new-password" />
</div>
<div class="auth-row"> <div class="auth-row">
<label for="registerEmail">Email (optional)</label> <label for="registerEmail">Email (optional)</label>
<input id="registerEmail" type="email" maxlength="320" autocomplete="email" /> <input id="registerEmail" type="email" maxlength="320" autocomplete="email" />
</div> </div>
<p id="authPolicyHintRegister" class="auth-hint"></p> <p id="authPolicyHintRegister" class="auth-hint"></p>
<button id="showLoginButton" type="button">Have an account? Log in</button> <button id="showLoginButton" type="button">Login</button>
</section> </section>
<div class="controls" id="button-container"> <div class="controls" id="button-container">
<button id="connectButton">Connect</button> <button id="connectButton">Connect</button>

View File

@@ -1,5 +1,5 @@
// Maintainer-controlled web client version. // Maintainer-controlled web client version.
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
window.CHGRID_WEB_VERSION = "2026.02.25 R247"; window.CHGRID_WEB_VERSION = "2026.02.25 R248";
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
window.CHGRID_TIME_ZONE = "America/Detroit"; window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -95,9 +95,9 @@ type Dom = {
registerView: HTMLElement; registerView: HTMLElement;
authUsername: HTMLInputElement; authUsername: HTMLInputElement;
authPassword: HTMLInputElement; authPassword: HTMLInputElement;
authPolicyHintLogin: HTMLParagraphElement;
registerUsername: HTMLInputElement; registerUsername: HTMLInputElement;
registerPassword: HTMLInputElement; registerPassword: HTMLInputElement;
registerPasswordConfirm: HTMLInputElement;
registerEmail: HTMLInputElement; registerEmail: HTMLInputElement;
authPolicyHintRegister: HTMLParagraphElement; authPolicyHintRegister: HTMLParagraphElement;
showRegisterButton: HTMLButtonElement; showRegisterButton: HTMLButtonElement;
@@ -128,9 +128,9 @@ const dom: Dom = {
registerView: requiredById('registerView'), registerView: requiredById('registerView'),
authUsername: requiredById('authUsername'), authUsername: requiredById('authUsername'),
authPassword: requiredById('authPassword'), authPassword: requiredById('authPassword'),
authPolicyHintLogin: requiredById('authPolicyHintLogin'),
registerUsername: requiredById('registerUsername'), registerUsername: requiredById('registerUsername'),
registerPassword: requiredById('registerPassword'), registerPassword: requiredById('registerPassword'),
registerPasswordConfirm: requiredById('registerPasswordConfirm'),
registerEmail: requiredById('registerEmail'), registerEmail: requiredById('registerEmail'),
authPolicyHintRegister: requiredById('authPolicyHintRegister'), authPolicyHintRegister: requiredById('authPolicyHintRegister'),
showRegisterButton: requiredById('showRegisterButton'), showRegisterButton: requiredById('showRegisterButton'),
@@ -551,9 +551,7 @@ function applyAuthPolicy(policy: unknown): void {
passwordMaxLength: passwordMax, passwordMaxLength: passwordMax,
}; };
localStorage.setItem(AUTH_POLICY_STORAGE_KEY, JSON.stringify(authPolicy)); localStorage.setItem(AUTH_POLICY_STORAGE_KEY, JSON.stringify(authPolicy));
const hint = `Username: ${usernameMin}-${usernameMax} chars (a-z, 0-9, _, -). Password: ${passwordMin}-${passwordMax} chars.`; dom.authPolicyHintRegister.textContent = `Username, ${usernameMin}-${usernameMax} characters. Password, ${passwordMin}-${passwordMax} characters.`;
dom.authPolicyHintLogin.textContent = hint;
dom.authPolicyHintRegister.textContent = hint;
dom.authUsername.minLength = usernameMin; dom.authUsername.minLength = usernameMin;
dom.authUsername.maxLength = usernameMax; dom.authUsername.maxLength = usernameMax;
dom.registerUsername.minLength = usernameMin; dom.registerUsername.minLength = usernameMin;
@@ -562,6 +560,8 @@ function applyAuthPolicy(policy: unknown): void {
dom.authPassword.maxLength = passwordMax; dom.authPassword.maxLength = passwordMax;
dom.registerPassword.minLength = passwordMin; dom.registerPassword.minLength = passwordMin;
dom.registerPassword.maxLength = passwordMax; dom.registerPassword.maxLength = passwordMax;
dom.registerPasswordConfirm.minLength = passwordMin;
dom.registerPasswordConfirm.maxLength = passwordMax;
updateConnectAvailability(); updateConnectAvailability();
} }
@@ -597,7 +597,8 @@ function updateConnectAvailability(): void {
sanitizeAuthUsername(dom.authUsername.value).length >= usernameMin && dom.authPassword.value.trim().length >= passwordMin; sanitizeAuthUsername(dom.authUsername.value).length >= usernameMin && dom.authPassword.value.trim().length >= passwordMin;
const hasRegisterCredentials = const hasRegisterCredentials =
sanitizeAuthUsername(dom.registerUsername.value).length >= usernameMin && sanitizeAuthUsername(dom.registerUsername.value).length >= usernameMin &&
dom.registerPassword.value.trim().length >= passwordMin; dom.registerPassword.value.trim().length >= passwordMin &&
dom.registerPassword.value === dom.registerPasswordConfirm.value;
const authReady = hasSessionToken || (authMode === 'login' ? hasLoginCredentials : hasRegisterCredentials); const authReady = hasSessionToken || (authMode === 'login' ? hasLoginCredentials : hasRegisterCredentials);
dom.connectButton.textContent = hasSessionToken ? 'Connect' : authMode === 'login' ? 'Log In & Connect' : 'Register & Connect'; dom.connectButton.textContent = hasSessionToken ? 'Connect' : authMode === 'login' ? 'Log In & Connect' : 'Register & Connect';
dom.connectButton.disabled = mediaSession.isConnecting() || !authReady; dom.connectButton.disabled = mediaSession.isConnecting() || !authReady;
@@ -1423,7 +1424,7 @@ function buildAuthRequestPacket(): OutgoingMessage | null {
const username = sanitizeAuthUsername(dom.registerUsername.value); const username = sanitizeAuthUsername(dom.registerUsername.value);
const password = dom.registerPassword.value; const password = dom.registerPassword.value;
const email = dom.registerEmail.value.trim(); const email = dom.registerEmail.value.trim();
if (!username || !password) return null; if (!username || !password || password !== dom.registerPasswordConfirm.value) return null;
return { type: 'auth_register', username, password, ...(email ? { email } : {}) }; return { type: 'auth_register', username, password, ...(email ? { email } : {}) };
} }
const username = sanitizeAuthUsername(dom.authUsername.value); const username = sanitizeAuthUsername(dom.authUsername.value);
@@ -1462,6 +1463,7 @@ async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_
if (!message.ok) { if (!message.ok) {
dom.authPassword.value = ''; dom.authPassword.value = '';
dom.registerPassword.value = ''; dom.registerPassword.value = '';
dom.registerPasswordConfirm.value = '';
if (message.message.toLowerCase().includes('session')) { if (message.message.toLowerCase().includes('session')) {
authSessionToken = ''; authSessionToken = '';
settings.saveAuthSessionToken(''); settings.saveAuthSessionToken('');
@@ -1493,6 +1495,7 @@ async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_
} }
dom.authPassword.value = ''; dom.authPassword.value = '';
dom.registerPassword.value = ''; dom.registerPassword.value = '';
dom.registerPasswordConfirm.value = '';
setConnectionStatus('Authenticated. Joining world...'); setConnectionStatus('Authenticated. Joining world...');
} }
@@ -2672,6 +2675,9 @@ function setupUiHandlers(): void {
dom.registerPassword.addEventListener('input', () => { dom.registerPassword.addEventListener('input', () => {
updateConnectAvailability(); updateConnectAvailability();
}); });
dom.registerPasswordConfirm.addEventListener('input', () => {
updateConnectAvailability();
});
dom.registerEmail.addEventListener('input', () => { dom.registerEmail.addEventListener('input', () => {
updateConnectAvailability(); updateConnectAvailability();
}); });

View File

@@ -5,9 +5,6 @@ REPO_ROOT="${1:-/home/bestmidi/chgrid}"
PUBLISH_DIR="${2:-/home/bestmidi/public_html/chgrid}" PUBLISH_DIR="${2:-/home/bestmidi/public_html/chgrid}"
BASE_PATH="${3:-/chgrid/}" BASE_PATH="${3:-/chgrid/}"
SERVICE_NAME="${4:-chat-grid.service}" SERVICE_NAME="${4:-chat-grid.service}"
if [[ -z "${SERVICE_NAME// }" || "$SERVICE_NAME" == "-" || "$SERVICE_NAME" == ".service" ]]; then
SERVICE_NAME="chat-grid.service"
fi
"$REPO_ROOT/deploy/scripts/deploy_client.sh" "$REPO_ROOT" "$PUBLISH_DIR" "$BASE_PATH" "$REPO_ROOT/deploy/scripts/deploy_client.sh" "$REPO_ROOT" "$PUBLISH_DIR" "$BASE_PATH"
sudo systemctl restart "$SERVICE_NAME" sudo systemctl restart "$SERVICE_NAME"

View File

@@ -12,7 +12,6 @@ import re
import secrets import secrets
import sqlite3 import sqlite3
import time import time
import uuid
SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000 SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000
@@ -111,16 +110,15 @@ class AuthService:
if role not in {"user", "admin"}: if role not in {"user", "admin"}:
raise AuthError("role must be user or admin.") raise AuthError("role must be user or admin.")
now_ms = self.now_ms() now_ms = self.now_ms()
user_id = str(uuid.uuid4())
password_hash = self._hash_password(password) password_hash = self._hash_password(password)
try: try:
self._conn.execute( self._conn.execute(
""" """
INSERT INTO users ( INSERT INTO users (
id, username, password_hash, email, role, status, last_nickname, created_at_ms, updated_at_ms, last_login_at_ms username, password_hash, email, role, status, last_nickname, created_at_ms, updated_at_ms, last_login_at_ms
) VALUES (?, ?, ?, ?, ?, 'active', NULL, ?, ?, ?) ) VALUES (?, ?, ?, ?, 'active', NULL, ?, ?, ?)
""", """,
(user_id, normalized_username, password_hash, normalized_email, role, now_ms, now_ms, now_ms), (normalized_username, password_hash, normalized_email, role, now_ms, now_ms, now_ms),
) )
self._conn.commit() self._conn.commit()
except sqlite3.IntegrityError as exc: except sqlite3.IntegrityError as exc:
@@ -154,6 +152,16 @@ class AuthService:
if not self._verify_password(password, user_row["password_hash"]): if not self._verify_password(password, user_row["password_hash"]):
raise AuthError("Invalid username or password.") raise AuthError("Invalid username or password.")
user = self._row_to_user(user_row) user = self._row_to_user(user_row)
if not user.last_nickname:
self.set_last_nickname(user.id, user.username)
user = AuthUser(
id=user.id,
username=user.username,
role=user.role,
status=user.status,
email=user.email,
last_nickname=user.username,
)
self._conn.execute( self._conn.execute(
"UPDATE users SET last_login_at_ms = ?, updated_at_ms = ? WHERE id = ?", "UPDATE users SET last_login_at_ms = ?, updated_at_ms = ? WHERE id = ?",
(self.now_ms(), self.now_ms(), user.id), (self.now_ms(), self.now_ms(), user.id),
@@ -196,13 +204,23 @@ class AuthService:
) )
self._conn.commit() self._conn.commit()
user = AuthUser( user = AuthUser(
id=row["user_id"], id=str(row["user_id"]),
username=row["username"], username=row["username"],
role=row["role"], role=row["role"],
status=row["status"], status=row["status"],
email=row["email"], email=row["email"],
last_nickname=row["last_nickname"], last_nickname=row["last_nickname"],
) )
if not user.last_nickname:
self.set_last_nickname(user.id, user.username)
user = AuthUser(
id=user.id,
username=user.username,
role=user.role,
status=user.status,
email=user.email,
last_nickname=user.username,
)
return AuthSession(session_id=row["session_id"], token=cleaned, user=user) return AuthSession(session_id=row["session_id"], token=cleaned, user=user)
def revoke(self, token: str) -> None: def revoke(self, token: str) -> None:
@@ -243,7 +261,7 @@ class AuthService:
self._conn.execute( self._conn.execute(
""" """
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
email TEXT UNIQUE, email TEXT UNIQUE,
@@ -259,8 +277,8 @@ class AuthService:
self._conn.execute( self._conn.execute(
""" """
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL, user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL UNIQUE, token_hash TEXT NOT NULL UNIQUE,
created_at_ms INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,
last_seen_at_ms INTEGER NOT NULL, last_seen_at_ms INTEGER NOT NULL,
@@ -288,14 +306,14 @@ class AuthService:
token_hash = self._hash_token(token) token_hash = self._hash_token(token)
now_ms = self.now_ms() now_ms = self.now_ms()
expires_at_ms = now_ms + SESSION_TTL_MS expires_at_ms = now_ms + SESSION_TTL_MS
session_id = str(uuid.uuid4())
self._conn.execute( self._conn.execute(
""" """
INSERT INTO sessions (id, user_id, token_hash, created_at_ms, last_seen_at_ms, expires_at_ms, revoked_at_ms, ip, user_agent) INSERT INTO sessions (user_id, token_hash, created_at_ms, last_seen_at_ms, expires_at_ms, revoked_at_ms, ip, user_agent)
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL) VALUES (?, ?, ?, ?, ?, NULL, NULL, NULL)
""", """,
(session_id, user.id, token_hash, now_ms, now_ms, expires_at_ms), (user.id, token_hash, now_ms, now_ms, expires_at_ms),
) )
session_id = str(self._conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
self._conn.commit() self._conn.commit()
return AuthSession(session_id=session_id, token=token, user=user) return AuthSession(session_id=session_id, token=token, user=user)
@@ -315,7 +333,7 @@ class AuthService:
"""Convert a DB row into AuthUser.""" """Convert a DB row into AuthUser."""
return AuthUser( return AuthUser(
id=row["id"], id=str(row["id"]),
username=row["username"], username=row["username"],
role=row["role"], role=row["role"],
status=row["status"], status=row["status"],