Clarify repeater interaction button re: passwords and ACLs

This commit is contained in:
Jack Kingsman
2026-01-14 20:17:08 -08:00
parent 076d466fbd
commit d838a5e1b5
7 changed files with 59 additions and 61 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-w8xAO9M9.js"></script>
<script type="module" crossorigin src="/assets/index-DkHyTtP0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DZ67iE5i.css">
</head>
<body>
+1
View File
@@ -264,6 +264,7 @@ export function ConfigModal({
<Input
id="private-key"
type="password"
autoComplete="off"
value={privateKey}
onChange={(e) => setPrivateKey(e.target.value)}
placeholder="64-character hex private key"
+10 -11
View File
@@ -104,14 +104,12 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
e.preventDefault();
const trimmed = text.trim();
// For repeater mode, allow empty password via "."
// For repeater mode, empty password means guest login
if (isRepeaterMode) {
if (sending || disabled) return;
// "." means empty password
const password = trimmed === '.' ? '' : trimmed;
setSending(true);
try {
await onSend(password);
await onSend(trimmed);
setText('');
} catch (err) {
console.error('Failed to request telemetry:', err);
@@ -156,10 +154,8 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
[handleSubmit]
);
// For repeater mode, enable submit if there's text OR if it's just "." for empty password
const canSubmit = isRepeaterMode
? text.trim().length > 0 || text === '.'
: text.trim().length > 0;
// For repeater mode, always allow submit (empty = guest login)
const canSubmit = isRepeaterMode ? true : text.trim().length > 0;
// Show character counter for messages (not repeater mode or raw)
const showCharCounter = !isRepeaterMode && limits !== null;
@@ -170,12 +166,13 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
<Input
ref={inputRef}
type={isRepeaterMode ? 'password' : 'text'}
autoComplete={isRepeaterMode ? 'off' : undefined}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
placeholder ||
(isRepeaterMode ? 'Enter password (or . for none)...' : 'Type a message...')
(isRepeaterMode ? 'Enter password for admin login...' : 'Type a message...')
}
disabled={disabled || sending}
className="flex-1 min-w-0"
@@ -187,10 +184,12 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
>
{sending
? isRepeaterMode
? 'Fetching...'
? 'Logging in...'
: 'Sending...'
: isRepeaterMode
? 'Fetch'
? text.trim()
? 'Log in with password'
: 'Log in as guest/use repeater ACLs'
: 'Send'}
</Button>
</div>
+27 -29
View File
@@ -5,7 +5,7 @@
* regress if the code is modified:
*
* 1. Repeater messages should NOT have sender parsed from text (colons are common in CLI output)
* 2. Password field "." should convert to empty string (for repeaters with no password)
* 2. Empty password field = guest login, password field with text = password login
*/
import { describe, it, expect } from 'vitest';
@@ -91,42 +91,40 @@ describe('Repeater message sender parsing', () => {
});
});
describe('Repeater password handling', () => {
describe('Repeater login behavior', () => {
/**
* The "." password convention allows users to specify an empty password
* for repeaters that don't require authentication. Without this, users
* couldn't submit an empty password through the form.
* Repeater login has two modes:
* - Empty password field = guest login (uses repeater's ACL permissions)
* - Password in field = admin login attempt
*/
it('"." converts to empty password', () => {
it('empty input results in empty password (guest login)', () => {
// This is the logic in MessageInput.tsx handleSubmit
const trimmed = '.';
const password = trimmed === '.' ? '' : trimmed;
expect(password).toBe('');
});
it('normal password is passed through unchanged', () => {
const trimmed: string = 'mySecretPassword';
const password = trimmed === '.' ? '' : trimmed;
expect(password).toBe('mySecretPassword');
});
it('"." with surrounding whitespace still works after trim', () => {
// In MessageInput, text.trim() is called before the check
const text = ' . ';
const text = '';
const trimmed = text.trim();
const password = trimmed === '.' ? '' : trimmed;
expect(password).toBe('');
// Empty string is passed directly to onSend
expect(trimmed).toBe('');
});
it('".." is NOT converted (only single dot)', () => {
const trimmed: string = '..';
const password = trimmed === '.' ? '' : trimmed;
it('password is passed through unchanged', () => {
const text = 'mySecretPassword';
const trimmed = text.trim();
// Double dot is passed through as-is (it's a valid password)
expect(password).toBe('..');
expect(trimmed).toBe('mySecretPassword');
});
it('whitespace-only input is treated as empty (guest login)', () => {
const text = ' ';
const trimmed = text.trim();
expect(trimmed).toBe('');
});
it('password with surrounding whitespace is trimmed', () => {
const text = ' secret123 ';
const trimmed = text.trim();
expect(trimmed).toBe('secret123');
});
});