Compare commits

...

2 Commits

Author SHA1 Message Date
910ab9522b feat(altcha): add altcha validation layer to login
Some checks reported warnings
Cadoles/hydra-sql/pipeline/head This commit is unstable
2025-03-26 16:12:31 +01:00
1cb5ae6bc3 feat(altcha): add altcha and update dependencies
Some checks failed
Cadoles/hydra-sql/pipeline/head There was a failure building this commit
2025-03-24 13:51:37 +01:00
35 changed files with 4840 additions and 1989 deletions

9
.env
View File

@ -41,3 +41,12 @@ LOCK_DSN=flock
SENTRY_DSN=
###< sentry/sentry-symfony ###
REDIS_DSN=redis://redis:6379
### Altcha
ALTCHA_HOST='http://altcha:3333'
ALTCHA_BASE_URL=''
ALTCHA_DEBUG=false
ALTCHA_WORKERS=8
ALTCHA_DELAY=100
ALTCHA_MOCK_ERROR=false
ALTCHA_ENABLED=true

View File

@ -8,5 +8,4 @@
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';
// start the Stimulus application
import './bootstrap';
require('altcha');

11
assets/bootstrap.js vendored
View File

@ -1,11 +0,0 @@
import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.[jt]sx?$/
));
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

View File

@ -1,4 +0,0 @@
{
"controllers": [],
"entrypoints": []
}

View File

@ -1,16 +0,0 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

View File

@ -5,6 +5,7 @@ twig:
default_path: "%kernel.project_dir%/templates"
form_themes:
- "bootstrap_5_layout.html.twig"
- "altcha.html.twig"
when@test:
twig:
strict_variables: true

View File

@ -2,6 +2,10 @@ controllers:
resource: ../../src/Controller/
type: annotation
controllers_flag:
resource: ../../src/Flag/Controller/
type: annotation
kernel:
resource: ../../src/Kernel.php
type: annotation

View File

@ -75,3 +75,28 @@ services:
$securityPattern: '%security_pattern%'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Altcha\Form\AltchaType:
arguments:
$altchaHost: "%env(ALTCHA_HOST)%"
$altchaBaseUrl: "%env(ALTCHA_BASE_URL)%"
$altchaDebug: "%env(ALTCHA_DEBUG)%"
$altchaWorkers: "%env(ALTCHA_WORKERS)%"
$altchaDelay: "%env(ALTCHA_DELAY)%"
$altchaMockError: "%env(ALTCHA_MOCK_ERROR)%"
tags:
- { name: form.type, alias: altcha}
App\Form\LoginType:
arguments:
$altchaEnabled: "%env(ALTCHA_ENABLED)%"
App\Altcha\AltchaValidator:
arguments:
$altchaHost: "%env(ALTCHA_HOST)%"
$altchaBaseUrl: "%env(ALTCHA_BASE_URL)%"
Predis\ClientInterface:
class: 'Predis\Client'
arguments:
- 'tcp://redis:6379'

View File

@ -21,6 +21,8 @@ services:
- /tmp
links:
- hydra
depends_on:
- redis
extra_hosts:
- "localhost:127.0.0.1"
- "localhost:host-gateway"
@ -134,6 +136,11 @@ services:
- TZ=Europe/Paris
volumes:
- /etc/localtime:/etc/localtime:ro
altcha:
image: reg.cadoles.com/cadoles/altcha:2024.10.29-develop.1213.22e038b
environment:
ALTCHA_HMAC_KEY: 'change_me'
volumes:
postgres:
mariadb:

