Implement ALTCHA captcha service with challenge generation and solution verification

- Added AltchaService class for managing ALTCHA captcha challenges and solutions.
- Created CaptchaController to handle API requests for generating challenges.
- Introduced CaptchaGuard for validating captcha solutions on public endpoints.
- Developed unit tests for AltchaService to ensure challenge creation and solution verification functionality.
- Implemented integration tests for the Captcha API endpoint and captcha bypass behavior when disabled.
- Added documentation for the Captcha API in the corresponding markdown file.
This commit is contained in:
hamed
2026-07-10 10:31:59 +03:30
parent 11efed4100
commit 10b0743d9a
43 changed files with 5586 additions and 1554 deletions
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "label": "CaptchaGuard.php", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L1"}, {"id": "captcha_captchaguard_captchaguard", "label": "CaptchaGuard", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L16"}, {"id": "captcha_captchaguard_captchaguard_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L18"}, {"id": "captcha_captchaguard_captchaguard_assertvalid", "label": ".assertValid()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20"}, {"id": "request", "label": "Request", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "errorcodes", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "appexception", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L6", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "request", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "captcha_captchaguard_captchaguard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L16", "weight": 1.0}, {"source": "captcha_captchaguard_captchaguard", "target": "captcha_captchaguard_captchaguard_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L18", "weight": 1.0}, {"source": "captcha_captchaguard_captchaguard", "target": "captcha_captchaguard_captchaguard_assertvalid", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20", "weight": 1.0}, {"source": "captcha_captchaguard_captchaguard_assertvalid", "target": "request", "relation": "references", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20", "weight": 1.0, "context": "parameter_type"}], "raw_calls": [{"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "enabled", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L22", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "json_decode", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L26", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "getContent", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L26", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "is_array", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L27", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "verifySolution", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L29", "receiver": null}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_captcha_md", "label": "captcha.md", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_captcha_api_altcha", "label": "Captcha API (ALTCHA)", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_get_api_v1_altcha_challenge", "label": "GET `/api/v1/altcha/challenge`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L10"}, {"id": "api_captcha_response_200", "label": "Response `200`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L14"}, {"id": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "label": "\u0627\u0639\u0645\u0627\u0644 \u06a9\u067e\u0686\u0627 \u0631\u0648\u06cc endpoint\u0647\u0627\u06cc \u0645\u062d\u0627\u0641\u0638\u062a\u200c\u0634\u062f\u0647", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L31"}, {"id": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "label": "\u062e\u0637\u0627\u06cc \u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc \u06a9\u067e\u0686\u0627 `422`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L53"}, {"id": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "label": "\u0627\u0645\u0646\u06cc\u062a", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L66"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_captcha_md", "target": "api_captcha_captcha_api_altcha", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L1", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_get_api_v1_altcha_challenge", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L10", "weight": 1.0}, {"source": "api_captcha_get_api_v1_altcha_challenge", "target": "api_captcha_response_200", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L14", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L31", "weight": 1.0}, {"source": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "target": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L53", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L66", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "label": "CaptchaController.php", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L1"}, {"id": "captcha_captchacontroller_captchacontroller", "label": "CaptchaController", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L10"}, {"id": "basecontroller", "label": "BaseController", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "captcha_captchacontroller_captchacontroller_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L13"}, {"id": "captcha_captchacontroller_captchacontroller_challenge", "label": ".challenge()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15"}, {"id": "jsonresponse", "label": "JsonResponse", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "basecontroller", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "attributes", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L6", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "jsonresponse", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "route", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L8", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "captcha_captchacontroller_captchacontroller", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L10", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller", "target": "basecontroller", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L11", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller", "target": "captcha_captchacontroller_captchacontroller_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L13", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller", "target": "captcha_captchacontroller_captchacontroller_challenge", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller_challenge", "target": "jsonresponse", "relation": "references", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15", "weight": 1.0, "context": "return_type"}], "raw_calls": [{"caller_nid": "captcha_captchacontroller_captchacontroller_challenge", "callee": "createChallenge", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaController.php", "source_location": "L23", "receiver": null}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_captcha_md", "label": "captcha.md", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_captcha_api_altcha", "label": "Captcha API (ALTCHA)", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_get_api_v1_altcha_challenge", "label": "GET `/api/v1/altcha/challenge`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L10"}, {"id": "api_captcha_response_200", "label": "Response `200`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L14"}, {"id": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "label": "\u0627\u0639\u0645\u0627\u0644 \u06a9\u067e\u0686\u0627 \u0631\u0648\u06cc endpoint\u0647\u0627\u06cc \u0645\u062d\u0627\u0641\u0638\u062a\u200c\u0634\u062f\u0647", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L31"}, {"id": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "label": "\u062e\u0637\u0627\u06cc \u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc \u06a9\u067e\u0686\u0627 `422`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L51"}, {"id": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "label": "\u0627\u0645\u0646\u06cc\u062a", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L64"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_captcha_md", "target": "api_captcha_captcha_api_altcha", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L1", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_get_api_v1_altcha_challenge", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L10", "weight": 1.0}, {"source": "api_captcha_get_api_v1_altcha_challenge", "target": "api_captcha_response_200", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L14", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L31", "weight": 1.0}, {"source": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "target": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L51", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L64", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "label": "ErrorCodes.php", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L1"}, {"id": "constant_errorcodes_errorcodes", "label": "ErrorCodes", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5"}, {"id": "constant_errorcodes_errorcodes_message", "label": ".message()", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L106"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "target": "constant_errorcodes_errorcodes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5", "weight": 1.0}, {"source": "constant_errorcodes_errorcodes", "target": "constant_errorcodes_errorcodes_message", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L106", "weight": 1.0}], "raw_calls": []}