Show server auth policy limits in client auth UI
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user