From 12c43635a54e266adba3a4b11d77e75fdf86a7fd Mon Sep 17 00:00:00 2001 From: tmphost-dev Date: Sat, 24 Oct 2026 02:46:56 +0000 Subject: [PATCH] Keep browsers from autofilling the database form The DSN was a password field, and browsers ignore autocomplete="off" on one: Chrome offered saved logins and filled the name as the username. The DSN is now a text field masked with -webkit-text-security, the form and both fields say autocomplete="off", and both carry the attributes 1Password, LastPass, Bitwarden and Dashlane read as "not a login". Co-Authored-By: Claude Opus 5.5 --- frontend/src/views/DatabaseFormView.vue | 19 ++++++++++++++----- frontend/test/DatabaseFormView.test.js | 22 ++++++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/DatabaseFormView.vue b/frontend/src/views/DatabaseFormView.vue index 7829d2e..c9bedb6 100644 --- a/frontend/src/views/DatabaseFormView.vue +++ b/frontend/src/views/DatabaseFormView.vue @@ -16,11 +16,14 @@ {{ loadError }} -
+ +
- +

Shown in the sidebar and used as the MCP server name.

@@ -55,9 +58,12 @@
- + + @@ -105,6 +111,9 @@ import { useDatabaseStore } from '../stores/databaseStore' import { DSN_FORMATS, DSN_PLACEHOLDERS, FALLBACK_DRIVERS } from '../utils/drivers' import { BTN_DANGER, BTN_GHOST, BTN_PRIMARY, BTN_SECONDARY, CARD, HINT, INPUT, LABEL, PAGE_TITLE } from '../ui' +// What 1Password, LastPass, Bitwarden and Dashlane read as "not a login field". +const NO_AUTOFILL = { 'data-1p-ignore': '', 'data-lpignore': 'true', 'data-bwignore': '', 'data-form-type': 'other' } + const route = useRoute() const router = useRouter() const store = useDatabaseStore() diff --git a/frontend/test/DatabaseFormView.test.js b/frontend/test/DatabaseFormView.test.js index a60e4dd..a53a17e 100644 --- a/frontend/test/DatabaseFormView.test.js +++ b/frontend/test/DatabaseFormView.test.js @@ -53,14 +53,32 @@ describe('DatabaseFormView', () => { expect(JSON.parse(f.mock.calls.at(-1)[1].body)).toEqual({ database: { name: 'shop', driver: 'mysql', dsn: 'app:p@tcp(db:3306)/shop' } }) }) + it('keeps browsers and password managers from filling the form', async () => { + mockFetch({ 'GET /drivers': DRIVERS }) + const { wrapper } = await mountAt(DatabaseFormView, { path: '/databases/new' }) + expect(wrapper.find('form').attributes('autocomplete')).toBe('off') + expect(wrapper.find('input[type="password"]').exists()).toBe(false) + for (const id of ['#db-name', '#db-dsn']) { + const input = wrapper.find(id) + expect(input.attributes('autocomplete')).toBe('off') + expect(input.attributes('data-1p-ignore')).toBeDefined() + expect(input.attributes('data-lpignore')).toBe('true') + expect(input.attributes('data-bwignore')).toBeDefined() + expect(input.attributes('data-form-type')).toBe('other') + } + }) + it('toggles DSN visibility and tests the connection', async () => { const f = mockFetch({ 'GET /drivers': DRIVERS, 'POST /databases/test': { ok: true, serverVersion: 'PostgreSQL 17.2' } }) const { wrapper } = await mountAt(DatabaseFormView, { path: '/databases/new' }) const dsn = wrapper.find('#db-dsn') - expect(dsn.attributes('type')).toBe('password') + expect(dsn.attributes('type')).toBe('text') + expect(dsn.attributes('data-masked')).toBe('true') + expect(dsn.classes()).toContain('[-webkit-text-security:disc]') expect(dsn.attributes('placeholder')).toBe('postgres://user:pass@host:5432/db?sslmode=require') await wrapper.find('[data-test="toggle-dsn"]').trigger('click') - expect(dsn.attributes('type')).toBe('text') + expect(dsn.attributes('data-masked')).toBe('false') + expect(dsn.classes()).not.toContain('[-webkit-text-security:disc]') expect(wrapper.find('[data-test="test-connection"]').attributes('disabled')).toBeDefined() await dsn.setValue('postgres://u:p@h/db') await wrapper.find('[data-test="test-connection"]').trigger('click')