Tighten auth defaults and register form behavior
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
Reference in New Issue
Block a user