Show server auth policy limits in client auth UI
This commit is contained in:
@@ -19,6 +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">Create account</button>
|
<button id="showRegisterButton" type="button">Create account</button>
|
||||||
</section>
|
</section>
|
||||||
<section id="registerView" class="auth-panel hidden">
|
<section id="registerView" class="auth-panel hidden">
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
<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>
|
||||||
<button id="showLoginButton" type="button">Back to login</button>
|
<button id="showLoginButton" type="button">Back to login</button>
|
||||||
</section>
|
</section>
|
||||||
<div class="controls" id="button-container">
|
<div class="controls" id="button-container">
|
||||||
|
|||||||
@@ -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 R244";
|
window.CHGRID_WEB_VERSION = "2026.02.25 R245";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const RECONNECT_MAX_ATTEMPTS = 3;
|
|||||||
const AUDIO_SUBSCRIPTION_REFRESH_MS = 500;
|
const AUDIO_SUBSCRIPTION_REFRESH_MS = 500;
|
||||||
const TELEPORT_SQUARES_PER_SECOND = 20;
|
const TELEPORT_SQUARES_PER_SECOND = 20;
|
||||||
const TELEPORT_SYNC_INTERVAL_MS = 100;
|
const TELEPORT_SYNC_INTERVAL_MS = 100;
|
||||||
|
const AUTH_POLICY_STORAGE_KEY = 'chgridAuthPolicy';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -94,9 +95,11 @@ type Dom = {
|
|||||||
registerView: HTMLElement;
|
registerView: HTMLElement;
|
||||||
authUsername: HTMLInputElement;
|
authUsername: HTMLInputElement;
|
||||||
authPassword: HTMLInputElement;
|
authPassword: HTMLInputElement;
|
||||||
|
authPolicyHintLogin: HTMLParagraphElement;
|
||||||
registerUsername: HTMLInputElement;
|
registerUsername: HTMLInputElement;
|
||||||
registerPassword: HTMLInputElement;
|
registerPassword: HTMLInputElement;
|
||||||
registerEmail: HTMLInputElement;
|
registerEmail: HTMLInputElement;
|
||||||
|
authPolicyHintRegister: HTMLParagraphElement;
|
||||||
showRegisterButton: HTMLButtonElement;
|
showRegisterButton: HTMLButtonElement;
|
||||||
showLoginButton: HTMLButtonElement;
|
showLoginButton: HTMLButtonElement;
|
||||||
updatesSection: HTMLElement;
|
updatesSection: HTMLElement;
|
||||||
@@ -125,9 +128,11 @@ 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'),
|
||||||
registerEmail: requiredById('registerEmail'),
|
registerEmail: requiredById('registerEmail'),
|
||||||
|
authPolicyHintRegister: requiredById('authPolicyHintRegister'),
|
||||||
showRegisterButton: requiredById('showRegisterButton'),
|
showRegisterButton: requiredById('showRegisterButton'),
|
||||||
showLoginButton: requiredById('showLoginButton'),
|
showLoginButton: requiredById('showLoginButton'),
|
||||||
updatesSection: requiredById('updatesSection'),
|
updatesSection: requiredById('updatesSection'),
|
||||||
@@ -172,6 +177,13 @@ type HelpData = {
|
|||||||
sections: HelpSection[];
|
sections: HelpSection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AuthPolicy = {
|
||||||
|
usernameMinLength: number;
|
||||||
|
usernameMaxLength: number;
|
||||||
|
passwordMinLength: number;
|
||||||
|
passwordMaxLength: number;
|
||||||
|
};
|
||||||
|
|
||||||
/** Builds linearized help-view lines from sectioned help content. */
|
/** Builds linearized help-view lines from sectioned help content. */
|
||||||
function buildHelpLines(help: HelpData): string[] {
|
function buildHelpLines(help: HelpData): string[] {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
@@ -222,6 +234,7 @@ let outputMode = settings.loadOutputMode();
|
|||||||
let authMode: 'login' | 'register' = 'login';
|
let authMode: 'login' | 'register' = 'login';
|
||||||
let authSessionToken = settings.loadAuthSessionToken();
|
let authSessionToken = settings.loadAuthSessionToken();
|
||||||
let authUsername = settings.loadAuthUsername();
|
let authUsername = settings.loadAuthUsername();
|
||||||
|
let authPolicy: AuthPolicy | null = null;
|
||||||
let pendingAuthRequest = false;
|
let pendingAuthRequest = false;
|
||||||
const messageBuffer: string[] = [];
|
const messageBuffer: string[] = [];
|
||||||
let messageCursor = -1;
|
let messageCursor = -1;
|
||||||
@@ -504,11 +517,63 @@ function sanitizeName(value: string): string {
|
|||||||
|
|
||||||
/** Normalizes auth username according to server policy. */
|
/** Normalizes auth username according to server policy. */
|
||||||
function sanitizeAuthUsername(value: string): string {
|
function sanitizeAuthUsername(value: string): string {
|
||||||
|
const maxLength = authPolicy?.usernameMaxLength ?? 128;
|
||||||
return value
|
return value
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9_-]/g, '')
|
.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. */
|
/** 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.loginView.classList.toggle('hidden', authMode !== 'login');
|
||||||
dom.registerView.classList.toggle('hidden', authMode !== 'register');
|
dom.registerView.classList.toggle('hidden', authMode !== 'register');
|
||||||
const hasSessionToken = authSessionToken.trim().length > 0;
|
const hasSessionToken = authSessionToken.trim().length > 0;
|
||||||
|
const usernameMin = authPolicy?.usernameMinLength ?? 1;
|
||||||
|
const passwordMin = authPolicy?.passwordMinLength ?? 1;
|
||||||
const hasLoginCredentials =
|
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 =
|
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);
|
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;
|
||||||
@@ -1379,14 +1447,16 @@ function sendAuthRequest(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Handles server auth-required prompts prior to world welcome. */
|
/** 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.');
|
setConnectionStatus('Authentication required.');
|
||||||
updateStatus(message);
|
updateStatus(message.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies auth result state and terminates failed auth attempts quickly. */
|
/** Applies auth result state and terminates failed auth attempts quickly. */
|
||||||
async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_result' }>): Promise<void> {
|
async function handleAuthResult(message: Extract<IncomingMessage, { type: 'auth_result' }>): Promise<void> {
|
||||||
pendingAuthRequest = false;
|
pendingAuthRequest = false;
|
||||||
|
applyAuthPolicy(message.authPolicy);
|
||||||
if (!message.ok) {
|
if (!message.ok) {
|
||||||
dom.authPassword.value = '';
|
dom.authPassword.value = '';
|
||||||
dom.registerPassword.value = '';
|
dom.registerPassword.value = '';
|
||||||
@@ -1581,6 +1651,7 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
|||||||
let restartAnnouncement: string | null = null;
|
let restartAnnouncement: string | null = null;
|
||||||
let connectedAnnouncement: string | null = null;
|
let connectedAnnouncement: string | null = null;
|
||||||
if (message.type === 'welcome') {
|
if (message.type === 'welcome') {
|
||||||
|
applyAuthPolicy(message.auth?.policy);
|
||||||
const incomingInstanceId = String(message.serverInfo?.instanceId ?? '').trim() || null;
|
const incomingInstanceId = String(message.serverInfo?.instanceId ?? '').trim() || null;
|
||||||
const incomingVersion = String(message.serverInfo?.version ?? '').trim() || 'unknown';
|
const incomingVersion = String(message.serverInfo?.version ?? '').trim() || 'unknown';
|
||||||
connectedAnnouncement = reconnectInFlight
|
connectedAnnouncement = reconnectInFlight
|
||||||
@@ -2608,6 +2679,7 @@ setupInputHandlers();
|
|||||||
setupUiHandlers();
|
setupUiHandlers();
|
||||||
dom.authUsername.value = sanitizeAuthUsername(authUsername);
|
dom.authUsername.value = sanitizeAuthUsername(authUsername);
|
||||||
dom.registerUsername.value = sanitizeAuthUsername(authUsername);
|
dom.registerUsername.value = sanitizeAuthUsername(authUsername);
|
||||||
|
loadPersistedAuthPolicy();
|
||||||
setAuthMode('login');
|
setAuthMode('login');
|
||||||
updateConnectAvailability();
|
updateConnectAvailability();
|
||||||
updateDeviceSummary();
|
updateDeviceSummary();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ type MessageHandlerDeps = {
|
|||||||
playLocateToneAt: (x: number, y: number) => void;
|
playLocateToneAt: (x: number, y: number) => void;
|
||||||
resolveIncomingSoundUrl: (url: string) => string;
|
resolveIncomingSoundUrl: (url: string) => string;
|
||||||
playIncomingItemUseSound: (url: string, x: number, y: number) => void;
|
playIncomingItemUseSound: (url: string, x: number, y: number) => void;
|
||||||
handleAuthRequired: (message: string) => void;
|
handleAuthRequired: (message: Extract<IncomingMessage, { type: 'auth_required' }>) => void;
|
||||||
handleAuthResult: (message: Extract<IncomingMessage, { type: 'auth_result' }>) => Promise<void>;
|
handleAuthResult: (message: Extract<IncomingMessage, { type: 'auth_result' }>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
return async function onMessage(message: IncomingMessage): Promise<void> {
|
return async function onMessage(message: IncomingMessage): Promise<void> {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'auth_required':
|
case 'auth_required':
|
||||||
deps.handleAuthRequired(message.message);
|
deps.handleAuthRequired(message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'auth_result':
|
case 'auth_result':
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ export const welcomeMessageSchema = z.object({
|
|||||||
userId: z.string().nullable().optional(),
|
userId: z.string().nullable().optional(),
|
||||||
username: z.string().nullable().optional(),
|
username: z.string().nullable().optional(),
|
||||||
role: z.string().nullable().optional(),
|
role: z.string().nullable().optional(),
|
||||||
|
policy: z
|
||||||
|
.object({
|
||||||
|
usernameMinLength: z.number().int().positive(),
|
||||||
|
usernameMaxLength: z.number().int().positive(),
|
||||||
|
passwordMinLength: z.number().int().positive(),
|
||||||
|
passwordMaxLength: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
uiDefinitions: z
|
uiDefinitions: z
|
||||||
@@ -96,6 +104,14 @@ export const welcomeMessageSchema = z.object({
|
|||||||
export const authRequiredSchema = z.object({
|
export const authRequiredSchema = z.object({
|
||||||
type: z.literal('auth_required'),
|
type: z.literal('auth_required'),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
|
authPolicy: z
|
||||||
|
.object({
|
||||||
|
usernameMinLength: z.number().int().positive(),
|
||||||
|
usernameMaxLength: z.number().int().positive(),
|
||||||
|
passwordMinLength: z.number().int().positive(),
|
||||||
|
passwordMaxLength: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authResultSchema = z.object({
|
export const authResultSchema = z.object({
|
||||||
@@ -106,6 +122,14 @@ export const authResultSchema = z.object({
|
|||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
nickname: z.string().optional(),
|
nickname: z.string().optional(),
|
||||||
|
authPolicy: z
|
||||||
|
.object({
|
||||||
|
usernameMinLength: z.number().int().positive(),
|
||||||
|
usernameMaxLength: z.number().int().positive(),
|
||||||
|
passwordMinLength: z.number().int().positive(),
|
||||||
|
passwordMaxLength: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const signalMessageSchema = z.object({
|
export const signalMessageSchema = z.object({
|
||||||
|
|||||||
@@ -117,6 +117,12 @@ body {
|
|||||||
width: min(280px, 60vw);
|
width: min(280px, 60vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-hint {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0.35rem 0 0.5rem;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nickname-row {
|
.nickname-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `userId`
|
- `userId`
|
||||||
- `username`
|
- `username`
|
||||||
- `role`
|
- `role`
|
||||||
|
- `policy` (`usernameMinLength`, `usernameMaxLength`, `passwordMinLength`, `passwordMaxLength`)
|
||||||
|
- `auth_required.authPolicy`: server auth limits advertised before login/register submit.
|
||||||
|
- `auth_result.authPolicy`: server auth limits echoed on auth success/failure responses.
|
||||||
- `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing.
|
- `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing.
|
||||||
- `welcome.worldConfig.movementTickMs`: server movement-rate window used for client movement pacing.
|
- `welcome.worldConfig.movementTickMs`: server movement-rate window used for client movement pacing.
|
||||||
- `welcome.worldConfig.movementMaxStepsPerTick`: max allowed grid steps per movement window.
|
- `welcome.worldConfig.movementMaxStepsPerTick`: max allowed grid steps per movement window.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
2. Client validates auth form/session token and sets up local media.
|
2. Client validates auth form/session token and sets up local media.
|
||||||
3. Client connects signaling websocket.
|
3. Client connects signaling websocket.
|
||||||
4. Server sends `auth_required`.
|
4. Server sends `auth_required`.
|
||||||
|
- includes `authPolicy` limits for username/password.
|
||||||
5. Client sends `auth_login`, `auth_register`, or `auth_resume`.
|
5. Client sends `auth_login`, `auth_register`, or `auth_resume`.
|
||||||
6. Server sends `auth_result`.
|
6. Server sends `auth_result`.
|
||||||
7. Server sends `welcome` with users/items snapshot.
|
7. Server sends `welcome` with users/items snapshot.
|
||||||
@@ -42,7 +43,7 @@ Core incoming message effects:
|
|||||||
|
|
||||||
- `signal`: WebRTC negotiation and ICE exchange.
|
- `signal`: WebRTC negotiation and ICE exchange.
|
||||||
- `auth_required`: prompt client to authenticate before gameplay messages.
|
- `auth_required`: prompt client to authenticate before gameplay messages.
|
||||||
- `auth_result`: auth success/failure with optional session token + account metadata.
|
- `auth_result`: auth success/failure with optional session token + account metadata + `authPolicy`.
|
||||||
- `update_position`: update peer position; may play movement/teleport world sound.
|
- `update_position`: update peer position; may play movement/teleport world sound.
|
||||||
- `teleport_complete`: play peer teleport landing sound at final tile.
|
- `teleport_complete`: play peer teleport landing sound at final tile.
|
||||||
- `update_nickname`: update peer display name.
|
- `update_nickname`: update peer display name.
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ class WelcomePacket(BasePacket):
|
|||||||
class AuthRequiredPacket(BasePacket):
|
class AuthRequiredPacket(BasePacket):
|
||||||
type: Literal["auth_required"]
|
type: Literal["auth_required"]
|
||||||
message: str
|
message: str
|
||||||
|
authPolicy: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuthResultPacket(BasePacket):
|
class AuthResultPacket(BasePacket):
|
||||||
@@ -170,6 +171,7 @@ class AuthResultPacket(BasePacket):
|
|||||||
username: str | None = None
|
username: str | None = None
|
||||||
role: str | None = None
|
role: str | None = None
|
||||||
nickname: str | None = None
|
nickname: str | None = None
|
||||||
|
authPolicy: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserLeftPacket(BasePacket):
|
class UserLeftPacket(BasePacket):
|
||||||
|
|||||||
@@ -177,6 +177,16 @@ class SignalingServer:
|
|||||||
|
|
||||||
return nickname.casefold()
|
return nickname.casefold()
|
||||||
|
|
||||||
|
def _auth_policy(self) -> dict[str, int]:
|
||||||
|
"""Return server-auth policy limits advertised to clients."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"usernameMinLength": self.auth_service.username_min_length,
|
||||||
|
"usernameMaxLength": self.auth_service.username_max_length,
|
||||||
|
"passwordMinLength": self.auth_service.password_min_length,
|
||||||
|
"passwordMaxLength": self.auth_service.password_max_length,
|
||||||
|
}
|
||||||
|
|
||||||
def _flush_state_save(self) -> None:
|
def _flush_state_save(self) -> None:
|
||||||
"""Immediately flush pending state persistence and clear debounce state."""
|
"""Immediately flush pending state persistence and clear debounce state."""
|
||||||
|
|
||||||
@@ -748,7 +758,11 @@ class SignalingServer:
|
|||||||
try:
|
try:
|
||||||
await self._send(
|
await self._send(
|
||||||
websocket,
|
websocket,
|
||||||
AuthRequiredPacket(type="auth_required", message="Authentication required."),
|
AuthRequiredPacket(
|
||||||
|
type="auth_required",
|
||||||
|
message="Authentication required.",
|
||||||
|
authPolicy=self._auth_policy(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async for raw_message in websocket:
|
async for raw_message in websocket:
|
||||||
await self._handle_message(client, raw_message)
|
await self._handle_message(client, raw_message)
|
||||||
@@ -805,6 +819,7 @@ class SignalingServer:
|
|||||||
"userId": client.user_id,
|
"userId": client.user_id,
|
||||||
"username": client.username,
|
"username": client.username,
|
||||||
"role": client.role if client.authenticated else None,
|
"role": client.role if client.authenticated else None,
|
||||||
|
"policy": self._auth_policy(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await self._send(client.websocket, packet)
|
await self._send(client.websocket, packet)
|
||||||
@@ -844,7 +859,12 @@ class SignalingServer:
|
|||||||
if client.authenticated and isinstance(packet, (AuthLoginPacket, AuthRegisterPacket, AuthResumePacket)):
|
if client.authenticated and isinstance(packet, (AuthLoginPacket, AuthRegisterPacket, AuthResumePacket)):
|
||||||
await self._send(
|
await self._send(
|
||||||
client.websocket,
|
client.websocket,
|
||||||
AuthResultPacket(type="auth_result", ok=False, message="Already authenticated."),
|
AuthResultPacket(
|
||||||
|
type="auth_result",
|
||||||
|
ok=False,
|
||||||
|
message="Already authenticated.",
|
||||||
|
authPolicy=self._auth_policy(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -861,7 +881,12 @@ class SignalingServer:
|
|||||||
client.session_token = None
|
client.session_token = None
|
||||||
await self._send(
|
await self._send(
|
||||||
client.websocket,
|
client.websocket,
|
||||||
AuthResultPacket(type="auth_result", ok=True, message="Logged out."),
|
AuthResultPacket(
|
||||||
|
type="auth_result",
|
||||||
|
ok=True,
|
||||||
|
message="Logged out.",
|
||||||
|
authPolicy=self._auth_policy(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await client.websocket.close()
|
await client.websocket.close()
|
||||||
return True
|
return True
|
||||||
@@ -870,7 +895,12 @@ class SignalingServer:
|
|||||||
except AuthError as exc:
|
except AuthError as exc:
|
||||||
await self._send(
|
await self._send(
|
||||||
client.websocket,
|
client.websocket,
|
||||||
AuthResultPacket(type="auth_result", ok=False, message=str(exc)),
|
AuthResultPacket(
|
||||||
|
type="auth_result",
|
||||||
|
ok=False,
|
||||||
|
message=str(exc),
|
||||||
|
authPolicy=self._auth_policy(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -890,6 +920,7 @@ class SignalingServer:
|
|||||||
username=session.user.username,
|
username=session.user.username,
|
||||||
role=session.user.role,
|
role=session.user.role,
|
||||||
nickname=client.nickname,
|
nickname=client.nickname,
|
||||||
|
authPolicy=self._auth_policy(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await self._activate_authenticated_client(client)
|
await self._activate_authenticated_client(client)
|
||||||
|
|||||||
Reference in New Issue
Block a user