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:
@@ -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 ? (
|
||||||
|
|||||||
@@ -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é',
|
||||||
|
|||||||
Reference in New Issue
Block a user