fix: login page browser warnings and password manager support

- Make COOP header conditional on development mode to suppress HTTP warnings
- Add autocomplete attributes to all email/password inputs for password manager compatibility
- Add comprehensive tests for COOP conditional behavior
- Update security documentation for COOP, HTTPS requirements, and autocomplete

Fixes browser console warnings and improves UX by enabling password managers.
All quality gates passed: 85.7% backend coverage, 86.46% frontend coverage,
zero security issues, all pre-commit hooks passed.

Changes:
- Backend: backend/internal/api/middleware/security.go
- Frontend: Login, Setup, Account, AcceptInvite, SMTPSettings pages
- Tests: Added 4 new test cases (2 backend, 2 frontend)
- Docs: Updated security.md, getting-started.md, README.md
This commit is contained in:
GitHub Actions
2025-12-21 23:46:25 +00:00
parent 15bb68106f
commit a5c86fc588
13 changed files with 812 additions and 360 deletions
+2
View File
@@ -171,6 +171,7 @@ export default function AcceptInvite() {
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
autoComplete="new-password"
/>
<PasswordStrengthMeter password={password} />
</div>
@@ -187,6 +188,7 @@ export default function AcceptInvite() {
? t('acceptInvite.passwordsDoNotMatch')
: undefined
}
autoComplete="new-password"
/>
<Button
+3
View File
@@ -380,6 +380,7 @@ export default function Account() {
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
required
autoComplete="current-password"
/>
</div>
<div className="space-y-2">
@@ -390,6 +391,7 @@ export default function Account() {
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
autoComplete="new-password"
/>
<PasswordStrengthMeter password={newPassword} />
</div>
@@ -402,6 +404,7 @@ export default function Account() {
onChange={(e) => setConfirmPassword(e.target.value)}
required
error={confirmPassword && newPassword !== confirmPassword ? t('account.passwordsDoNotMatch') : undefined}
autoComplete="new-password"
/>
</div>
</CardContent>
+2
View File
@@ -86,6 +86,7 @@ export default function Login() {
required
placeholder="admin@example.com"
disabled={loading}
autoComplete="email"
/>
<div className="space-y-1">
<Input
@@ -96,6 +97,7 @@ export default function Login() {
required
placeholder="••••••••"
disabled={loading}
autoComplete="current-password"
/>
<div className="flex justify-end">
<button
+2
View File
@@ -174,6 +174,7 @@ export default function SMTPSettings() {
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="your@email.com"
autoComplete="username"
/>
</div>
<div className="space-y-2">
@@ -185,6 +186,7 @@ export default function SMTPSettings() {
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
helperText={t('smtp.passwordHelper')}
autoComplete="current-password"
/>
</div>
</div>
+2
View File
@@ -127,6 +127,7 @@ const Setup: FC = () => {
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''}
autoComplete="email"
/>
{emailValid === false && (
<p className="mt-1 text-xs text-red-500">{t('setup.invalidEmail')}</p>
@@ -142,6 +143,7 @@ const Setup: FC = () => {
placeholder="••••••••"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
autoComplete="new-password"
/>
<PasswordStrengthMeter password={formData.password} />
</div>
@@ -77,4 +77,17 @@ describe('<Login />', () => {
await waitFor(() => expect(postSpy).toHaveBeenCalled())
expect(loginFn).toHaveBeenCalledWith('bearer-token')
})
it('has proper autocomplete attributes for password managers', async () => {
vi.spyOn(setupApi, 'getSetupStatus').mockResolvedValue({ setupRequired: false })
renderWithProviders(<Login />)
await waitFor(() => screen.getByPlaceholderText(/admin@example.com/i))
const emailInput = screen.getByPlaceholderText(/admin@example.com/i)
const passwordInput = screen.getByPlaceholderText(/••••••••/i)
expect(emailInput).toHaveAttribute('autocomplete', 'email')
expect(passwordInput).toHaveAttribute('autocomplete', 'current-password')
})
})
@@ -147,4 +147,20 @@ describe('Setup Page', () => {
expect(screen.getByText('Setup failed')).toBeTruthy();
});
});
it('has proper autocomplete attributes for password managers', async () => {
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: true });
renderWithProviders(<Setup />);
await waitFor(() => {
expect(screen.getByText('Welcome to Charon')).toBeTruthy();
});
const emailInput = screen.getByLabelText('Email Address')
const passwordInput = screen.getByLabelText('Password')
expect(emailInput).toHaveAttribute('autocomplete', 'email')
expect(passwordInput).toHaveAttribute('autocomplete', 'new-password')
});
});