diff --git a/client/index.html b/client/index.html
index 9ab3b37..c7de718 100644
--- a/client/index.html
+++ b/client/index.html
@@ -19,8 +19,7 @@
-
-
+
diff --git a/client/public/version.js b/client/public/version.js
index 82d151e..2bc04bc 100644
--- a/client/public/version.js
+++ b/client/public/version.js
@@ -1,5 +1,5 @@
// Maintainer-controlled web client version.
// 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.
window.CHGRID_TIME_ZONE = "America/Detroit";
diff --git a/client/src/main.ts b/client/src/main.ts
index dab65e0..34364ac 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -95,9 +95,9 @@ type Dom = {
registerView: HTMLElement;
authUsername: HTMLInputElement;
authPassword: HTMLInputElement;
- authPolicyHintLogin: HTMLParagraphElement;
registerUsername: HTMLInputElement;
registerPassword: HTMLInputElement;
+ registerPasswordConfirm: HTMLInputElement;
registerEmail: HTMLInputElement;
authPolicyHintRegister: HTMLParagraphElement;
showRegisterButton: HTMLButtonElement;
@@ -128,9 +128,9 @@ const dom: Dom = {
registerView: requiredById('registerView'),
authUsername: requiredById('authUsername'),
authPassword: requiredById('authPassword'),
- authPolicyHintLogin: requiredById('authPolicyHintLogin'),
registerUsername: requiredById('registerUsername'),
registerPassword: requiredById('registerPassword'),
+ registerPasswordConfirm: requiredById('registerPasswordConfirm'),
registerEmail: requiredById('registerEmail'),
authPolicyHintRegister: requiredById('authPolicyHintRegister'),
showRegisterButton: requiredById('showRegisterButton'),
@@ -551,9 +551,7 @@ function applyAuthPolicy(policy: unknown): void {
passwordMaxLength: passwordMax,
};
localStorage.setItem(AUTH_POLICY_STORAGE_KEY, JSON.stringify(authPolicy));
- const hint = `Username: ${usernameMin}-${usernameMax} chars (a-z, 0-9, _, -). Password: ${passwordMin}-${passwordMax} chars.`;
- dom.authPolicyHintLogin.textContent = hint;
- dom.authPolicyHintRegister.textContent = hint;
+ dom.authPolicyHintRegister.textContent = `Username, ${usernameMin}-${usernameMax} characters. Password, ${passwordMin}-${passwordMax} characters.`;
dom.authUsername.minLength = usernameMin;
dom.authUsername.maxLength = usernameMax;
dom.registerUsername.minLength = usernameMin;
@@ -562,6 +560,8 @@ function applyAuthPolicy(policy: unknown): void {
dom.authPassword.maxLength = passwordMax;
dom.registerPassword.minLength = passwordMin;
dom.registerPassword.maxLength = passwordMax;
+ dom.registerPasswordConfirm.minLength = passwordMin;
+ dom.registerPasswordConfirm.maxLength = passwordMax;
updateConnectAvailability();
}
@@ -597,7 +597,8 @@ function updateConnectAvailability(): void {
sanitizeAuthUsername(dom.authUsername.value).length >= usernameMin && dom.authPassword.value.trim().length >= passwordMin;
const hasRegisterCredentials =
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);
dom.connectButton.textContent = hasSessionToken ? 'Connect' : authMode === 'login' ? 'Log In & Connect' : 'Register & Connect';
dom.connectButton.disabled = mediaSession.isConnecting() || !authReady;
@@ -1423,7 +1424,7 @@ function buildAuthRequestPacket(): OutgoingMessage | null {
const username = sanitizeAuthUsername(dom.registerUsername.value);
const password = dom.registerPassword.value;
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 } : {}) };
}
const username = sanitizeAuthUsername(dom.authUsername.value);
@@ -1462,6 +1463,7 @@ async function handleAuthResult(message: Extract {
updateConnectAvailability();
});
+ dom.registerPasswordConfirm.addEventListener('input', () => {
+ updateConnectAvailability();
+ });
dom.registerEmail.addEventListener('input', () => {
updateConnectAvailability();
});
diff --git a/deploy/scripts/up.sh b/deploy/scripts/up.sh
index eca875b..d59671c 100644
--- a/deploy/scripts/up.sh
+++ b/deploy/scripts/up.sh
@@ -5,9 +5,6 @@ REPO_ROOT="${1:-/home/bestmidi/chgrid}"
PUBLISH_DIR="${2:-/home/bestmidi/public_html/chgrid}"
BASE_PATH="${3:-/chgrid/}"
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"
sudo systemctl restart "$SERVICE_NAME"
diff --git a/server/app/auth_service.py b/server/app/auth_service.py
index 37e6859..82fec3b 100644
--- a/server/app/auth_service.py
+++ b/server/app/auth_service.py
@@ -12,7 +12,6 @@ import re
import secrets
import sqlite3
import time
-import uuid
SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000
@@ -111,16 +110,15 @@ class AuthService:
if role not in {"user", "admin"}:
raise AuthError("role must be user or admin.")
now_ms = self.now_ms()
- user_id = str(uuid.uuid4())
password_hash = self._hash_password(password)
try:
self._conn.execute(
"""
INSERT INTO users (
- id, username, password_hash, email, role, status, last_nickname, created_at_ms, updated_at_ms, last_login_at_ms
- ) VALUES (?, ?, ?, ?, ?, 'active', NULL, ?, ?, ?)
+ username, password_hash, email, role, status, last_nickname, created_at_ms, updated_at_ms, last_login_at_ms
+ ) 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()
except sqlite3.IntegrityError as exc:
@@ -154,6 +152,16 @@ class AuthService:
if not self._verify_password(password, user_row["password_hash"]):
raise AuthError("Invalid username or password.")
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(
"UPDATE users SET last_login_at_ms = ?, updated_at_ms = ? WHERE id = ?",
(self.now_ms(), self.now_ms(), user.id),
@@ -196,13 +204,23 @@ class AuthService:
)
self._conn.commit()
user = AuthUser(
- id=row["user_id"],
+ id=str(row["user_id"]),
username=row["username"],
role=row["role"],
status=row["status"],
email=row["email"],
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)
def revoke(self, token: str) -> None:
@@ -243,7 +261,7 @@ class AuthService:
self._conn.execute(
"""
CREATE TABLE IF NOT EXISTS users (
- id TEXT PRIMARY KEY,
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
email TEXT UNIQUE,
@@ -259,8 +277,8 @@ class AuthService:
self._conn.execute(
"""
CREATE TABLE IF NOT EXISTS sessions (
- id TEXT PRIMARY KEY,
- user_id TEXT NOT NULL,
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
created_at_ms INTEGER NOT NULL,
last_seen_at_ms INTEGER NOT NULL,
@@ -288,14 +306,14 @@ class AuthService:
token_hash = self._hash_token(token)
now_ms = self.now_ms()
expires_at_ms = now_ms + SESSION_TTL_MS
- session_id = str(uuid.uuid4())
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)
- VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL)
+ 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)
""",
- (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()
return AuthSession(session_id=session_id, token=token, user=user)
@@ -315,7 +333,7 @@ class AuthService:
"""Convert a DB row into AuthUser."""
return AuthUser(
- id=row["id"],
+ id=str(row["id"]),
username=row["username"],
role=row["role"],
status=row["status"],