diff --git a/Gemfile.lock b/Gemfile.lock
index 2f187abb57f..d069e6e7e33 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -205,6 +205,12 @@ PATH
pdf-inspector (~> 1.3.0)
prawn (~> 2.2)
+PATH
+ remote: modules/recaptcha
+ specs:
+ openproject-recaptcha (1.0.0)
+ recaptcha (~> 5.1.0)
+
PATH
remote: modules/reporting_engine
specs:
@@ -716,6 +722,8 @@ GEM
rake (12.3.2)
rbtree (0.4.2)
rdoc (6.1.1)
+ recaptcha (5.1.0)
+ json
redcarpet (3.4.0)
reform (2.2.4)
disposable (>= 0.4.1)
@@ -984,6 +992,7 @@ DEPENDENCIES
openproject-meeting!
openproject-openid_connect!
openproject-pdf_export!
+ openproject-recaptcha!
openproject-reporting!
openproject-token (~> 1.0.1)
openproject-translations!
diff --git a/Gemfile.modules b/Gemfile.modules
index 3e101083205..0fdf6b50373 100644
--- a/Gemfile.modules
+++ b/Gemfile.modules
@@ -37,6 +37,7 @@ group :opf_plugins do
gem 'openproject-webhooks', path: 'modules/webhooks'
gem 'openproject-github_integration', path: 'modules/github_integration'
gem 'openproject-ldap_groups', path: 'modules/ldap_groups'
+ gem 'openproject-recaptcha', path: 'modules/recaptcha'
gem 'grids', path: 'modules/grids'
gem 'my_page', path: 'modules/my_page'
diff --git a/frontend/npm-shrinkwrap.json b/frontend/npm-shrinkwrap.json
index 986612e1d5f..818a66c3ebe 100644
--- a/frontend/npm-shrinkwrap.json
+++ b/frontend/npm-shrinkwrap.json
@@ -1507,7 +1507,7 @@
},
"@types/q": {
"version": "0.0.32",
- "resolved": "http://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
"integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
"dev": true
},
@@ -2204,7 +2204,7 @@
},
"chalk": {
"version": "1.1.3",
- "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
@@ -2570,7 +2570,7 @@
},
"browserify-aes": {
"version": "1.2.0",
- "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"requires": {
"buffer-xor": "^1.0.3",
@@ -2806,7 +2806,7 @@
},
"camelcase-keys": {
"version": "2.1.0",
- "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
"requires": {
"camelcase": "^2.0.0",
@@ -2884,7 +2884,7 @@
"dependencies": {
"color-convert": {
"version": "0.5.3",
- "resolved": "http://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
"integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
}
}
@@ -3115,7 +3115,7 @@
},
"colors": {
"version": "1.1.2",
- "resolved": "http://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
"integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
"dev": true
},
@@ -3400,7 +3400,7 @@
},
"create-hash": {
"version": "1.2.0",
- "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"requires": {
"cipher-base": "^1.0.1",
@@ -3412,7 +3412,7 @@
},
"create-hmac": {
"version": "1.1.7",
- "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"requires": {
"cipher-base": "^1.0.3",
@@ -3520,7 +3520,7 @@
},
"d": {
"version": "0.1.1",
- "resolved": "http://registry.npmjs.org/d/-/d-0.1.1.tgz",
+ "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz",
"integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=",
"requires": {
"es5-ext": "~0.10.2"
@@ -3784,7 +3784,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
- "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"requires": {
"bn.js": "^4.1.0",
@@ -4001,7 +4001,7 @@
},
"engine.io-client": {
"version": "3.2.1",
- "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
"integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
"dev": true,
"requires": {
@@ -4124,7 +4124,7 @@
"dependencies": {
"d": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"requires": {
"es5-ext": "^0.10.9"
@@ -4139,7 +4139,7 @@
},
"es6-promisify": {
"version": "5.0.0",
- "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
@@ -4161,7 +4161,7 @@
"dependencies": {
"d": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"requires": {
"es5-ext": "^0.10.9"
@@ -4259,7 +4259,7 @@
"dependencies": {
"d": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"requires": {
"es5-ext": "^0.10.9"
@@ -5123,7 +5123,7 @@
"dependencies": {
"async": {
"version": "1.5.0",
- "resolved": "http://registry.npmjs.org/async/-/async-1.5.0.tgz",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz",
"integrity": "sha1-J5ZkJyNXOFlWVjP8YnRES+4vjOM="
}
}
@@ -5939,7 +5939,7 @@
},
"fast-deep-equal": {
"version": "1.1.0",
- "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
},
"json-schema-traverse": {
@@ -6088,13 +6088,13 @@
"dependencies": {
"commander": {
"version": "0.6.1",
- "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
"integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=",
"dev": true
},
"mkdirp": {
"version": "0.3.5",
- "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
}
@@ -6200,7 +6200,7 @@
},
"jsesc": {
"version": "1.3.0",
- "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
"integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s="
},
"json-parse-better-errors": {
@@ -6595,7 +6595,7 @@
},
"load-json-file": {
"version": "1.1.0",
- "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"requires": {
"graceful-fs": "^4.1.2",
@@ -6607,7 +6607,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
- "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
}
}
@@ -6873,7 +6873,7 @@
},
"media-typer": {
"version": "0.3.0",
- "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"mem": {
@@ -6902,7 +6902,7 @@
"dependencies": {
"next-tick": {
"version": "0.2.2",
- "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz",
"integrity": "sha1-ddpKkn7liH45BliABltzNkE7MQ0="
}
}
@@ -6918,7 +6918,7 @@
},
"meow": {
"version": "3.7.0",
- "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"requires": {
"camelcase-keys": "^2.0.0",
@@ -7191,7 +7191,7 @@
},
"mkdirp": {
"version": "0.5.1",
- "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -7221,7 +7221,7 @@
"dependencies": {
"readdirp": {
"version": "0.2.5",
- "resolved": "http://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz",
"integrity": "sha1-xMJ25Sl3riXbUZH+UdAIVQ8V2bs=",
"dev": true,
"requires": {
@@ -7307,7 +7307,7 @@
},
"next-tick": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"ng-dynamic-component": {
@@ -7378,7 +7378,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
- "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
}
}
@@ -7429,7 +7429,7 @@
"dependencies": {
"semver": {
"version": "5.3.0",
- "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
},
"tar": {
@@ -7520,7 +7520,7 @@
},
"chalk": {
"version": "1.1.3",
- "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "^2.2.1",
@@ -7876,7 +7876,7 @@
},
"os-homedir": {
"version": "1.0.2",
- "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-locale": {
@@ -7891,7 +7891,7 @@
},
"os-tmpdir": {
"version": "1.0.2",
- "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
@@ -8129,7 +8129,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
- "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
@@ -8381,7 +8381,7 @@
},
"chalk": {
"version": "1.1.3",
- "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@@ -8409,7 +8409,7 @@
},
"globby": {
"version": "5.0.0",
- "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
"dev": true,
"requires": {
@@ -8429,7 +8429,7 @@
},
"pify": {
"version": "2.3.0",
- "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
@@ -8692,7 +8692,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
- "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
}
}
@@ -8741,7 +8741,7 @@
},
"pify": {
"version": "2.3.0",
- "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
}
}
@@ -8776,7 +8776,7 @@
},
"readable-stream": {
"version": "2.3.6",
- "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -9049,7 +9049,7 @@
},
"safe-regex": {
"version": "1.1.0",
- "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"requires": {
"ret": "~0.1.10"
@@ -9233,7 +9233,7 @@
"dependencies": {
"source-map": {
"version": "0.4.4",
- "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
"requires": {
"amdefine": ">=0.0.4"
@@ -9360,7 +9360,7 @@
},
"serialize-error": {
"version": "2.1.0",
- "resolved": "http://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz",
"integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go="
},
"serialize-javascript": {
@@ -9437,7 +9437,7 @@
},
"sha.js": {
"version": "2.4.11",
- "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
@@ -9667,7 +9667,7 @@
},
"socket.io-parser": {
"version": "3.2.0",
- "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
"integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
"dev": true,
"requires": {
@@ -10109,7 +10109,7 @@
},
"string_decoder": {
"version": "1.1.1",
- "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
@@ -10117,7 +10117,7 @@
},
"strip-ansi": {
"version": "3.0.1",
- "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
@@ -10133,7 +10133,7 @@
},
"strip-eof": {
"version": "1.0.0",
- "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-indent": {
@@ -10314,7 +10314,7 @@
},
"through": {
"version": "2.3.8",
- "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"through2": {
@@ -10452,7 +10452,7 @@
},
"promise": {
"version": "2.0.0",
- "resolved": "http://registry.npmjs.org/promise/-/promise-2.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz",
"integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=",
"dev": true,
"requires": {
@@ -12431,7 +12431,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
- "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"requires": {
"string-width": "^1.0.1",
@@ -12494,7 +12494,7 @@
},
"xmlbuilder": {
"version": "9.0.7",
- "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
"dev": true
},
diff --git a/modules/recaptcha/Gemfile b/modules/recaptcha/Gemfile
new file mode 100644
index 00000000000..851fabc21dd
--- /dev/null
+++ b/modules/recaptcha/Gemfile
@@ -0,0 +1,2 @@
+source 'https://rubygems.org'
+gemspec
diff --git a/modules/recaptcha/app/controllers/recaptcha/admin_controller.rb b/modules/recaptcha/app/controllers/recaptcha/admin_controller.rb
new file mode 100644
index 00000000000..302a5b201fe
--- /dev/null
+++ b/modules/recaptcha/app/controllers/recaptcha/admin_controller.rb
@@ -0,0 +1,38 @@
+module ::Recaptcha
+ class AdminController < ApplicationController
+ include ::RecaptchaHelper
+
+ before_action :require_admin
+ before_action :validate_settings, only: :update
+ layout 'admin'
+
+ menu_item :plugin_recaptcha
+
+ def show; end
+
+ def update
+ Setting.plugin_openproject_recaptcha = @settings
+ flash[:notice] = I18n.t(:notice_successful_update)
+ redirect_to action: :show
+ end
+
+ private
+
+ def validate_settings
+ new_params = permitted_params
+ allowed_options = recaptcha_available_options.map(&:last)
+
+ unless allowed_options.include? new_params[:recaptcha_type]
+ flash[:error] = I18n.t(:error_code, code: '400')
+ redirect_to action: :show
+ return
+ end
+
+ @settings = new_params.to_h.symbolize_keys
+ end
+
+ def permitted_params
+ params.permit(:recaptcha_type, :website_key, :secret_key)
+ end
+ end
+end
diff --git a/modules/recaptcha/app/controllers/recaptcha/request_controller.rb b/modules/recaptcha/app/controllers/recaptcha/request_controller.rb
new file mode 100644
index 00000000000..1e0f84c60b4
--- /dev/null
+++ b/modules/recaptcha/app/controllers/recaptcha/request_controller.rb
@@ -0,0 +1,111 @@
+require 'recaptcha'
+
+module ::Recaptcha
+ class RequestController < ApplicationController
+ # Include global layout helper
+ layout 'no_menu'
+
+ # User is not yet logged in, so skip login required check
+ skip_before_action :check_if_login_required
+
+ # Skip if recaptcha was disabled
+ before_action :skip_if_disabled
+
+ # Require authenticated user from the core to be present
+ before_action :require_authenticated_user
+
+ # Skip if user has confirmed already
+ before_action :skip_if_user_verified
+
+ ##
+ # Request verification form
+ def perform
+ use_content_security_policy_named_append(:recaptcha)
+ end
+
+ def verify
+ if valid_recaptcha?
+ save_recpatcha_verification_success!
+ complete_stage_redirect
+ else
+ fail_recaptcha I18n.t('recaptcha.error_captcha')
+ end
+ end
+
+ private
+
+ ##
+ # Insert that the account was verified
+ def save_recpatcha_verification_success!
+ # Remove all previous
+ ::Recaptcha::Entry.where(user_id: @authenticated_user.id).delete_all
+ ::Recaptcha::Entry.create!(user_id: @authenticated_user.id, version: recaptcha_version)
+ end
+
+ def recaptcha_version
+ case recaptcha_settings[:recaptcha_type]
+ when ::OpenProject::Recaptcha::TYPE_DISABLED
+ 0
+ when ::OpenProject::Recaptcha::TYPE_V2
+ 2
+ when ::OpenProject::Recaptcha::TYPE_V3
+ 3
+ end
+ end
+
+ ##
+ #
+ def valid_recaptcha?
+ call_args = { secret_key: recaptcha_settings[:secret_key] }
+ if recaptcha_version == 3
+ call_args[:action] = 'login'
+ end
+
+ verify_recaptcha call_args
+ end
+
+ ##
+ # fail the recaptcha
+ def fail_recaptcha(msg)
+ flash[:error] = msg
+ failure_stage_redirect
+ end
+
+ ##
+ # Ensure the authentication stage from the core provided the authenticated user
+ def require_authenticated_user
+ @authenticated_user = User.find(session[:authenticated_user_id])
+ rescue ActiveRecord::RecordNotFound
+ Rails.logger.error "Failed to find authenticated_user for recaptcha verify."
+ failure_stage_redirect
+ end
+
+ def recaptcha_settings
+ Setting.plugin_openproject_recaptcha
+ end
+
+ def skip_if_disabled
+ if recaptcha_settings[:recaptcha_type] == ::OpenProject::Recaptcha::TYPE_DISABLED
+ complete_stage_redirect
+ end
+ end
+
+ def skip_if_user_verified
+ if ::Recaptcha::Entry.where(user_id: @authenticated_user.id).exists?
+ Rails.logger.debug { "User #{@authenticated_user.id} already provided recaptcha. Skipping. " }
+ complete_stage_redirect
+ end
+ end
+
+ ##
+ # Complete this authentication step and return to core
+ # logging in the user
+ def complete_stage_redirect
+ redirect_to authentication_stage_complete_path :recaptcha
+ end
+
+ def failure_stage_redirect
+ redirect_to authentication_stage_failure_path :recaptcha
+ end
+ end
+end
diff --git a/modules/recaptcha/app/helpers/recaptcha_helper.rb b/modules/recaptcha/app/helpers/recaptcha_helper.rb
new file mode 100644
index 00000000000..8c8aa2e2bc0
--- /dev/null
+++ b/modules/recaptcha/app/helpers/recaptcha_helper.rb
@@ -0,0 +1,13 @@
+module RecaptchaHelper
+ def recaptcha_available_options
+ [
+ [I18n.t('recaptcha.settings.type_disabled'), ::OpenProject::Recaptcha::TYPE_DISABLED],
+ [I18n.t('recaptcha.settings.type_v2'), ::OpenProject::Recaptcha::TYPE_V2],
+ [I18n.t('recaptcha.settings.type_v3'), ::OpenProject::Recaptcha::TYPE_V3]
+ ]
+ end
+
+ def recaptcha_settings
+ Setting.plugin_openproject_recaptcha
+ end
+end
diff --git a/modules/recaptcha/app/models/recaptcha/entry.rb b/modules/recaptcha/app/models/recaptcha/entry.rb
new file mode 100644
index 00000000000..3af1e68c1ef
--- /dev/null
+++ b/modules/recaptcha/app/models/recaptcha/entry.rb
@@ -0,0 +1,6 @@
+module Recaptcha
+ class Entry < ::ApplicationRecord
+ self.table_name_prefix = 'recaptcha_'
+ belongs_to :user
+ end
+end
diff --git a/modules/recaptcha/app/views/recaptcha/admin/show.html.erb b/modules/recaptcha/app/views/recaptcha/admin/show.html.erb
new file mode 100644
index 00000000000..77079db7b59
--- /dev/null
+++ b/modules/recaptcha/app/views/recaptcha/admin/show.html.erb
@@ -0,0 +1,46 @@
+<% html_title(t(:label_administration), t('recaptcha.label_recaptcha')) -%>
+
+<%= breadcrumb_toolbar t('recaptcha.label_recaptcha') %>
+
+
<%= t('recaptcha.button_please_wait') %>
+ <%= nonced_javascript_tag do %> + function submitRecaptchaForm(id, val) { + document.getElementById(id).value = val; + document.getElementById('submit_captcha').submit(); + } + <% end %> + <% end %> + <% end %> +