Compare commits

...

91 Commits

Author SHA1 Message Date
5ea5d4d0ba Merge pull request 'Version 0.7.0' (#1) from develop into main
Reviewed-on: #1
2023-12-08 21:18:22 +06:00
e72dc03589
Update README with project specifics.
The README file has been updated to provide specific details about the current project.
2023-12-08 21:15:51 +06:00
6bf2bc793b
Add Docker setup for production environment. 2023-12-08 21:10:31 +06:00
8ccbd5000d
Add demo mode restriction to CaptchaToken destroy method.
This commit adds a check to the `destroy` method in the `CaptchaTokenService`. It uses the `Helpers::isDemoModeAndUserDenyUpdate` function to prevent users from deleting tokens while the application is in demo mode. This was added to protect the application's state during presentations or demos.
2023-12-05 21:48:02 +06:00
a52b148101
Add license information for captcha backgrounds. 2023-12-05 21:01:49 +06:00
b5db913c24
Add demo mode restrictions to user operations.
Added functionalities to restrict certain user operations like update, password change, and deletion in demo mode. This is done to prevent demo users from modifying crucial data. Helper methods are created for standard re-usable checks. Also, Blade directive is added for frontend UI demo checks.
2023-12-05 00:53:04 +06:00
ebc2dfd944
Update copyright link in footer.
The copyright link in the footer of the private layout app has been updated in this revision. Previously, it was pointing to a Github account, kor-elf. It is now pointing to the actual repository, which now resides on git.kor-elf.net. In addition to this, the full name of the owner, Leonid Nikitin (kor-elf), has been added for better transparency and clarity.
2023-12-04 22:46:37 +06:00
63d3c8cc6e
Add captcha code integration instructions to edit page.
In the captcha tokens edit page, a new section has been added to provide instructions for integrating the captcha code in user's website. This aims to facilitate ease of integration for users, by providing them a readily available code snippet.
2023-12-04 22:44:40 +06:00
630c316aaf
Added service-captcha-gui. 2023-12-04 22:42:35 +06:00
fcb8532b9d
Added file LICENSE. 2023-12-04 22:41:23 +06:00
2a7aaed0db
Correct typo in swagger.json
Fixed a typo in the 'user_agent' variable of the 'GetCaptchaKeyInfo' object in swagger.json. This was important to rectify in order to achieve accuracy and prevent potential errors during API documentation generation.
2023-12-03 00:57:33 +06:00
9221e089dd
Revived API POST /captcha/{captcha_uuid}.
Receiving captcha information for validation.
2023-11-26 22:42:22 +06:00
10425db5e0
Fixed a bug where two identical symbols appeared in a captcha.
The commit fixes an issue where wrong variable was passed as second argument to randomSymbols method. Previously it was using an uninitialized variable 'fakes' which was supposed to be populated by 'success' variable. The 'success' variable already has the set of valid symbols which should be used to generate fake symbols for captcha.
2023-11-26 15:25:36 +06:00
520a3ba068
Revived API POST /api/v1/captcha.
Captcha validation has been adjusted.
2023-11-26 15:09:42 +06:00
18899c81f2
Increase captcha image dimensions
The dimensions for both the 'image_head' and 'image_body' captcha images have been increased to provide a clearer and more readable captcha for users. 'Image_head' has been updated from 150x40 to 500x75, and 'image_body' from 300x240 to 500x500.
2023-11-23 00:26:47 +06:00
17fcc6e976
Removed unnecessary copying in Docker for development. 2023-09-19 17:26:26 +06:00
35c734d840
Add CaptchaToken to permissions language files
This commit adds 'CaptchaToken' key to both English and Russian versions of the permissions language files. The addition of the 'CaptchaToken' key was necessary to provide translations for tokens used in captcha creation. The lack of these labels was causing readability issues in UI.
2023-09-19 15:19:48 +06:00
c3e4c68a41
Add CryptographyContract and its implementation
Added a new CryptographyContract interface and CryptographyString class that implements this contract. The CryptographyContract encapsulates the encryption and decryption of strings, enforcing these operations to be standardized across the application. The CryptographyString class uses Laravel's native crypt facades to perform these actions. In the AppServiceProvider, CryptographyContract is now bound to CryptographyString class, allowing the container to automatically resolve the dependencies wherever the interface is type hinted.
2023-09-19 14:29:01 +06:00
27046e6674
Revived API /api/v1/captcha.
Now a new captcha is created to check for a bot.
2023-09-19 14:27:33 +06:00
56cd409301
The return comment of the getCoordinators method has been changed. 2023-09-19 14:21:56 +06:00
d8bf5dd116
Remove commented volumes from docker-compose.yml.
The lines setting up volumes for the 'swagger' service were commented out and redundant, leading to potential confusion. This commit removes those lines to clean up and simplify the file.
2023-08-27 22:25:09 +06:00
61cc272e2f
Rename parameter names in Swagger API documentation.
In our Swagger API documentation, the parameter names 'public_token' and 'private_token' have been renamed to match our established naming conventions. The hyphenated form ('public-token' and 'private-token') is more consistent with other parameters.
2023-08-23 23:45:57 +06:00
d72d6ff614
Update swagger.json for accurate API path.
The "servers" section was deleted and the "/api/v1" part was added to "paths" values for a more refined URL structure. This change makes it more clear that the API version is part of the API route.
2023-08-23 21:56:06 +06:00
9bfd3fef1a
Add Swagger API documentation and proxy routing.
Added Swagger as a new service in the docker-compose file for generating the API documentation. The new API documentation is provided via an OpenAPI file located at `public/swagger.json`. Changes in routing were done to handle /api-docs requests and redirect them to the running Swagger UI instance. This will make API easier to understand and debug.
2023-08-23 01:02:56 +06:00
742b0feaf0
Add captcha tokens management. 2023-08-22 00:15:35 +06:00
d2b29e2225
The refactoring added final readonly to App\Services\Role\BuilderCommand. 2023-08-01 23:43:03 +06:00
8f143c031f
Fixed warning in analysis PHP Storm. 2023-08-01 23:40:10 +06:00
3e18ca6510
Fixed warning in analysis PHP Storm. 2023-08-01 23:38:09 +06:00
faa706a79a
Removed duplicate "Toggle Navigation" keys in languages. 2023-08-01 23:36:16 +06:00
941699f473
Added @can('delete', $role). 2023-08-01 22:09:39 +06:00
9c1ed593b5
Added method getRolesForSelect in the RoleRepository. 2023-08-01 22:09:06 +06:00
73d99a0432
Fixed typo deleting RolePolicy: Role to role. 2023-08-01 22:07:47 +06:00
52c6fd88d7
Added the ability to manage users. 2023-08-01 22:04:35 +06:00
dc6b6b0d42
Added @canany(['create', 'update'], $role) to resources/views/private/roles/_from.blade.php. 2023-08-01 21:53:07 +06:00
4083e2ec5e
Added the ability to manage a group of users. 2023-07-16 19:21:09 +06:00
ba7e52f8ac
Added a new function 'errors' in Private Controller to handle service errors. This function checks if the resulted error code matches HTTP_UNPROCESSABLE_ENTITY and in that case, it redirects back with input and errors. For any other case, it aborts with the resulted error code and message. The main purpose of this change is to centralize error handling in one place and make the controller's actions cleaner. 2023-07-12 23:44:53 +06:00
1facb19efb
Refactor Role.php for improved code clarity.
The Role class in app/Models has been refactored to improve readability and maintainability. The 'slug' field was replaced with 'code' for consistency with the rest of the codebase. In addition, the 'isRemove' and 'isAdmin' methods were rewritten using new 'Attribute' Eloquent cast. This refactor enhances the code's clarity, promotes better comprehension, and clears redundant code comments.
2023-07-12 23:42:29 +06:00
919f6e1e42
Added click confirmation script. 2023-07-12 23:31:20 +06:00
ec5be8167e
Add @stack('scripts') to main layout. 2023-07-12 23:23:51 +06:00
d10cc8603e
The color of the links in the footer was changed from primary (probably blue) to gray-700 (dark gray) to improve readability against the white background. The change affects the link to the author's Github page and the link to the Volt - Bootstrap 5 Dashboard Template product page. 2023-07-12 22:53:27 +06:00
adb20f70a5
Update primary color variable in SCSS. 2023-07-12 22:52:29 +06:00
2121af2914
Renamed the "slug" field in the roles table to "code" to facilitate coding standards and improve readability. This unique identifier will now be referred to as 'code' across the application, creating consistency and reducing potential confusion in the future.
!!! Since the project has not yet been launched, therefore, I do not create a new migration, but change the current one. !!!
2023-07-12 22:30:05 +06:00
a3fa966643
Adjusted padding in sidebar for large screens. 2023-07-10 23:12:38 +06:00
9319c2d92d
A new middleware UserLocale.php has been added. This middleware sets the language locale based on each user's preference. It operates by checking if the user's preferred language is set during the request cycle and if so, it changes the app's locale accordingly. This feature facilitates personalization by displaying the app in a user's preferred language.
Also, registering the middleware in the `Kernel.php` allows it to be used throughout the application
2023-07-10 21:44:00 +06:00
907bac5586
Added the ability to save user settings. 2023-07-10 21:42:55 +06:00
b1c9dac3cc
The type for 'timezone' in the 'users' table was previously assigned as an unsigned integer. For flexible usage and to support a larger range of timezones, it has been altered to a string. The maximum length is set to 50 characters. This change ensures a more universal and user-friendly handling of various timezone situations.
!!! Since the project has not yet been launched, therefore, I do not create a new migration, but change the current one. !!!
2023-07-10 21:39:40 +06:00
79112680bd
Updated the visibility of getTitle, getType, and getValue methods in the Input class from protected to private. This was done to limit and control access to these methods to only within the class, enhancing encapsulation and improving security. 2023-07-09 19:39:37 +06:00
524bf569e3
The 'locale' column in the users table migration was renamed to 'lang'. This change was made to improve the clarity and consistency of naming conventions across the database. The term 'lang' is more universally recognized and thus provides better code readability.
!!! Since the project has not yet been launched, therefore, I do not create a new migration, but change the current one. !!!
2023-07-09 19:31:42 +06:00
e5d0cac07d
Changed the data type and default value of 'locale' and 'timezone' fields in the users table. Instead of using strings, we are now using unsigned integers.
!!! Since the project has not yet been launched, therefore, I do not create a new migration, but change the current one. !!!
2023-07-08 15:12:10 +06:00
39eae7f196
Added a new feature to allow users to update their passwords. 2023-07-07 18:08:14 +06:00
00a0624eea
This commit adds update functionality to user profiles. New routes, views, and controller methods have been created to facilitate this along with form requests for validation. Significant changes include new methods in the ProfileController, addition of an UpdateRequest class for validation purposes and the creation of a profile update view. These changes allow users to edit and update their profile information on the application. 2023-07-07 00:07:19 +06:00
55cd927f12
This commit adds two new components for handling form inputs in the private section of the site. 'Form.php' provides a base class for forms with methods for retrieving request names. 'Input.php' extends this to handle input fields specifically, allowing title, name, type, and value to be specified. An associated Blade view 'input.blade.php' has been added to render these inputs in views. This helps in reusability and maintainability. 2023-07-07 00:05:38 +06:00
24098415b5
Changed the way bootstrap is imported and added Chartist for charting functionality. This update provides wider access to Bootstrap features and integrates Chartist for better data representation. 2023-07-07 00:03:57 +06:00
398f5e2097
Added necessary template files and view components for scaffolding of the private area of the application. This includes error and success message components for efficient handling of user feedback messages. Added navigation components and dashboard structure to the private layout. This will streamline the implementation of features in the private area and ensure a consistent user interface. 2023-07-07 00:03:03 +06:00
92206a028a
Refactor error check in AuthController.
Changed the error check in the AuthController from checking if the result is not successful to checking if the result has an error. This change was made to clarify the code and ensure that errors are handled properly.
2023-07-06 22:32:29 +06:00
6dd24ac1d3
A new method 'isError' has been introduced to the ServiceResult interface and its implementation. This change was made to improve readability and logic. Now instead of checking if the result is not successful, we can directly check if it's an error using isError method. This improves the code clarity. 2023-07-06 22:31:39 +06:00
91810190b7
Introduced a new class ServiceResultSuccess to distinctively handle successful service responses. Changes were made in AuthService and Service base class to replace ServiceResultArray with ServiceResultSuccess for successful operations. This provides a more accurate response type and improves code readability. 2023-07-06 21:54:10 +06:00
c18e7e54b7
Modified the Middleware Kernel to use a new method for request throttling that uses Redis and created a new middleware to ensure that user email is verified.
These changes allow for more efficient request throttling by utilizing Redis. The new EnsureUserIsVerified middleware improves security by validating that a user's email has been verified or if the user is active before granting access, returning a 403 error if the user fails these checks.
2023-07-06 19:52:39 +06:00
00910831a4
The authorization method of the AuthController has been updated to specify a return type. This was done to ensure that the method always returns an instance of a RedirectResponse, thereby enhancing the code's readability and preventing unexpected return types. 2023-07-06 19:50:22 +06:00
5d61ab425e
Extended rate limiting functionality within the RouteServiceProvider to limit the login requests. Now the application limits the number of requests both per IP address and per email. This will drastically improve security by minimizing automated spam and brute-force attack attempts. 2023-07-06 10:50:15 +06:00
f481ee765d
Made authorization. 2023-07-06 10:48:32 +06:00
6b2aff910b
Added console command to add user with admin role. 2023-07-02 16:17:18 +06:00
d0bd480dc1
A new enumeration 'SystemRole' has been created for user roles. For now, it only contains one case 'Admin'. This list is intended to designate system roles that cannot be removed. 2023-07-02 16:07:49 +06:00
0073dffc28
Implemented password validation rules in the AppServiceProvider class. Ensured passwords should be at least 8 characters long, contain a mix of uppercase, lowercase, and special characters. These rules apply only for the production environment. 2023-07-02 15:21:01 +06:00
719d4c7f10
Added and configured css, js in Vite. 2023-06-29 22:20:38 +06:00
fbb56d8191
Added packages:
"dependencies": {
        "@popperjs/core": "^2.9.2",
        "bootstrap": "5.0.2",
        "chartist": "^0.11.4",
        "chartist-plugin-tooltips": "^0.0.17",
        "notyf": "^3.10.0",
        "nouislider": "^15.2.0",
        "onscreen": "^1.4.0",
        "sass": "^1.47.0",
        "simplebar": "^5.3.4",
        "smooth-scroll": "^16.1.3",
        "sweetalert2": "^11.0.18",
        "vanillajs-datepicker": "^1.2.0",
        "waypoints": "^4.0.1"
    },
    "devDependencies": {
        "sass-loader": "^13.3.2",
    }
2023-06-29 22:17:55 +06:00
37e5c6f8a6
Changed nodejs to 20. 2023-06-29 22:14:58 +06:00
aed3e0c803
Added language "Ru". 2023-06-29 22:14:16 +06:00
fe967d71a7
Added language "Ru". 2023-06-29 22:13:48 +06:00
2ebc9e3218
Added language "En". 2023-06-29 22:13:01 +06:00
6b8554d6d8
Updated composer.lock dependencies settings. Added laravel-lang/common. 2023-06-29 22:11:53 +06:00
b9f88ba2eb
Change default locale settings to Russian. 2023-06-29 00:06:52 +06:00
9b56522f02
A new 'locale' field of string type with a maximum length of 2 characters and a default value as 'ru' has been added to the users table through a migration file. This field has been also added to the User model. The addition of the 'locale' field will allow users to specify their preferred language in their profile settings. 2023-06-29 00:06:27 +06:00
90cab02d62
Enforce strict types and final class in User model. 2023-06-28 23:46:16 +06:00
34319e5724
Additional fields 'is_active' and 'timezone' have been added to the users table in the database migrations and the User model. This update allows us to better manage user's activation status and time zone preferences. 'is_active' field, a boolean field, signifies the active status of a user and has been set to default to false. 'timezone', an optional string, will store user's preferred timezone. 2023-06-28 23:44:06 +06:00
cf449eb8e2
Add SoftDeletes feature to User model. 2023-06-28 23:05:31 +06:00
fcf7cfa584
This commit introduces a MySQL container and an Adminer container to the docker-compose.yml file. The dependencies list was also updated, swapping the 'app' and 'db' listings to ensure the correct start-up order. An additional MySQL configuration file was added to mitigate heavy CPU usage. A new .gitignore file is also introduced to avoid unintended inclusion of database files in version control. 2023-06-28 21:43:39 +06:00
a35b8db281
Add captcha image generation functionality. 2023-06-28 17:29:56 +06:00
81635b4efa
This commit adds an ImageManager class that handles the creation of Image instances. It implements an ImageManagerContract, thereby adhering to set guidelines for the creation of captchas. The ImageManager class contains a createImage method capable of creating new instances of Image, allowing for increased flexibility and consistency in CAPTCHA image generation. 2023-06-28 17:26:15 +06:00
c1cf5a1ae9
This change introduces ImageLines, an interface for adding randomized lines to captcha images for enhanced security. The interface has been implemented in the Lines class. The goal is to randomize lines on captcha images to prevent bot reads. 2023-06-28 17:24:41 +06:00
f2ecdfcf97
This commit adds an Image interface and implementation that will be used in the Captcha generation process. The Image object includes methods for manipulating an image, such as adding text, inserting a background, and adding a line. These methods will provide the necessary functionalities for generating a Captcha. 2023-06-28 17:22:19 +06:00
f6669e93dc
Added config for captcha. 2023-06-28 17:17:44 +06:00
00177a134e
Introduced ServiceResult and ServiceResultError interfaces, and respective classes with related methods. These changes were necessary to add and handle service results throughout the app. We now have a standard way to return and handle both successes and errors from our services. This leads to cleaner, clearer, and more maintainable code. 2023-06-28 14:36:51 +06:00
20fbdd7a34
The PHP version in the composer.lock file has been updated from version 8.1 to 8.2. 2023-06-28 14:05:31 +06:00
e189137f6a
Added channel deprecations. 2023-03-10 21:04:26 +06:00
3ddfb77508
Changed php 8.1 to 8.2. 2023-03-09 23:20:34 +06:00
f49cc60f9a
Renamed route api to route api v1. 2023-03-05 20:42:09 +06:00
587679a33c
Removed route examples. 2023-03-05 20:26:41 +06:00
795945326e
Removed the welcome template. 2023-03-05 20:24:03 +06:00
15225e860c
Install Laravel. 2023-03-05 20:14:04 +06:00
5545a3e1c5
Added the .idea folder to ignore. 2023-03-05 19:50:17 +06:00
387 changed files with 32741 additions and 24 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

66
.env.example Normal file
View File

@ -0,0 +1,66 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_DEMO_MODE=false
APP_DEMO_EMAIL=
APP_DEMO_PASSWORD=
APP_DEFAULT_USER_TIMEZONE=UTC
# Valid languages: ru | en
APP_DEFAULT_LOCALE=ru
LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=deprecations
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

40
.gitignore vendored
View File

@ -1,25 +1,19 @@
# ---> Laravel
/vendor/
node_modules/
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
# Laravel 4 specific
bootstrap/compiled.php
app/storage/
# Laravel 5 & Lumen specific
public/storage
public/hot
# Laravel 5 & Lumen specific with changed public path
public_html/storage
public_html/hot
storage/*.key
.env
Homestead.yaml
Homestead.json
/.vagrant
.phpunit.result.cache
/.fleet
/.idea
/.vscode

9
LICENSE.md Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Leonid Nikitin (kor-elf)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,2 +1,50 @@
# Service_captcha
## О проекте
Захотелось написать свой независимый сервис защиты от роботов. Сервис каптча написан на фреймворке Laravel. Вдохновлялся, а так же брал картинки с проекта <a href="https://github.com/wenlng/go-captcha" target="_blank">Go Captcha</a>.
## Зависимости
php 8.2 (модули: redis, gd)
redis
mysql 8
## Демострация
Демо сервис каптча: https://captcha-admin-demo.tut-site.net/
Email: demo@tut-site.net
Пароль: demodemo
Демо каптча: https://captcha-demo.tut-site.net/
## API
https://captcha-admin-demo.tut-site.net/api-docs/
## Javascript клиент для сайта
https://git.kor-elf.net/kor-elf/captcha-rule-for-laravel
## Как проверять со стороны бэкенда
Для Laravel 10 есть готовый пакет: https://git.kor-elf.net/kor-elf/captcha-rule-for-laravel
Можно установить этот пакет так: composer require kor-elf/captcha-rule-for-laravel
<br><b>Curl:</b>
curl -X POST 'https://captcha-admin-demo.tut-site.net/api/v1/captcha/{captcha-token}' -H 'private-token: {your-private-token}' -H 'Content-Type: application/json' -d '{"user_agent": "{user-agent}"}' --max-time 10
Где {captcha-token} - токен получил пользователь от сервиса каптча после успешной проверки.
Где {your-private-token} - приватный токен, который мы создали в админке.
Где {user-agent} - передаём user agent от пользователя, который проходил каптчу.
Успешная проверка пользователя вернёт ответ код 200 и status = true. Иначе считаем, что пользователь не прошёл проверку на робота.
## Репозиторий с демо
https://git.kor-elf.net/kor-elf/service-captcha-demo
## Лицензия
[MIT license](https://opensource.org/licenses/MIT).

View File

@ -0,0 +1,144 @@
<?php declare(strict_types=1);
namespace App\Captcha\Config;
use App\Captcha\Exceptions\CaptchaException;
use Illuminate\Support\Arr;
final readonly class ImageBody
{
public function __construct(
private array $backgrounds,
private array $fonts,
private array $fontColors,
private int $width = 300,
private int $height = 240,
private int $angle = 20,
private array | int $fontSize = [20, 50],
private int $numberLines = 3,
private array $lineColors = []
) {
if ($this->width <= 0) {
throw new CaptchaException('Incorrect $width settings.');
}
if ($this->height <= 0) {
throw new CaptchaException('Incorrect $height settings.');
}
if (count($this->backgrounds) < 1) {
throw new CaptchaException('Invalid $backgrounds parameter.');
}
if (count($this->fonts) < 1) {
throw new CaptchaException('Invalid $fonts parameter.');
}
if (count($this->fontColors) < 1) {
throw new CaptchaException('Invalid $fontColors parameter.');
}
if ($this->angle < 0) {
throw new CaptchaException('Incorrect $angle settings.');
}
if (!is_integer($this->fontSize) && !is_array($this->fontSize)) {
throw new CaptchaException('The $fontSize parameter is not an array or integer.');
} elseif (is_array($this->fontSize)) {
if (count($this->fontSize) > 2) {
throw new CaptchaException('The array of $fontSize parameters contains more than 2 keys.');
}
if ($this->fontSize[0] > $this->fontSize[1]) {
throw new CaptchaException('The number of $fontSize[1] is less than $fontSize[0].');
}
}
}
/**
* @return array
*/
public function getBackgrounds(): array
{
return $this->backgrounds;
}
public function randomBackground(): string
{
return Arr::random($this->getBackgrounds());
}
/**
* @return array
*/
public function getFonts(): array
{
return $this->fonts;
}
public function randomFont(): string
{
return Arr::random($this->getFonts());
}
/**
* @return array
*/
public function getFontColors(): array
{
return $this->fontColors;
}
public function randomFontColor(): string
{
return Arr::random($this->getFontColors());
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
/**
* @return int
*/
public function getAngle(): int
{
return $this->angle;
}
public function randomAngle(): int
{
return mt_rand($this->getAngle() * -1, $this->getAngle());
}
public function getNumberLines(): int
{
return $this->numberLines;
}
public function fontSize(): int
{
$fontSize = $this->fontSize;
if (is_integer($fontSize)) {
return $fontSize;
}
if (empty($fontSize[1])) {
return $fontSize[0];
}
return random_int($fontSize[0], $fontSize[1]);
}
/**
* @return array
*/
public function getLineColors(): array
{
return $this->lineColors;
}
}

View File

@ -0,0 +1,147 @@
<?php declare(strict_types=1);
namespace App\Captcha\Config;
use App\Captcha\Exceptions\CaptchaException;
use Illuminate\Support\Arr;
final readonly class ImageHead
{
public function __construct(
private array $backgrounds,
private array $fonts,
private array $fontColors,
private int $width = 150,
private int $height = 40,
private int $textPaddingTop = 5,
private int $textPaddingLeft = 10,
private int $angle = 20,
private int $numberLines = 3,
private array $lineColors = []
) {
if ($this->width <= 0) {
throw new CaptchaException('Incorrect $width settings.');
}
if ($this->height <= 0) {
throw new CaptchaException('Incorrect $height settings.');
}
if ($this->textPaddingTop < 0) {
throw new CaptchaException('Incorrect $textPaddingTop settings.');
}
if ($this->textPaddingLeft < 0) {
throw new CaptchaException('Incorrect $textPaddingLeft settings.');
}
if (count($this->backgrounds) < 1) {
throw new CaptchaException('Invalid $backgrounds parameter.');
}
if (count($this->fonts) < 1) {
throw new CaptchaException('Invalid $fonts parameter.');
}
if (count($this->fontColors) < 1) {
throw new CaptchaException('Invalid $fontColors parameter.');
}
if ($this->angle < 0) {
throw new CaptchaException('Incorrect $angle settings.');
}
if ($this->numberLines < 0) {
throw new CaptchaException('Incorrect $numberLines settings.');
}
}
/**
* @return array
*/
public function getBackgrounds(): array
{
return $this->backgrounds;
}
public function randomBackground(): string
{
return Arr::random($this->getBackgrounds());
}
/**
* @return array
*/
public function getFonts(): array
{
return $this->fonts;
}
public function randomFont(): string
{
return Arr::random($this->getFonts());
}
/**
* @return array
*/
public function getFontColors(): array
{
return $this->fontColors;
}
public function randomFontColor(): string
{
return Arr::random($this->getFontColors());
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
/**
* @return int
*/
public function getTextPaddingTop(): int
{
return $this->textPaddingTop;
}
/**
* @return int
*/
public function getTextPaddingLeft(): int
{
return $this->textPaddingLeft;
}
/**
* @return int
*/
public function getAngle(): int
{
return $this->angle;
}
public function randomAngle(): int
{
return mt_rand($this->getAngle() * -1, $this->getAngle());
}
public function getNumberLines(): int
{
return $this->numberLines;
}
/**
* @return array
*/
public function getLineColors(): array
{
return $this->lineColors;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Dto\Coordinators;
interface Image
{
public function __construct(int $width, int $height);
public function getWidth(): int;
public function getHeight(): int;
public function insertBackground(string $pathToFile): self;
public function addText(string $text, int $x, int $y, float $size, float $angle, string $hexColor, string $fontName): Coordinators;
public function addLine(int $x1, int $y1, int $x2, int $y2, string $hexColor): self;
public function encode(): string;
public function __destruct();
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Config\ImageBody as ImageBodyConfig;
use App\Captcha\Dto\ImageBody as ImageBodyDto;
use App\Captcha\Dto\Symbols;
interface ImageBody
{
public function processing(Symbols $symbols, ImageBodyConfig $config): ImageBodyDto;
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Dto\ImageHead as ImageHeadDto;
use App\Captcha\Dto\Symbols;
use App\Captcha\Config\ImageHead as ImageHeadConfig;
interface ImageHead
{
public function processing(Symbols $symbols, ImageHeadConfig $config): ImageHeadDto;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
interface ImageLines
{
public function processing(Image $image, array $colors, int $lines = 3): Image;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
interface ImageManager
{
public function createImage(int $width, int $height): Image;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Dto\Symbols;
interface Type
{
public function getSymbols(): Symbols;
}

View File

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class Coordinators
{
public function __construct(
private int $x1,
private int $y1,
private int $x2,
private int $y2,
private int $x3,
private int $y3,
private int $x4,
private int $y4
) { }
/**
* lower left x-coordinate
* @return int
*/
public function getX1(): int
{
return $this->x1;
}
/**
* lower left y-coordinate
* @return int
*/
public function getY1(): int
{
return $this->y1;
}
/**
* lower right x-coordinate
* @return int
*/
public function getX2(): int
{
return $this->x2;
}
/**
* lower right y-coordinate
* @return int
*/
public function getY2(): int
{
return $this->y2;
}
/**
* upper right x-coordinate
* @return int
*/
public function getX3(): int
{
return $this->x3;
}
/**
* upper right y-coordinate
* @return int
*/
public function getY3(): int
{
return $this->y3;
}
/**
* upper left x-coordinate
* @return int
*/
public function getX4(): int
{
return $this->x4;
}
/**
* upper left y-coordinate
* @return int
*/
public function getY4(): int
{
return $this->y4;
}
}

36
app/Captcha/Dto/Image.php Normal file
View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class Image
{
public function __construct(
private string $imageBase64,
private int $width,
private int $height
) { }
/**
* @return string
*/
public function getImageBase64(): string
{
return $this->imageBase64;
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class ImageBody
{
public function __construct(
private Image $image,
private array $coordinators
) { }
/**
* @return Image
*/
public function getImage(): Image
{
return $this->image;
}
/**
* @return array
*/
public function getCoordinators(): array
{
return $this->coordinators;
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class ImageHead
{
public function __construct(
private Image $image
) { }
/**
* @return Image
*/
public function getImage(): Image
{
return $this->image;
}
}

View File

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class Sector
{
public function __construct(
private int $x,
private int $y,
private int $width,
private int $height,
) { }
/**
* @return int
*/
public function getX(): int
{
return $this->x;
}
/**
* @return int
*/
public function getY(): int
{
return $this->y;
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final class Sectors
{
private array $points = [];
public function add(int|float $x, int|float $y, int|float $width, int|float $height): self
{
$this->points[] = new Sector((int) $x, (int) $y, (int) $width, (int) $height);
return $this;
}
public function random(): Sector
{
$key = array_rand($this->points);
$sector = $this->points[$key];
unset($this->points[$key]);
return $sector;
}
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
use App\Captcha\Enums\SymbolType;
final readonly class Symbols
{
public function __construct(
private array $success,
private array $fakes,
private SymbolType $type
) { }
/**
* @return array
*/
public function getSuccess(): array
{
return $this->success;
}
/**
* @return array
*/
public function getFakes(): array
{
return $this->fakes;
}
/**
* @return SymbolType
*/
public function getType(): SymbolType
{
return $this->type;
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Enums;
Enum SymbolType {
case String;
case ImagePath;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Exceptions;
final class CaptchaException extends \Exception
{
}

117
app/Captcha/Images/Body.php Normal file
View File

@ -0,0 +1,117 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Config\ImageBody as ImageBodyConfig;
use App\Captcha\Contracts\ImageLines;
use App\Captcha\Dto\Coordinators;
use App\Captcha\Dto\Image as ImageDto;
use App\Captcha\Dto\ImageBody as ImageBodyDto;
use App\Captcha\Dto\Sector;
use App\Captcha\Dto\Sectors;
use App\Captcha\Dto\Symbols;
use App\Captcha\Contracts\ImageBody;
use App\Captcha\Enums\SymbolType;
use App\Captcha\Contracts\ImageManager;
final class Body implements ImageBody
{
public function __construct(
private readonly ImageManager $imageManager,
private readonly ImageLines $imageLines
) { }
public function processing(Symbols $symbols, ImageBodyConfig $config): ImageBodyDto
{
$image = $this->imageManager->createImage($config->getWidth(), $config->getHeight());
$image->insertBackground($config->randomBackground());
$imageData = match ($symbols->getType()) {
SymbolType::String => $this->processingString($image, $symbols, $config),
};
$image = $imageData['image'];
if ($config->getNumberLines() > 0) {
$this->imageLines->processing(image: $image, colors: $config->getLineColors(), lines: $config->getNumberLines());
}
$image = new ImageDto(
imageBase64: $image->encode(),
width: $image->getWidth(),
height: $image->getHeight()
);
return new ImageBodyDto(
image: $image,
coordinators: $imageData['coordinators']
);
}
private function processingString(Image $image, Symbols $symbols, ImageBodyConfig $config): array
{
$sectors = $this->calculateSectors($symbols, $image->getWidth(), $image->getHeight());
$coordinators = [];
foreach ($symbols->getSuccess() as $number => $symbol) {
$coordinators[] = $this->processingStringSymbol($image, $symbol, $config, $sectors->random());
}
if (!empty($symbols->getFakes())) {
foreach ($symbols->getFakes() as $number => $symbol) {
$this->processingStringSymbol($image, $symbol, $config, $sectors->random());
}
}
return [
'image' => $image,
'coordinators' => $coordinators
];
}
private function processingStringSymbol(Image &$image, string|int $symbol, ImageBodyConfig $config, Sector $sector): Coordinators
{
$fontSize = $config->fontSize();
if ($fontSize > $sector->getHeight()) {
$fontSize = $sector->getHeight();
}
$fontPathFile = $config->randomFont();
$fontColor = $config->randomFontColor();
$angle = $config->randomAngle();
$marginLeft = mt_rand($sector->getX(), $sector->getX() + $sector->getWidth() - $fontSize);
$marginTop = mt_rand($sector->getY(), $sector->getY() + $sector->getHeight() - $fontSize);
return $image->addText(
text: $symbol,
x: $marginLeft,
y: $marginTop,
size: $fontSize,
angle: $angle,
hexColor: $fontColor,
fontName: $fontPathFile
);
}
private function calculateSectors(Symbols $symbols, int $width, int $height): Sectors
{
$points = count($symbols->getSuccess()) + count($symbols->getFakes());
$sumFloors = 3;
$sumRooms = ceil($points / $sumFloors);
$heightFloor = floor($height / $sumFloors);
$widthFloor = floor($width / $sumRooms);
$sectors = new Sectors();
for ($floor = 0; $floor < $sumFloors; $floor++) {
$y = $heightFloor * $floor;
for ($room = 0; $room < $sumRooms; $room++) {
$sectors->add(
x: ($widthFloor * $room),
y: $y,
width: $widthFloor,
height: $heightFloor
);
}
}
return $sectors;
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Config\ImageHead as ImageHeadConfig;
use App\Captcha\Contracts\ImageLines;
use App\Captcha\Dto\Image as ImageDto;
use App\Captcha\Dto\ImageHead as ImageHeadDto;
use App\Captcha\Dto\Symbols;
use App\Captcha\Contracts\ImageHead;
use \App\Captcha\Contracts\Image as ImageContract;
use App\Captcha\Enums\SymbolType;
use App\Captcha\Contracts\ImageManager;
final class Head implements ImageHead
{
public function __construct(
private readonly ImageManager $imageManager,
private readonly ImageLines $imageLines
) { }
public function processing(Symbols $symbols, ImageHeadConfig $config): ImageHeadDto
{
$image = $this->imageManager->createImage($config->getWidth(), $config->getHeight());
$image->insertBackground($config->randomBackground());
$image = match ($symbols->getType()) {
SymbolType::String => $this->processingString($image, $symbols, $config),
};
if ($config->getNumberLines() > 0) {
$this->imageLines->processing(image: $image, colors: $config->getLineColors(), lines: $config->getNumberLines());
}
$image = new ImageDto(
imageBase64: $image->encode(),
width: $image->getWidth(),
height: $image->getHeight()
);
return new ImageHeadDto(image: $image);
}
private function processingString(ImageContract $image, Symbols $symbols, ImageHeadConfig $config): Image
{
$textLeftPadding = $config->getTextPaddingLeft();
$textTopPadding = $config->getTextPaddingTop();
$countSymbols = count($symbols->getSuccess());
$widthPrefix = $image->getWidth() - $textLeftPadding;
$imageHeight = $image->getHeight() - $textTopPadding;
foreach ($symbols->getSuccess() as $number => $symbol) {
$marginLeft = $textLeftPadding + $number * $widthPrefix / $countSymbols;
$image->addText(
text: $symbol,
x: intval($marginLeft),
y: $textTopPadding,
size: $this->randomFontSize($imageHeight),
angle: $config->randomAngle(),
hexColor: $config->randomFontColor(),
fontName: $config->randomFont()
);
}
return $image;
}
private function randomFontSize(int $imageHeight): int
{
return mt_rand($imageHeight - 10, $imageHeight);
}
}

View File

@ -0,0 +1,138 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Contracts\Image as ImageContract;
use App\Captcha\Dto\Coordinators;
use App\Captcha\Exceptions\CaptchaException;
final class Image implements ImageContract
{
private readonly \GdImage $image;
public function __construct(int $width, int $height) {
$this->image = imagecreatetruecolor($width, $height);
}
public function getWidth(): int
{
return imagesx($this->image);
}
public function getHeight(): int
{
return imagesy($this->image);
}
public function insertBackground(string $pathToFile): ImageContract
{
list($backgroundWidth, $backgroundHeight) = getimagesize($pathToFile);
$background = $this->createImageFromPathToFile($pathToFile);
imagecopyresampled(
dst_image: $this->image,
src_image: $background,
dst_x: 0,
dst_y: 0,
src_x: 0,
src_y: 0,
dst_width: $this->getWidth(),
dst_height: $this->getHeight(),
src_width: $backgroundWidth,
src_height: $backgroundHeight
);
imagedestroy($background);
return $this;
}
public function addText(string $text, int $x, int $y, float $size, float $angle, string $hexColor, string $fontName): Coordinators
{
$y += intval($size);
$result = imagefttext(
image: $this->image,
size: $size,
angle: $angle,
x: $x,
y: $y,
color: $this->colorConvertHexToColorIndex($hexColor),
font_filename: $fontName,
text: $text
);
return new Coordinators(
x1: $result[0],
y1: $result[1],
x2: $result[2],
y2: $result[3],
x3: $result[4],
y3: $result[5],
x4: $result[6],
y4: $result[7]
);
}
public function addLine(int $x1, int $y1, int $x2, int $y2, string $hexColor): self
{
imageline(
image: $this->image,
x1: $x1,
y1: $y1,
x2: $x2,
y2: $y2,
color: $this->colorConvertHexToColorIndex($hexColor)
);
return $this;
}
public function encode(): string
{
ob_start();
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
imagepng($this->image, null, -1);
$mime = image_type_to_mime_type(IMAGETYPE_PNG);
$buffer = ob_get_contents();
ob_end_clean();
return sprintf('data:%s;base64,%s',
$mime,
base64_encode($buffer)
);
}
public function __destruct() {
imagedestroy($this->image);
}
private function createImageFromPathToFile(string $pathToFile): \GdImage
{
return match (mime_content_type($pathToFile)) {
'image/jpeg' => imagecreatefromjpeg($pathToFile),
'image/png' => imagecreatefrompng($pathToFile),
default => throw new CaptchaException('Couldn\'t open the file. Not a valid type. File ' . $pathToFile . '.')
};
}
private function colorConvertHexToColorIndex(string $hexColor): int
{
$hexColor = str_replace('#', '', $hexColor);
if (strlen($hexColor) === 3) {
return imagecolorexact(
image: $this->image,
red: hexdec(str_repeat(substr($hexColor, 0, 1), 2)),
blue: hexdec(str_repeat(substr($hexColor, 1, 1), 2)),
green: hexdec(str_repeat(substr($hexColor, 2, 1), 2))
);
}
$colorVal = hexdec($hexColor);
return imagecolorexact(
image: $this->image,
red: 0xFF & ($colorVal >> 0x10),
blue: 0xFF & ($colorVal >> 0x8),
green: 0xFF & $colorVal
);
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Contracts\Image;
use App\Captcha\Contracts\ImageManager as ImageManagerContract;
final class ImageManager implements ImageManagerContract
{
public function __construct(
private readonly string $imageClassName
) { }
public function createImage(int $width, int $height): Image
{
return new $this->imageClassName($width, $height);
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Contracts\ImageLines;
use Illuminate\Support\Arr;
use App\Captcha\Contracts\Image as ImageContract;
class Lines implements ImageLines
{
public function processing(ImageContract $image, array $colors, $lines = 3): ImageContract
{
$imageWidth = $image->getWidth();
$imageHeight = $image->getHeight();
for ($i = 0; $i <= $lines; $i++) {
$image->addLine(
x1: mt_rand(0, $imageWidth) + $i * mt_rand(0, $imageHeight),
y1: mt_rand(0, $imageHeight),
x2: mt_rand(0, $imageWidth),
y2: mt_rand(0, $imageHeight),
hexColor: $this->lineColors($colors)
);
}
return $image;
}
private function lineColors(array $colors): string
{
if (!empty($colors)) {
return Arr::random($colors);
}
return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT);
}
}

26
app/Captcha/Type.php Normal file
View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Captcha;
use App\Captcha\Contracts\Type as TypeContract;
use App\Captcha\Dto\Symbols;
abstract class Type implements TypeContract
{
public function __construct(
private readonly array $config
) { }
abstract public function getSymbols(): Symbols;
final public function getConfig(): array
{
return $this->config;
}
final public function getConfigValue(string | int $name, mixed $default = null): mixed
{
$config = $this->getConfig();
return $config[$name] ?? $default;
}
}

View File

@ -0,0 +1,104 @@
<?php declare(strict_types=1);
namespace App\Captcha\Types;
use App\Captcha\Dto\Symbols;
use App\Captcha\Enums\SymbolType;
use App\Captcha\Exceptions\CaptchaException;
use App\Captcha\Type;
use Illuminate\Support\Arr;
final class StringType extends Type
{
public function getSymbols(): Symbols
{
$lengthSymbols = $this->lengthSymbols();
$lengthFakeSymbols = $this->lengthFakeSymbols();
$success = $this->randomSymbols($lengthSymbols);
$fakes = [];
if (!empty($lengthFakeSymbols)) {
$fakes = $this->randomSymbols($lengthFakeSymbols, $success);
}
return new Symbols(
success: $success,
fakes: $fakes,
type: SymbolType::String
);
}
/**
* @param int $min
* @param int $max
* @param array $except
* @return array
* @throws CaptchaException
*/
private function randomSymbols(int $lenght, array $except = []): array
{
$symbols = $this->symbols();
if (!empty($except)) {
$symbols = array_diff($symbols, $except);
}
if (count($symbols) < $lenght) {
throw new CaptchaException('The number of characters is less than $lenght.');
}
return Arr::random($symbols, $lenght);
}
/**
* @return array
* @throws CaptchaException
*/
private function symbols(): array
{
$symbols = $this->getConfigValue('symbols');
if (!is_array($symbols)) {
throw new CaptchaException('The symbols parameter is not an array.');
}
return $symbols;
}
private function lengthSymbols(): int
{
$length = $this->getConfigValue('length_symbols');
if (is_integer($length)) {
return $length;
}
if (!is_array($length)) {
throw new CaptchaException('The length_symbols parameter is not an array or integer.');
}
if (count($length) > 2) {
throw new CaptchaException('The array of length_symbols parameters contains more than 2 keys.');
}
if ($length[0] > $length[1]) {
throw new CaptchaException('The number of length_symbols[1] is less than length_symbols[0].');
}
return random_int($length[0], $length[1]);
}
private function lengthFakeSymbols(): int
{
$length = $this->getConfigValue('length_fake_symbols');
if (is_integer($length)) {
return $length;
}
if (is_null($length) || $length === false) {
return 0;
}
if (!is_array($length)) {
throw new CaptchaException('The length_fake_symbols parameter is not an array or integer or null or false.');
}
if (count($length) > 2) {
throw new CaptchaException('The array of length_fake_symbols parameters contains more than 2 keys.');
}
if ($length[0] > $length[1]) {
throw new CaptchaException('The number of length_fake_symbols[1] is less than length_fake_symbols[0].');
}
return random_int($length[0], $length[1]);
}
}

View File

@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Dto\User\ManyRoleDto;
use App\Enums\SystemRole;
use App\Repositories\RoleRepository;
use App\Services\User\UserCommandHandler;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Validator;
final class CreateUserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:create-user-admin {email} {password}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create admin user.';
/**
* Execute the console command.
*/
public function handle(UserCommandHandler $userCommandHandler, RoleRepository $roleRepository): void
{
$validator = $this->getData();
if ($validator->fails()) {
$this->errorMessageAndStop($validator->errors()->all());
}
$data = $validator->valid();
try {
$role = $roleRepository->getRoleByCode(SystemRole::Admin->value);
if (is_null($role)) {
$this->errorMessageAndStop('Administrator role not found.');
}
$user = DB::transaction(function () use($data, $userCommandHandler, $role) {
$data['name'] = 'Administrator';
$user = $userCommandHandler->handleStore($data, $data['password']);
$userCommandHandler->handleConfirmationByEmail($user);
$roles = new ManyRoleDto([$role->id]);
$userCommandHandler->handleSyncRoles($user, $roles);
return $user;
});
} catch (\Throwable $e) {
$this->errorMessageAndStop($e->getMessage());
}
$this->info('The command was successful!');
}
private function getData(): Validator
{
return ValidatorFacade::make([
'email' => $this->argument('email'),
'password' => $this->argument('password'),
], [
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', Password::default()],
]);
}
private function stop(): never
{
exit;
}
private function errorMessageAndStop(string | array $error): never
{
$this->info('User not created. See error messages below:');
if (is_array($error)) {
foreach ($error as $err) {
$this->error($err);
}
} else {
$this->error($error);
}
$this->stop();
}
}

27
app/Console/Kernel.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface CryptographyContract
{
public function encrypt(string $text): string;
public function decrypt(string $encryptedText): string;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use App\Dto\Request\Dto;
interface FormRequestDto
{
public function getDto(): Dto;
}

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
interface GenerateTokenCommand
{
public function execute(): string;
public function unique(Builder $builder, string $field, int $numberAttempts = 10): string;
}

18
app/Contracts/Search.php Normal file
View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
interface Search
{
public function __construct(Relation | Builder $query);
public function all(): Collection;
public function get(int $limit): Collection;
public function pagination(int $limit, int $page = 1): LengthAwarePaginator;
public function cursorPaginate(int $limit): CursorPaginator;
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface ServiceResult
{
public function isSuccess(): bool;
public function isError(): bool;
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface ServiceResultError
{
public function getCode(): ?int;
public function getMessage(): string;
public function getErrors(): array;
public function getErrorsOrMessage(): array|string;
public function getData(): array;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class CaptchaToken
{
public function __construct(
) { }
}

10
app/Dto/Builder/Role.php Normal file
View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class Role
{
public function __construct(
) { }
}

10
app/Dto/Builder/User.php Normal file
View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class User
{
public function __construct(
) { }
}

27
app/Dto/HttpUserData.php Normal file
View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto;
final readonly class HttpUserData
{
public function __construct(
private ?string $clientIp = null,
private ?string $userAgent = null,
private ?string $referer = null
) { }
public function getClientIp(): ?string
{
return $this->clientIp;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getReferer(): ?string
{
return $this->referer;
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto;
final readonly class QuerySettingsDto
{
public function __construct(
private int $limit,
private int $page = 1,
private array $queryWith = []
) { }
public function getLimit(): int
{
return $this->limit;
}
public function getPage(): int
{
return $this->page;
}
public function getQueryWith(): array
{
return $this->queryWith;
}
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Repository\DataCaptchaRepository;
final readonly class DataCaptcha
{
public function __construct(
private int $captchaId,
private array $coordinators,
) { }
public function getCaptchaId(): int
{
return $this->captchaId;
}
public function getCoordinators(): array
{
return $this->coordinators;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\HttpUserData;
use App\Dto\Request\Dto;
use App\Models\CaptchaToken;
final readonly class CaptchaPublicToken extends Dto
{
public function __construct(
private CaptchaToken $captchaToken,
private HttpUserData $httpUserData
) { }
public function getCaptchaToken(): CaptchaToken
{
return $this->captchaToken;
}
public function getHttpUserData(): HttpUserData
{
return $this->httpUserData;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\Request\Dto;
final readonly class CheckingDto extends Dto
{
public function __construct(
private CaptchaPublicToken $captchaPublicToken,
private string $captchaKey,
private array $coordinators,
) { }
public function getCaptchaPublicToken(): CaptchaPublicToken
{
return $this->captchaPublicToken;
}
public function getCaptchaKey(): string
{
return $this->captchaKey;
}
public function getCoordinators(): array
{
return $this->coordinators;
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\HttpUserData;
use App\Dto\Request\Dto;
use App\Models\CaptchaToken;
final readonly class VerificationInformationDto extends Dto
{
public function __construct(
private CaptchaToken $captchaToken,
private HttpUserData $httpUserData,
private ?string $userAgent = null,
) { }
public function getCaptchaToken(): CaptchaToken
{
return $this->captchaToken;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getHttpUserData(): HttpUserData
{
return $this->httpUserData;
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto\Request;
final readonly class Authorization extends Dto
{
public function __construct(
private string $email,
private string $password,
private bool $remember = false
) { }
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
{
return $this->password;
}
public function getRemember(): bool
{
return $this->remember;
}
}

8
app/Dto/Request/Dto.php Normal file
View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Dto\Request;
abstract readonly class Dto
{
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\CaptchaToken;
use App\Dto\Builder\CaptchaToken;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private CaptchaToken $captchaTokenDto,
private int $page
) { }
public function getCaptchaTokenDto(): CaptchaToken
{
return $this->captchaTokenDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\CaptchaToken;
use App\Dto\Request\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $title
) { }
public function getTitle(): string
{
return $this->title;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Profile;
use App\Dto\Request\Dto;
final readonly class Update extends Dto
{
public function __construct(
private string $name
) { }
public function getName(): string
{
return $this->name;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Profile;
use App\Dto\Request\Dto;
use App\Enums\Lang;
final readonly class UpdateSettings extends Dto
{
public function __construct(
private ?Lang $lang,
private ?string $timezone
) { }
public function getLang(): ?Lang
{
return $this->lang;
}
public function getTimezone(): ?string
{
return $this->timezone;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Role;
use App\Dto\Builder\Role;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private Role $roleBuilderDto,
private int $page
) { }
public function getRoleBuilderDto(): Role
{
return $this->roleBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Role;
use App\Dto\Request\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private ?string $code,
private array $permissions,
) { }
public function getName(): string
{
return $this->name;
}
public function getCode(): ?string
{
return $this->code;
}
public function getPermissions(): array
{
return $this->permissions;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Builder\User;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private User $userBuilderDto,
private int $page
) { }
public function getUserBuilderDto(): User
{
return $this->userBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Request\Dto;
use App\Dto\User\ManyRoleDto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $email,
private bool $isActive,
private ManyRoleDto $roles,
private ?string $password = null
) { }
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getRoles(): ManyRoleDto
{
return $this->roles;
}
public function isActive(): bool
{
return $this->isActive;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Request\Dto;
final readonly class UpdatePassword extends Dto
{
public function __construct(
private string $password
) { }
public function getPassword(): string
{
return $this->password;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Dto\User;
use App\Exceptions\Dto\User\ManyRoleDtoException;
final class ManyRoleDto
{
private array $roles = [];
public function __construct(array $roles = []) {
foreach ($roles as $role) {
if (!is_numeric($role) || is_float($role)) {
throw new ManyRoleDtoException('Not an integer: ' . $role . '.');
}
$this->add((int) $role);
}
}
public function add(int $id): void
{
if ($id < 1) {
throw new ManyRoleDtoException('Only Integer > 0.');
}
$this->roles[] = $id;
}
public function toArray(): array
{
return $this->roles;
}
}

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum CaptchaLogType: int
{
case Created = 1;
case Error = 2;
case Verified = 3;
case ReadVerified = 4;
}

46
app/Enums/Lang.php Normal file
View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\Enums;
use Illuminate\Support\Collection;
enum Lang: int
{
case Ru = 1;
case En = 2;
public function getTitle(): string
{
return match ($this) {
self::Ru => 'Русский',
self::En => 'English'
};
}
public function getLocale(): string
{
return match ($this) {
self::Ru => 'ru',
self::En => 'en'
};
}
public static function toArray(): array
{
$choices = [];
foreach (self::cases() as $lang) {
$choices[] = [
'name' => $lang->name,
'value' => $lang->value,
'title' => $lang->getTitle(),
'locale' => $lang->getLocale()
];
}
return $choices;
}
public static function toCollection(): Collection
{
return collect(self::toArray());
}
}

67
app/Enums/Permission.php Normal file
View File

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum Permission: string
{
case Role = 'role';
case User = 'user';
case CaptchaToken = 'captcha_token';
public function getPermissions(): array
{
$permissions = match ($this) {
self::CaptchaToken => [
'view' => __('permissions.Allowed to watch all tokens'),
'view_own' => __('permissions.Allowed to view own tokens'),
'create' => __('permissions.Allowed to create tokens'),
'update' => __('permissions.Allowed to edit all tokens'),
'update_own' => __('permissions.Allowed to edit own tokens'),
'delete' => __('permissions.Allowed to delete all tokens'),
'delete_own' => __('permissions.Allowed to delete own tokens'),
],
default => $this->getBasePermissions()
};
return $permissions;
}
public function getTitle(): string
{
return __('permissions.' . $this->name);
}
public function formatValue(string $permission): string
{
return $this->value . '.' . $permission;
}
public static function toArrayList(): array
{
$permissions = [];
foreach (self::cases() as $permissionEnum) {
foreach ($permissionEnum->getPermissions() as $permissionName => $permissionTitle) {
$name = $permissionEnum->formatValue($permissionName);
$title = $permissionEnum->getTitle() . ' - ' . $permissionTitle;
$permissions[$name] = $title;
}
}
return $permissions;
}
public static function toArrayListCodes(): array
{
return \array_keys(self::toArrayList());
}
private function getBasePermissions(): array
{
return [
'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'),
'update' => __('permissions.Allowed to edit'),
'delete' => __('permissions.Allowed to delete'),
];
}
}

8
app/Enums/SystemRole.php Normal file
View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum SystemRole: string
{
case Admin = 'admin';
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Dto\User;
final class ManyRoleDtoException extends \Exception
{
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Repositories;
final class DataCaptchaRepositoryException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Rule;
final class RoleSyncPermissionsCommandHandlerException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Service;
final class GenerateTokenCommandException extends \Exception
{
}

39
app/Helpers/Helpers.php Normal file
View File

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace App\Helpers;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Collection;
final readonly class Helpers
{
public static function getTimeZoneList(): Collection
{
return Cache::rememberForever('timezones_list_collection', function () {
$timezone = [];
foreach (timezone_identifiers_list(\DateTimeZone::ALL) as $key => $value) {
$timezone[$value] = $value . ' (UTC ' . now($value)->format('P') . ')';
}
return collect($timezone)->sortKeys();
});
}
public static function getUserTimeZone() {
return auth()->user()?->timezone ?? config('app.user_timezone');
}
public static function isDemoMode(): bool
{
return config('app.demo_mode', false);
}
public static function isDemoModeAndUserDenyUpdate(User $user): bool
{
if (self::isDemoMode() !== true) {
return false;
}
return $user->email === config('app.demo_email');
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\Api\V1\Captcha\CaptchaRequest;
use App\Http\Requests\Api\V1\Captcha\CheckingRequest;
use App\Http\Requests\Api\V1\Captcha\VerificationInformationRequest;
use App\Http\Resources\Api\V1\Captcha;
use App\Http\Resources\Api\V1\CaptchaVerificationInformation;
use App\Http\Resources\Api\V1\CaptchaVerified;
use App\Services\Api\V1\CaptchaService;
use Illuminate\Http\JsonResponse;
final class CaptchaController extends Controller
{
public function __construct(
private readonly CaptchaService $captchaService
) { }
public function getCaptcha(CaptchaRequest $request): JsonResponse
{
$captchaPublicToken = $request->getDto();
$expires = now()->addSeconds(config('captcha.waiting_for_captcha_verification_in_seconds'));
$result = $this->captchaService->createKeyWithCaptcha($captchaPublicToken, $expires);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new Captcha($result));
}
public function checking(CheckingRequest $request): JsonResponse
{
$maxCountError = config('captcha.validate_max_count_errors');
$params = $request->getDto();
$result = $this->captchaService->checking($params, $maxCountError);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new CaptchaVerified($result));
}
public function verificationInformation(string $captchaUuid, VerificationInformationRequest $request): JsonResponse
{
$params = $request->getDto();
$expiresMinutes = config('captcha.verification_data_view_limit_in_minutes');
$maxInfoDisplayCount = config('captcha.max_info_display_count');
$result = $this->captchaService->verificationInformation($captchaUuid, $params, $expiresMinutes, $maxInfoDisplayCount);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new CaptchaVerificationInformation($result));
}
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

View File

@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\AuthorizationRequest;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
final class AuthController extends Controller
{
public function __construct(
private readonly AuthService $authService
) { }
public function login(): View
{
return view('public/login');
}
public function authorization(AuthorizationRequest $request): RedirectResponse
{
$authorization = $request->getDto();
$result = $this->authService->authorization($authorization);
if ($result->isError()) {
if ($result->getCode() === Response::HTTP_UNAUTHORIZED) {
Log::warning('Unauthorized ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
}
return redirect()->route('login')->withInput()->withErrors($result->getMessage());
}
$request->session()->regenerate();
Log::notice('Logged in ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
return redirect()->route('home');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(route('login'));
}
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@ -0,0 +1,93 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\CaptchaTokens\IndexRequest;
use App\Http\Requests\Private\CaptchaTokens\StoreUpdateRequest;
use App\Services\Private\CaptchaTokenService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class CaptchaTokensController extends Controller
{
public function __construct(
private readonly CaptchaTokenService $captchaTokenService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->captchaTokenService->index($data->getCaptchaTokenDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->captchaTokenService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->captchaTokenService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->captchaTokenService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->captchaTokenService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->captchaTokenService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.index')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
final protected function errors(ServiceResultErrorContract $result): never
{
if ($result->getCode() === Response::HTTP_UNPROCESSABLE_ENTITY) {
redirect()->back()->withInput()->withErrors($result->getErrors());
exit;
}
abort($result->getCode(), $result->getMessage());
}
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use Illuminate\View\View;
final class DashboardController extends Controller
{
public function index(): View
{
return view('private/dashboard/index');
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Enums\Lang;
use App\Helpers\Helpers;
use App\Http\Requests\Private\Profile\UpdatePasswordRequest;
use App\Http\Requests\Private\Profile\UpdateRequest;
use App\Http\Requests\Private\Profile\UpdateSettingsRequest;
use App\Services\Private\ProfileService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ProfileController extends Controller
{
public function __construct(
private readonly ProfileService $profileService
) { }
public function profile(Request $request): View
{
return view('private/profile/profile', [
'user' => $request->user()
]);
}
public function settings(Request $request): View
{
return view('private/profile/settings', [
'user' => $request->user(),
'languages' => Lang::toCollection()->pluck(value: 'title', key: 'value')->toArray(),
'timezone' => Helpers::getTimeZoneList()->toArray(),
]);
}
public function update(UpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->update($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updatePassword(UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updatePassword($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updateSettings(UpdateSettingsRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updateSettings($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.settings')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\Roles\IndexRequest;
use App\Http\Requests\Private\Roles\StoreUpdateRequest;
use App\Services\Private\RoleService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class RolesController extends Controller
{
public function __construct(
private readonly RoleService $roleService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->roleService->index($data->getRoleBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/roles/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->roleService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/roles/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->roleService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/roles/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->roleService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('roles.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->roleService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('roles.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->roleService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('roles.index')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,106 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\Users\IndexRequest;
use App\Http\Requests\Private\Users\StoreUpdateRequest;
use App\Http\Requests\Private\Users\UpdatePasswordRequest;
use App\Services\Private\UserService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UsersController extends Controller
{
public function __construct(
private readonly UserService $userService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->userService->index($data->getUserBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->userService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->userService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function updatePassword(int $id, UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->updatePassword($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->userService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.index')->withSuccess($result->getMessage());
}
}

68
app/Http/Kernel.php Normal file
View File

@ -0,0 +1,68 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
'verified' => \App\Http\Middleware\EnsureUserIsVerified::class,
'user.locale' => \App\Http\Middleware\UserLocale::class,
];
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
final class EnsureUserIsVerified
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $redirectToRoute
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse|null
*/
public function handle($request, Closure $next, $redirectToRoute = null)
{
if (! $request->user()) {
return $request->expectsJson()
? abort(403)
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
if ($request->user() instanceof MustVerifyEmail && ! $request->user()->hasVerifiedEmail()) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
if ($request->user()->is_active === false) {
return $request->expectsJson()
? abort(403, 'User disabled.')
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
return $next($request);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
final class UserLocale
{
public function handle(Request $request, Closure $next)
{
if ($request->user() && $request->user()->lang) {
App::setLocale($request->user()->lang->getLocale());
}
return $next($request);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\CaptchaPublicToken;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class CaptchaRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->hasHeader('public-token')) {
return false;
}
$captchaToken = $captchaTokenRepository->getCaptchaTokenByPublicToken($this->header('public-token'));
if (is_null($captchaToken)) {
return false;
}
$this->captchaToken = $captchaToken;
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
];
}
public function getDto(): CaptchaPublicToken
{
$httpUserData = new HttpUserData(
$this->getClientIp(),
$this->userAgent(),
$this->header('referer')
);
return new CaptchaPublicToken(
$this->captchaToken,
$httpUserData
);
}
}

View File

@ -0,0 +1,68 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\CaptchaPublicToken;
use App\Dto\Request\Api\V1\Captcha\CheckingDto;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class CheckingRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->hasHeader('public-token')) {
return false;
}
$captchaToken = $captchaTokenRepository->getCaptchaTokenByPublicToken($this->header('public-token'));
if (is_null($captchaToken)) {
return false;
}
$this->captchaToken = $captchaToken;
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'captcha_key' => ['required', 'string', 'max:75'],
'verification' => ['required', 'array'],
'verification.*' => ['required', 'array', 'size:2'],
'verification.*.x' => ['required', 'numeric', 'min:0'],
'verification.*.y' => ['required', 'numeric', 'min:0'],
];
}
public function getDto(): CheckingDto
{
$httpUserData = new HttpUserData(
$this->getClientIp(),
$this->userAgent(),
$this->header('referer')
);
$captchaPublicToken = new CaptchaPublicToken(
$this->captchaToken,
$httpUserData
);
return new CheckingDto(
captchaPublicToken: $captchaPublicToken,
captchaKey: $this->input('captcha_key'),
coordinators: $this->input('verification'),
);
}
}

View File

@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\VerificationInformationDto;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class VerificationInformationRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->hasHeader('private-token')) {
return false;
}
$captchaToken = $captchaTokenRepository->getCaptchaTokenByPrivateToken($this->header('private-token'));
if (is_null($captchaToken)) {
return false;
}
$this->captchaToken = $captchaToken;
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'user_agent' => ['nullable', 'string'],
];
}
public function getDto(): VerificationInformationDto
{
$httpUserData = new HttpUserData(
$this->getClientIp(),
$this->userAgent(),
$this->header('referer')
);
return new VerificationInformationDto(
captchaToken: $this->captchaToken,
httpUserData: $httpUserData,
userAgent: $this->input('user_agent', null),
);
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Http\Requests;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Authorization;
use Illuminate\Foundation\Http\FormRequest;
final class AuthorizationRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'],
'remember' => ['nullable', 'boolean'],
];
}
public function getDto(): Authorization
{
return new Authorization(
email: $this->input('email'),
password: $this->input('password'),
remember: (bool) $this->input('remember', false)
);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\CaptchaTokens;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\CaptchaToken;
use App\Dto\Request\Private\CaptchaToken\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('users.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
captchaTokenDto: new CaptchaToken(),
page: (int) $this->input('page', 1)
);
}
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\CaptchaTokens;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\CaptchaToken\StoreUpdate;
use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => ['required', 'max:255'],
];
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
title: $this->input('title'),
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', 'confirmed', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\Profile\Update;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'max:255'],
];
}
public function getDto(): Update
{
return new Update(name: $this->input('name'));
}
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\Profile\UpdateSettings;
use App\Enums\Lang;
use App\Helpers\Helpers;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
final class UpdateSettingsRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'lang' => ['nullable', new Enum(Lang::class)],
'timezone' => ['nullable', Rule::in(Helpers::getTimeZoneList()->keys()->toArray())]
];
}
public function getDto(): UpdateSettings
{
$lang = $this->input('lang', null);
if (!is_null($lang)) {
$lang = Lang::from((int) $lang);
}
return new UpdateSettings(
lang: $lang,
timezone: $this->input('timezone', null),
);
}
}

Some files were not shown because too many files have changed in this diff Show More