Show server auth policy limits in client auth UI

This commit is contained in:
Jage9
2026-02-24 22:35:29 -05:00
parent f26e1f3c27
commit 06d5e3fbf3
10 changed files with 154 additions and 13 deletions

View File

@@ -79,6 +79,7 @@ const RECONNECT_MAX_ATTEMPTS = 3;
const AUDIO_SUBSCRIPTION_REFRESH_MS = 500;
const TELEPORT_SQUARES_PER_SECOND = 20;
const TELEPORT_SYNC_INTERVAL_MS = 100;
const AUTH_POLICY_STORAGE_KEY = 'chgridAuthPolicy';
declare global {
interface Window {
@@ -94,9 +95,11 @@ type Dom = {
registerView: HTMLElement;
authUsername: HTMLInputElement;
authPassword: HTMLInputElement;
authPolicyHintLogin: HTMLParagraphElement;
registerUsername: HTMLInputElement;
registerPassword: HTMLInputElement;
registerEmail: HTMLInputElement;
authPolicyHintRegister: HTMLParagraphElement;
showRegisterButton: HTMLButtonElement;
showLoginButton: HTMLButtonElement;
updatesSection: HTMLElement;
@@ -125,9 +128,11 @@ const dom: Dom = {
registerView: requiredById('registerView'),
authUsername: requiredById('authUsername'),
authPassword: requiredById('authPassword'),
authPolicyHintLogin: requiredById('authPolicyHintLogin'),
registerUsername: requiredById('registerUsername'),
registerPassword: requiredById('registerPassword'),
registerEmail: requiredById('registerEmail'),
authPolicyHintRegister: requiredById('authPolicyHintRegister'),
showRegisterButton: requiredById('showRegisterButton'),
showLoginButton: requiredById('showLoginButton'),
updatesSection: requiredById('updatesSection'),
@@ -172,6 +177,13 @@ type HelpData = {
sections: HelpSection[];
};
type AuthPolicy = {
usernameMinLength: number;
usernameMaxLength: number;
passwordMinLength: number;
passwordMaxLength: number;
};
/** Builds linearized help-view lines from sectioned help content. */
function buildHelpLines(help: HelpData): string[] {
const lines: string[] = [];
@@ -222,6 +234,7 @@ let outputMode = settings.loadOutputMode();
let authMode: 'login' | 'register' = 'login';
let authSessionToken = settings.loadAuthSessionToken();
let authUsername = settings.loadAuthUsername();
let authPolicy: AuthPolicy | null = null;
let pendingAuthRequest = false;
const messageBuffer: string[] = [];
let messageCursor = -1;
@@ -504,11 +517,63 @@ function sanitizeName(value: string): string {
/** Normalizes auth username according to server policy. */
function sanitizeAuthUsername(value: string): string {
const maxLength = authPolicy?.usernameMaxLength ?? 128;
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]/g, '')
.slice(0, 32);
.slice(0, Math.max(1, maxLength));
}
/** Normalizes and stores server-advertised auth policy limits, then refreshes auth UI hints. */
function applyAuthPolicy(policy: unknown): void {
if (!policy || typeof policy !== 'object') return;
const raw = policy as Partial<AuthPolicy>;
const usernameMin = Number(raw.usernameMinLength);
const usernameMax = Number(raw.usernameMaxLength);
const passwordMin = Number(raw.passwordMinLength);
const passwordMax = Number(raw.passwordMaxLength);
if (
!Number.isInteger(usernameMin) ||
!Number.isInteger(usernameMax) ||
!Number.isInteger(passwordMin) ||
!Number.isInteger(passwordMax)
) {
return;
}
if (usernameMin < 1 || usernameMax < usernameMin || passwordMin < 1 || passwordMax < passwordMin) {
return;
}
authPolicy = {
usernameMinLength: usernameMin,
usernameMaxLength: usernameMax,
passwordMinLength: passwordMin,
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.authUsername.minLength = usernameMin;
dom.authUsername.maxLength = usernameMax;
dom.registerUsername.minLength = usernameMin;
dom.registerUsername.maxLength = usernameMax;
dom.authPassword.minLength = passwordMin;
dom.authPassword.maxLength = passwordMax;
dom.registerPassword.minLength = passwordMin;
dom.registerPassword.maxLength = passwordMax;
updateConnectAvailability();
}
/** Loads most recently-seen auth policy limits from local storage for pre-connect UI hints. */
function loadPersistedAuthPolicy(): void {
const raw = localStorage.getItem(AUTH_POLICY_STORAGE_KEY);
if (!raw) return;
try {
applyAuthPolicy(JSON.parse(raw));
} catch {
// Ignore malformed persisted policy and keep live server policy source of truth.
}
}
/** Enables/disables the connect button based on state and nickname validity. */
@@ -524,10 +589,13 @@ function updateConnectAvailability(): void {
dom.loginView.classList.toggle('hidden', authMode !== 'login');
dom.registerView.classList.toggle('hidden', authMode !== 'register');
const hasSessionToken = authSessionToken.trim().length > 0;
const usernameMin = authPolicy?.usernameMinLength ?? 1;
const passwordMin = authPolicy?.passwordMinLength ?? 1;
const hasLoginCredentials =
sanitizeAuthUsername(dom.authUsername.value).length >= 2 && dom.authPassword.value.trim().length >= 8;
sanitizeAuthUsername(dom.authUsername.value).length >= usernameMin && dom.authPassword.value.trim().length >= passwordMin;
const hasRegisterCredentials =
sanitizeAuthUsername(dom.registerUsername.value).length >= 2 && dom.registerPassword.value.trim().length >= 8;
sanitizeAuthUsername(dom.registerUsername.value).length >= usernameMin &&
dom.registerPassword.value.trim().length >= passwordMin;
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;
@@ -1379,14 +1447,16 @@ function sendAuthRequest(): void {
}
/** Handles server auth-required prompts prior to world welcome. */
function handleAuthRequired(message: string): void {
function handleAuthRequired(message: Extract<IncomingMessage, { type: 'auth_required' }>): void {
applyAuthPolicy(message.authPolicy);
setConnectionStatus('Authentication required.');
updateStatus(message);
updateStatus(message.message);
}
/** Applies auth result state and terminates failed auth attempts quickly. */
async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_result' }>): Promise<void> {
pendingAuthRequest = false;
applyAuthPolicy(message.authPolicy);
if (!message.ok) {
dom.authPassword.value = '';
dom.registerPassword.value = '';
@@ -1581,6 +1651,7 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
let restartAnnouncement: string | null = null;
let connectedAnnouncement: string | null = null;
if (message.type === 'welcome') {
applyAuthPolicy(message.auth?.policy);
const incomingInstanceId = String(message.serverInfo?.instanceId ?? '').trim() || null;
const incomingVersion = String(message.serverInfo?.version ?? '').trim() || 'unknown';
connectedAnnouncement = reconnectInFlight
@@ -2608,6 +2679,7 @@ setupInputHandlers();
setupUiHandlers();
dom.authUsername.value = sanitizeAuthUsername(authUsername);
dom.registerUsername.value = sanitizeAuthUsername(authUsername);
loadPersistedAuthPolicy();
setAuthMode('login');
updateConnectAvailability();
updateDeviceSummary();