feat: validate WebSocket URL on login screen

Show inline hint when the gateway URL doesn't start with ws:// or wss://
and disable the connect button until it's valid. Prevents confusing
connection errors from malformed URLs.
This commit is contained in:
Nicolas Varrot
2026-02-12 06:55:19 +00:00
parent cb882f5ead
commit dc49734819
2 changed files with 14 additions and 3 deletions

View File

@@ -26,10 +26,14 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
const [token, setToken] = useState(getInitialToken); const [token, setToken] = useState(getInitialToken);
const [showToken, setShowToken] = useState(false); const [showToken, setShowToken] = useState(false);
const urlTrimmed = url.trim();
const isValidWsUrl = /^wss?:\/\/.+/.test(urlTrimmed);
const showUrlHint = urlTrimmed.length > 0 && !isValidWsUrl;
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!url.trim() || !token.trim()) return; if (!urlTrimmed || !token.trim() || !isValidWsUrl) return;
onConnect(url.trim(), token.trim()); onConnect(urlTrimmed, token.trim());
}; };
return ( return (
@@ -61,6 +65,11 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
autoComplete="url" autoComplete="url"
disabled={isConnecting} disabled={isConnecting}
/> />
{showUrlHint && (
<p className="text-xs text-amber-400/80 mt-1.5 pl-1">
{t('login.wsHint')}
</p>
)}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -98,7 +107,7 @@ export function LoginScreen({ onConnect, error, isConnecting }: Props) {
<button <button
type="submit" type="submit"
disabled={!url.trim() || !token.trim() || isConnecting} disabled={!isValidWsUrl || !token.trim() || isConnecting}
className="w-full rounded-xl bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/30 hover:brightness-110 disabled:opacity-40 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2" className="w-full rounded-xl bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/30 hover:brightness-110 disabled:opacity-40 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
> >
{isConnecting ? ( {isConnecting ? (

View File

@@ -19,6 +19,7 @@ const en = {
'login.showToken': 'Show token', 'login.showToken': 'Show token',
'login.hideToken': 'Hide token', 'login.hideToken': 'Hide token',
'login.storedLocally': 'Credentials are stored locally in your browser', 'login.storedLocally': 'Credentials are stored locally in your browser',
'login.wsHint': 'URL must start with ws:// or wss://',
// Header // Header
'header.title': 'PinchChat', 'header.title': 'PinchChat',
@@ -97,6 +98,7 @@ const fr: Record<keyof typeof en, string> = {
'login.showToken': 'Afficher le token', 'login.showToken': 'Afficher le token',
'login.hideToken': 'Masquer le token', 'login.hideToken': 'Masquer le token',
'login.storedLocally': 'Les identifiants sont stockés localement dans votre navigateur', 'login.storedLocally': 'Les identifiants sont stockés localement dans votre navigateur',
'login.wsHint': 'L\'URL doit commencer par ws:// ou wss://',
'header.title': 'PinchChat', 'header.title': 'PinchChat',
'header.connected': 'Connecté', 'header.connected': 'Connecté',