6283
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,12 @@
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@hotwired/stimulus": "^3.0.0",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/webpack-encore": "^4.1.2",
"core-js": "^3.23.0",
"regenerator-runtime": "^0.13.9",
"webpack": "^5.91.0",
"webpack-cli": "^4.10.0",
"file-loader": "^6.2.0",
"webpack-notifier": "^1.15.0"
},
"license": "UNLICENSED",
@ -26,6 +25,7 @@
"jquery": "^3.6.1",
"postcss-loader": "^7.0.2",
"sass": "^1.56.2",
"sass-loader": "^13.2.0"
"sass-loader": "^13.2.0",
"altcha": "^1.0.0"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,30 +0,0 @@
/*!
* Bootstrap v5.2.3 (https://getbootstrap.com/)
* Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
/*!
* Sizzle CSS Selector Engine v2.3.9
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2022-12-19
*/
/*!
* jQuery JavaScript Library v3.6.3
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2022-12-20T21:28Z
*/

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(self.webpackChunk=self.webpackChunk||[]).push([[143],{4180:(t,e,r)=>{var n={"./hello_controller.js":4695};function o(t){var e=i(t);return r(e)}function i(t){if(!r.o(n,t)){var e=new Error("Cannot find module '"+t+"'");throw e.code="MODULE_NOT_FOUND",e}return n[t]}o.keys=function(){return Object.keys(n)},o.resolve=i,t.exports=o,o.id=4180},8205:(t,e,r)=>{"use strict";r.d(e,{Z:()=>n});const n={}},4695:(t,e,r)=>{"use strict";r.r(e),r.d(e,{default:()=>s});r(8304),r(4812),r(489),r(1539),r(2419),r(8011),r(9070),r(6649),r(6078),r(2526),r(1817),r(9653),r(2165),r(6992),r(8783),r(3948);function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(t)}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var r=0;r<e.length;r++){var o=e[r];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,(i=o.key,u=void 0,u=function(t,e){if("object"!==n(t)||null===t)return t;var r=t[Symbol.toPrimitive];if(void 0!==r){var o=r.call(t,e||"default");if("object"!==n(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(i,"string"),"symbol"===n(u)?u:String(u)),o)}var i,u}function u(t,e){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},u(t,e)}function c(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var r,n=l(t);if(e){var o=l(this).constructor;r=Reflect.construct(n,arguments,o)}else r=n.apply(this,arguments);return f(this,r)}}function f(t,e){if(e&&("object"===n(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t)}function l(t){return l=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},l(t)}var s=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&u(t,e)}(l,t);var e,r,n,f=c(l);function l(){return o(this,l),f.apply(this,arguments)}return e=l,(r=[{key:"connect",value:function(){this.element.textContent="Hello Stimulus! Edit me in assets/controllers/hello_controller.js"}}])&&i(e.prototype,r),n&&i(e,n),Object.defineProperty(e,"prototype",{writable:!1}),l}(r(6599).Qr)},9437:(t,e,r)=>{"use strict";(0,r(2192).x)(r(4180))}},t=>{t.O(0,[3],(()=>{return e=9437,t(t.s=e);var e}));t.O()}]);
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[524],{28:(e,s,u)=>{u(376)}},e=>{e.O(0,[376],(()=>{return s=28,e(e.s=s);var s}));e.O()}]);

View File

@ -1 +1 @@
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[113],{9218:()=>{}},s=>{var e;e=9218,s(s.s=e)}]);
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[505],{843:()=>{}},s=>{var e;e=843,s(s.s=e)}]);

View File

@ -1 +1 @@
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[144],{2576:(e,s,u)=>{u(3138)}},e=>{e.O(0,[138],(()=>{return s=2576,e(e.s=s);var s}));e.O()}]);
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[513],{675:(e,s,u)=>{u(336)}},e=>{e.O(0,[336],(()=>{return s=675,e(e.s=s);var s}));e.O()}]);

View File

