diff --git a/js/twofactor_totp-main-challenge.js b/js/twofactor_totp-main-challenge.js new file mode 100644 index 000000000..b87ed7956 --- /dev/null +++ b/js/twofactor_totp-main-challenge.js @@ -0,0 +1,2 @@ +(()=>{"use strict";const e=document.querySelector('.totp-form input[name="challenge"]');e&&function(e){e.addEventListener("input",()=>{const t=e.value.replace(/\D/g,"");t!==e.value&&(e.value=t)})}(e)})(); +//# sourceMappingURL=twofactor_totp-main-challenge.js.map?v=85242c9a4e4d172c7b4f \ No newline at end of file diff --git a/js/twofactor_totp-main-challenge.js.license b/js/twofactor_totp-main-challenge.js.license new file mode 100644 index 000000000..02786e352 --- /dev/null +++ b/js/twofactor_totp-main-challenge.js.license @@ -0,0 +1,8 @@ +SPDX-License-Identifier: AGPL-3.0-or-later +SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors + + +This file is generated from multiple sources. Included packages: +- nextcloud + - version: 1.0.0 + - license: AGPL-3.0-or-later diff --git a/js/twofactor_totp-main-challenge.js.map b/js/twofactor_totp-main-challenge.js.map new file mode 100644 index 000000000..02163b75b --- /dev/null +++ b/js/twofactor_totp-main-challenge.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twofactor_totp-main-challenge.js?v=85242c9a4e4d172c7b4f","mappings":"mBAOA,MAAMA,EAAQC,SAASC,cAAc,sCACjCF,GCYG,SAA0BA,GAChCA,EAAMG,iBAAiB,QAAS,KAC/B,MAAMC,EAAyBJ,EAAMK,MAVzBC,QAAQ,MAAO,IAWvBF,IAAcJ,EAAMK,QACvBL,EAAMK,MAAQD,IAGjB,CDlBCG,CAAiBP,E","sources":["webpack:///twofactor_totp/src/main-challenge.js","webpack:///twofactor_totp/src/challenge.js"],"sourcesContent":["/*\n * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\nimport { restrictToDigits } from './challenge.js'\n\nconst input = document.querySelector('.totp-form input[name=\"challenge\"]')\nif (input) {\n\trestrictToDigits(input)\n}\n","/*\n * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\n/**\n * Remove every non-digit character from a TOTP code\n *\n * @param {string} value the raw input value\n * @return {string}\n */\nexport function sanitizeCode(value) {\n\treturn value.replace(/\\D/g, '')\n}\n\n/**\n * Keep only digits in the given input while the user types or pastes\n *\n * @param {HTMLInputElement} input the TOTP code input\n */\nexport function restrictToDigits(input) {\n\tinput.addEventListener('input', () => {\n\t\tconst sanitized = sanitizeCode(input.value)\n\t\tif (sanitized !== input.value) {\n\t\t\tinput.value = sanitized\n\t\t}\n\t})\n}\n"],"names":["input","document","querySelector","addEventListener","sanitized","value","replace","restrictToDigits"],"sourceRoot":""} \ No newline at end of file diff --git a/js/twofactor_totp-main-challenge.js.map.license b/js/twofactor_totp-main-challenge.js.map.license new file mode 120000 index 000000000..a6600d8c4 --- /dev/null +++ b/js/twofactor_totp-main-challenge.js.map.license @@ -0,0 +1 @@ +twofactor_totp-main-challenge.js.license \ No newline at end of file diff --git a/src/challenge.js b/src/challenge.js new file mode 100644 index 000000000..e30d945eb --- /dev/null +++ b/src/challenge.js @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Remove every non-digit character from a TOTP code + * + * @param {string} value the raw input value + * @return {string} + */ +export function sanitizeCode(value) { + return value.replace(/\D/g, '') +} + +/** + * Keep only digits in the given input while the user types or pastes + * + * @param {HTMLInputElement} input the TOTP code input + */ +export function restrictToDigits(input) { + input.addEventListener('input', () => { + const sanitized = sanitizeCode(input.value) + if (sanitized !== input.value) { + input.value = sanitized + } + }) +} diff --git a/src/main-challenge.js b/src/main-challenge.js new file mode 100644 index 000000000..9880e78d5 --- /dev/null +++ b/src/main-challenge.js @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { restrictToDigits } from './challenge.js' + +const input = document.querySelector('.totp-form input[name="challenge"]') +if (input) { + restrictToDigits(input) +} diff --git a/src/tests/challenge.spec.js b/src/tests/challenge.spec.js new file mode 100644 index 000000000..454ed6bd9 --- /dev/null +++ b/src/tests/challenge.spec.js @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { restrictToDigits, sanitizeCode } from '../challenge.js' + +describe('sanitizeCode', () => { + it.each([ + ['123456', '123456'], + ['123 456', '123456'], + ['12-34-56', '123456'], + ['abc', ''], + ['', ''], + ])('turns %p into %p', (value, expected) => { + expect(sanitizeCode(value)).to.equal(expected) + }) +}) + +describe('restrictToDigits', () => { + it('strips non-digits on input', () => { + const input = document.createElement('input') + restrictToDigits(input) + + input.value = '12a3 4' + input.dispatchEvent(new Event('input')) + + expect(input.value).to.equal('1234') + }) + + it('leaves a digit-only value untouched', () => { + const input = document.createElement('input') + restrictToDigits(input) + + input.value = '123456' + input.dispatchEvent(new Event('input')) + + expect(input.value).to.equal('123456') + }) +}) diff --git a/templates/challenge.php b/templates/challenge.php index 0b3d95323..e887e429a 100644 --- a/templates/challenge.php +++ b/templates/challenge.php @@ -6,6 +6,7 @@ */ style('twofactor_totp', 'style'); +script('twofactor_totp', 'twofactor_totp-main-challenge'); ?> @@ -13,7 +14,7 @@

t('Get the authentication code from the two-factor authentication app on your device.')) ?>

- + diff --git a/webpack.config.js b/webpack.config.js index 5c12b54c1..0d0cf71a4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,7 @@ const isDev = process.env.NODE_ENV === 'development' webpackConfig.entry = { 'main-settings': path.join(__dirname, 'src', 'main-settings.js'), 'main-login-setup': path.join(__dirname, 'src', 'main-login-setup.js'), + 'main-challenge': path.join(__dirname, 'src', 'main-challenge.js'), } // Generate reuse license files if not in development mode