@ -3,7 +3,7 @@
"app": {
"js": [
"/build/app/runtime.js",
"/build/app/3.js",
"/build/app/376.js",
"/build/app/app.js"
],
"css": [
@ -22,7 +22,7 @@
"bootstrap-js": {
"js": [
"/build/app/runtime.js",
"/build/app/138.js",
"/build/app/336.js",
"/build/app/bootstrap-js.js"
]
}

View File

@ -5,6 +5,27 @@
"build/app/bootstrap-css.js": "/build/app/bootstrap-css.js",
"build/app/bootstrap-js.js": "/build/app/bootstrap-js.js",
"build/app/runtime.js": "/build/app/runtime.js",
"build/app/3.js": "/build/app/3.js",
"build/app/138.js": "/build/app/138.js"
"build/app/336.js": "/build/app/336.js",
"build/app/376.js": "/build/app/376.js",
"build/app/altcha/dist/altcha.umd.cjs": "/build/app/altcha/dist/altcha.umd.cjs",
"build/app/altcha/dist/altcha.js": "/build/app/altcha/dist/altcha.js",
"build/app/altcha/dist_external/altcha.umd.cjs": "/build/app/altcha/dist_external/altcha.umd.cjs",
"build/app/altcha/dist_external/altcha.js": "/build/app/altcha/dist_external/altcha.js",
"build/app/altcha/README.md": "/build/app/altcha/README.md",
"build/app/altcha/dist_plugins/upload.umd.cjs": "/build/app/altcha/dist_plugins/upload.umd.cjs",
"build/app/altcha/dist_plugins/upload.js": "/build/app/altcha/dist_plugins/upload.js",
"build/app/altcha/dist/altcha.d.ts": "/build/app/altcha/dist/altcha.d.ts",
"build/app/altcha/dist_external/altcha.d.ts": "/build/app/altcha/dist_external/altcha.d.ts",
"build/app/altcha/dist_plugins/analytics.umd.cjs": "/build/app/altcha/dist_plugins/analytics.umd.cjs",
"build/app/altcha/dist_plugins/analytics.js": "/build/app/altcha/dist_plugins/analytics.js",
"build/app/altcha/package.json": "/build/app/altcha/package.json",
"build/app/altcha/dist_plugins/obfuscation.umd.cjs": "/build/app/altcha/dist_plugins/obfuscation.umd.cjs",
"build/app/altcha/dist_plugins/obfuscation.js": "/build/app/altcha/dist_plugins/obfuscation.js",
"build/app/altcha/dist_external/altcha.css": "/build/app/altcha/dist_external/altcha.css",
"build/app/altcha/dist_external/worker.js": "/build/app/altcha/dist_external/worker.js",
"build/app/altcha/LICENSE.txt": "/build/app/altcha/LICENSE.txt",
"build/app/altcha/postinstall.js": "/build/app/altcha/postinstall.js",
"build/app/altcha/dist_plugins/obfuscation.d.ts": "/build/app/altcha/dist_plugins/obfuscation.d.ts",
"build/app/altcha/dist_plugins/analytics.d.ts": "/build/app/altcha/dist_plugins/analytics.d.ts",
"build/app/altcha/dist_plugins/upload.d.ts": "/build/app/altcha/dist_plugins/upload.d.ts"
}

View File

@ -1 +1 @@
(()=>{"use strict";var e,r={},t={};function o(e){var n=t[e];if(void 0!==n)return n.exports;var i=t[e]={exports:{}};return r[e].call(i.exports,i,i.exports,o),i.exports}o.m=r,e=[],o.O=(r,t,n,i)=>{if(!t){var l=1/0;for(s=0;s<e.length;s++){for(var[t,n,i]=e[s],a=!0,u=0;u<t.length;u++)(!1&i||l>=i)&&Object.keys(o.O).every((e=>o.O[e](t[u])))?t.splice(u--,1):(a=!1,i<l&&(l=i));if(a){e.splice(s--,1);var f=n();void 0!==f&&(r=f)}}return r}i=i||0;for(var s=e.length;s>0&&e[s-1][2]>i;s--)e[s]=e[s-1];e[s]=[t,n,i]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e={666:0};o.O.j=r=>0===e[r];var r=(r,t)=>{var n,i,[l,a,u]=t,f=0;if(l.some((r=>0!==e[r]))){for(n in a)o.o(a,n)&&(o.m[n]=a[n]);if(u)var s=u(o)}for(r&&r(t);f<l.length;f++)i=l[f],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(s)},t=self.webpackChunk=self.webpackChunk||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})()})();
(()=>{"use strict";var e,r={},o={};function t(e){var n=o[e];if(void 0!==n)return n.exports;var l=o[e]={exports:{}};return r[e].call(l.exports,l,l.exports,t),l.exports}t.m=r,e=[],t.O=(r,o,n,l)=>{if(!o){var a=1/0;for(u=0;u<e.length;u++){for(var[o,n,l]=e[u],i=!0,p=0;p<o.length;p++)(!1&l||a>=l)&&Object.keys(t.O).every((e=>t.O[e](o[p])))?o.splice(p--,1):(i=!1,l<a&&(a=l));if(i){e.splice(u--,1);var f=n();void 0!==f&&(r=f)}}return r}l=l||0;for(var u=e.length;u>0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[o,n,l]},t.d=(e,r)=>{for(var o in r)t.o(r,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.p="/build/app/",(()=>{var e={121:0};t.O.j=r=>0===e[r];var r=(r,o)=>{var n,l,[a,i,p]=o,f=0;if(a.some((r=>0!==e[r]))){for(n in i)t.o(i,n)&&(t.m[n]=i[n]);if(p)var u=p(t)}for(r&&r(o);f<a.length;f++)l=a[f],t.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return t.O(u)},o=self.webpackChunk=self.webpackChunk||[];o.forEach(r.bind(null,0)),o.push=r.bind(null,o.push.bind(o))})()})();

View File

@ -1 +1 @@
(()=>{"use strict";var e,r={},o={};function t(e){var n=o[e];if(void 0!==n)return n.exports;var l=o[e]={exports:{}};return r[e](l,l.exports,t),l.exports}t.m=r,e=[],t.O=(r,o,n,l)=>{if(!o){var a=1/0;for(p=0;p<e.length;p++){for(var[o,n,l]=e[p],i=!0,f=0;f<o.length;f++)(!1&l||a>=l)&&Object.keys(t.O).every((e=>t.O[e](o[f])))?o.splice(f--,1):(i=!1,l<a&&(a=l));if(i){e.splice(p--,1);var u=n();void 0!==u&&(r=u)}}return r}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[o,n,l]},t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e={666:0};t.O.j=r=>0===e[r];var r=(r,o)=>{var n,l,[a,i,f]=o,u=0;if(a.some((r=>0!==e[r]))){for(n in i)t.o(i,n)&&(t.m[n]=i[n]);if(f)var p=f(t)}for(r&&r(o);u<a.length;u++)l=a[u],t.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return t.O(p)},o=self.webpackChunk=self.webpackChunk||[];o.forEach(r.bind(null,0)),o.push=r.bind(null,o.push.bind(o))})()})();
(()=>{"use strict";var e,r={},o={};function t(e){var n=o[e];if(void 0!==n)return n.exports;var l=o[e]={exports:{}};return r[e](l,l.exports,t),l.exports}t.m=r,e=[],t.O=(r,o,n,l)=>{if(!o){var a=1/0;for(p=0;p<e.length;p++){for(var[o,n,l]=e[p],i=!0,f=0;f<o.length;f++)(!1&l||a>=l)&&Object.keys(t.O).every((e=>t.O[e](o[f])))?o.splice(f--,1):(i=!1,l<a&&(a=l));if(i){e.splice(p--,1);var u=n();void 0!==u&&(r=u)}}return r}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[o,n,l]},t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e={121:0};t.O.j=r=>0===e[r];var r=(r,o)=>{var n,l,[a,i,f]=o,u=0;if(a.some((r=>0!==e[r]))){for(n in i)t.o(i,n)&&(t.m[n]=i[n]);if(f)var p=f(t)}for(r&&r(o);u<a.length;u++)l=a[u],t.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return t.O(p)},o=self.webpackChunk=self.webpackChunk||[];o.forEach(r.bind(null,0)),o.push=r.bind(null,o.push.bind(o))})()})();

View File

@ -1 +1 @@
(self.webpackChunk=self.webpackChunk||[]).push([[505],{755:(s,e,k)=>{k(634),k(681)},634:()=>{},681:(s,e,k)=>{"use strict";k.r(e)}},s=>{var e;e=755,s(s.s=e)}]);
(self.webpackChunk=self.webpackChunk||[]).push([[694],{797:(s,e,k)=>{k(974),k(715)},974:()=>{},715:(s,e,k)=>{"use strict";k.r(e)}},s=>{var e;e=797,s(s.s=e)}]);

View File

@ -0,0 +1,48 @@
<?php
namespace App\Altcha;
use App\Altcha\Form\AltchaModel;
use Symfony\Component\Form\DataTransformerInterface;
class AltchaTransformer implements DataTransformerInterface
{
/**
* {@inheritDoc}
*/
public function reverseTransform($value): AltchaModel
{
if (empty($value)) {
return new AltchaModel();
}
$decodedValue = base64_decode($value);
$data = json_decode($decodedValue);
$model = new AltchaModel();
foreach ($data as $property => $value) {
$model->{$property} = $value;
}
return $model;
}
/**
* {@inheritDoc}
*/
public function transform($value): string
{
if (empty($value)) {
return '';
}
$json = json_encode($value);
if (false === $json) {
return '';
}
return base64_encode($json);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Altcha;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class AltchaValidator
{
public function __construct(
private readonly string $altchaHost,
private readonly string $altchaBaseUrl,
private readonly HttpClientInterface $httpClient,
private readonly TranslatorInterface $translator
) {
}
public function validate(FormEvent $formEvent): void
{
$form = $formEvent->getForm();
$data = $form->getData();
$response = $this->httpClient->request(
'POST',
$this->altchaHost.$this->altchaBaseUrl.'/verify',
[
'body' => json_encode($data),
'headers' => [
'Content-Type' => 'application/json',
],
],
);
if (Response::HTTP_OK !== $response->getStatusCode()) {
$form->addError(new FormError($this->translator->trans('altcha.validator.server_validation_error', [], 'form')));
return;
}
$content = $response->getContent();
$parsedResponse = json_decode($content);
if (true !== $parsedResponse->success) {
$form->addError(new FormError($this->translator->trans('altcha.validator.server_validation_error', [], 'form')));
return;
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Altcha\Form;
use Symfony\Component\Validator\Constraints as Assert;
class AltchaModel
{
/**
* @Assert\NotBlank()
* @Assert\Regex("/^(SHA-1|SHA-256|SHA-512)$/")
*/
public string $algorithm;
/**
* @Assert\NotBlank()
*/
public string $challenge;
/**
* @Assert\NotBlank()
*/
public string $salt;
/**
* @Assert\NotBlank()
*/
public int $number;
/**
* @Assert\NotBlank()
*/
public string $signature;
/**
* @Assert\NotBlank()
*/
public int $took;
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Altcha\Form;
use App\Altcha\AltchaValidator;
use App\Altcha\AltchaTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class AltchaType extends AbstractType
{
public function __construct(
private readonly string $altchaHost,
private readonly string $altchaBaseUrl,
private readonly string $altchaDebug,
private readonly string $altchaWorkers,
private readonly string $altchaDelay,
private readonly string $altchaMockError,
private readonly AltchaValidator $altchaValidator,
private readonly HttpClientInterface $httpClient,
private readonly TranslatorInterface $translator
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new AltchaTransformer());
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this->altchaValidator, 'validate']);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$translations = [
'label' => $this->translator->trans('altcha.widget.label', [], 'form'),
'verified' => $this->translator->trans('altcha.widget.verified', [], 'form'),
'verifying' => $this->translator->trans('altcha.widget.verifying', [], 'form'),
'waitAlert' => $this->translator->trans('altcha.widget.waitalert', [], 'form'),
'error' => $this->translator->trans('altcha.widget.error', [], 'form'),
'expired' => $this->translator->trans('altcha.widget.expired', [], 'form'),
];
$view->vars['translations'] = json_encode($translations);
$view->vars['challengeJson'] = $this->requestChallenge();
$view->vars['debug'] = $this->altchaDebug;
$view->vars['workers'] = $this->altchaWorkers;
$view->vars['delay'] = $this->altchaDelay;
$view->vars['mockError'] = $this->altchaMockError;
}
private function requestChallenge(): string
{
$resp = $this->httpClient->request('GET', $this->altchaHost.$this->altchaBaseUrl.'/request');
if (Response::HTTP_OK === $resp->getStatusCode()) {
return $resp->getContent();
}
return '';
}
public function getParent(): string
{
return TextType::class;
}
public function getName(): string
{
return $this->getBlockPrefix();
}
public function getBlockPrefix(): string
{
return 'altcha';
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Flag\Controller;
use App\Flag\FlagEnum;
use Predis\ClientInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class FlagController extends AbstractController
{
#[Route('/flag', name: 'flag_update', methods: ['PUT'])]
public function updateFlag(ClientInterface $redis, FlagEnum $flagName, bool $flagValue): Response
{
$redis->set($flagName->value, $flagValue);
return new JsonResponse(
[\sprintf('flag %s has been %s.', $flagName->value, $flagValue ? 'enabled' : 'disabled')]
);
}
}

24
src/Flag/FlagAccessor.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Flag;
use Predis\ClientInterface;
class FlagAccessor
{
public function __construct(
private readonly ClientInterface $redis
) {
}
public function isFlagEnabled(FlagEnum $flagName, bool $fallbackValue = false): bool
{
$flagValue = $this->redis->get($flagName->value);
if (null === $flagValue) {
return $fallbackValue;
}
return (bool) $flagValue;
}
}

7
src/Flag/FlagEnum.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace App\Flag;
enum FlagEnum: string {
case Altcha = 'altcha';
}

View File

@ -2,15 +2,25 @@
namespace App\Form;
use App\Flag\FlagEnum;
use Predis\Client;
use App\Flag\FlagAccessor;
use App\Altcha\Form\AltchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class LoginType extends AbstractType
{
public function __construct(
private readonly FlagAccessor $flagAccessor,
private readonly bool $altchaEnabled
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
@ -28,6 +38,13 @@ class LoginType extends AbstractType
'label' => 'form.label.remember_me',
])
;
if ($this->flagAccessor->isFlagEnabled(FlagEnum::Altcha, $this->altchaEnabled)) {
$builder->add('altcha', AltchaType::class, [
'label' => false,
'translation_domain' => 'form',
]);
};
}
public function configureOptions(OptionsResolver $resolver): void

View File

@ -9,19 +9,17 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
class Client
{
private HttpClientInterface $client;
private const MAX_RETRY = 3;
private const SLEEP_TIME = [
5,
500,
5000,
];
private string $hydraAdminBaseUrl;
public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl)
{
$this->client = $client;
$this->hydraAdminBaseUrl = $hydraAdminBaseUrl;
public function __construct(
private readonly HttpClientInterface $client,
private readonly string $hydraAdminBaseUrl
) {
}
public function fetchLoginRequestInfo(string $loginChallenge): ResponseInterface

View File

@ -0,0 +1,18 @@
{% block altcha_widget %}
<style>
.altcha label {
margin-bottom: 0;
}
</style>
<altcha-widget
challengejson={{challengeJson}}
name={{form.vars.full_name}}
strings="{{translations}}"
hidelogo
hidefooter
workers= {{ workers }}
delay={{ delay }}
{{ debug ? 'debug' : ''}}
{{ mockError ? 'mockerror' : ''}}
></altcha-widget>
{% endblock %}

View File

@ -17,6 +17,34 @@
<source>form.label.remember_me</source>
<target>Remember me</target>
</trans-unit>
<trans-unit id="4kfMq14" resname="altcha.validator.server_validation_error">
<source>altcha.validator.server_validation_error</source>
<target xml:space="preserve">Verification failed. Try again later.</target>
</trans-unit>
<trans-unit id="7mZdXx_" resname="altcha.widget.error">
<source>altcha.widget.error</source>
<target xml:space="preserve">Verification failed. Try again later.</target>
</trans-unit>
<trans-unit id="Yg33QZt" resname="altcha.widget.expired">
<source>altcha.widget.expired</source>
<target xml:space="preserve">Verification expired. Try again.</target>
</trans-unit>
<trans-unit id="3y0.Bhb" resname="altcha.widget.label">
<source>altcha.widget.label</source>
<target xml:space="preserve">I'm not a robot</target>
</trans-unit>
<trans-unit id="QFBWtGD" resname="altcha.widget.verified">
<source>altcha.widget.verified</source>
<target xml:space="preserve">Verified</target>
</trans-unit>
<trans-unit id="SfOrgtj" resname="altcha.widget.verifying">
<source>altcha.widget.verifying</source>
<target xml:space="preserve">Verifying...</target>
</trans-unit>
<trans-unit id="9dhbPuz" resname="altcha.widget.waitalert">
<source>altcha.widget.waitalert</source>
<target xml:space="preserve">Verifying... please wait.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -17,6 +17,34 @@
<source>form.label.remember_me</source>
<target>Se souvenir de moi</target>
</trans-unit>
<trans-unit id="4kfMq14" resname="altcha.validator.server_validation_error">
<source>altcha.validator.server_validation_error</source>
<target xml:space="preserve">Échec de la vérification. Réessayez plus tard.</target>
</trans-unit>
<trans-unit id="7mZdXx_" resname="altcha.widget.error">
<source>altcha.widget.error</source>
<target xml:space="preserve">Échec de la vérification. Réesayez plus tard.</target>
</trans-unit>
<trans-unit id="Yg33QZt" resname="altcha.widget.expired">
<source>altcha.widget.expired</source>
<target xml:space="preserve">Vérification expirée. Réessayez.</target>
</trans-unit>
<trans-unit id="3y0.Bhb" resname="altcha.widget.label">
<source>altcha.widget.label</source>
<target xml:space="preserve">Je ne suis pas un robot</target>
</trans-unit>
<trans-unit id="QFBWtGD" resname="altcha.widget.verified">
<source>altcha.widget.verified</source>
<target xml:space="preserve">Vérifié</target>
</trans-unit>
<trans-unit id="SfOrgtj" resname="altcha.widget.verifying">
<source>altcha.widget.verifying</source>
<target xml:space="preserve">Vérification en cours...</target>
</trans-unit>
<trans-unit id="9dhbPuz" resname="altcha.widget.waitalert">
<source>altcha.widget.waitalert</source>
<target xml:space="preserve">Vérification en cours... veuillez patienter.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -25,8 +25,9 @@ Encore
.addEntry('bootstrap-css', './assets/styles/bootstrap.scss')
.addEntry('bootstrap-js', './assets/app-bootstrap.js')
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge('./assets/controllers.json')
.copyFiles([
{ from: './node_modules/altcha', to: 'altcha/[path][name].[ext]' },
])
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()