Версия 0.1.0-beta #1
| @@ -1,6 +1,7 @@ | |||||||
| DOCKER_APP_PORT=8080 | DOCKER_APP_PORT=8080 | ||||||
| DOCKER_CAPTCHA_PORT=8081 | DOCKER_CAPTCHA_PORT=8081 | ||||||
| DOCKER_CAPTCHA_WEBSOCKET_PORT=8082 | DOCKER_CAPTCHA_WEBSOCKET_PORT=8082 | ||||||
|  | DOCKER_REGISTRY_WEB=8083 | ||||||
| DOCKER_DB_PORT=3306 | DOCKER_DB_PORT=3306 | ||||||
| MYSQL_ROOT_PASSWORD=root_pass | MYSQL_ROOT_PASSWORD=root_pass | ||||||
| DB_DATABASE=registry | DB_DATABASE=registry | ||||||
|   | |||||||
| @@ -1,11 +1,19 @@ | |||||||
| APP_NAME=Laravel | APP_NAME=Laravel | ||||||
| APP_ENV=local | APP_ENV=local | ||||||
| APP_KEY=base64:LAXuVFnfcrPlPcYftpV/hJ7mEO1TlbriMaDN7hT5WOo= | APP_KEY= | ||||||
| APP_DEBUG=true | APP_DEBUG=true | ||||||
| DEBUGBAR_OPEN_STORAGE=true | DEBUGBAR_OPEN_STORAGE=true | ||||||
| APP_TIMEZONE=UTC | APP_TIMEZONE=UTC | ||||||
| APP_URL= | APP_URL= | ||||||
|  |  | ||||||
|  | REGISTRY_SERVICE_NAME=container_registry | ||||||
|  | REGISTRY_SERVICE_HTTP=http://real_address_registry | ||||||
|  | REGISTRY_PRIVATE_KEY_NAME=registry-auth1.key | ||||||
|  | REGISTRY_ISSUER=token_issuer | ||||||
|  | REGISTRY_ALGORITHM=RS256 | ||||||
|  | REGISTRY_EXPIRES_IN_SECONDS=600 | ||||||
|  | REGISTRY_TOKEN_FOR_NOTIFICATIONS=token_for_notifications | ||||||
|  |  | ||||||
| APP_CAPTCHA=false | APP_CAPTCHA=false | ||||||
| CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081 | CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081 | ||||||
| CAPTCHA_PRIVATE_TOKEN= | CAPTCHA_PRIVATE_TOKEN= | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Contracts\Services\Registry; | ||||||
|  | 
 | ||||||
|  | interface Base32 | ||||||
|  | { | ||||||
|  |     public function encode(string $input, bool $padding = true): string; | ||||||
|  |     public function decode(string $input): string; | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								app/application/app/Dto/Builder/Repository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/application/app/Dto/Builder/Repository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Builder; | ||||||
|  | 
 | ||||||
|  | final readonly class Repository | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private ?bool $isPublic, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getIsPublic(): ?bool | ||||||
|  |     { | ||||||
|  |         return $this->isPublic; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,9 +10,9 @@ final readonly class StoreUpdate extends Dto | |||||||
|     public function __construct( |     public function __construct( | ||||||
|         private string $name, |         private string $name, | ||||||
|         private string $email, |         private string $email, | ||||||
|         private string $username, |  | ||||||
|         private bool   $isActive, |         private bool   $isActive, | ||||||
|         private ManyRoleDto $roles, |         private ManyRoleDto $roles, | ||||||
|  |         private ?string $username = null, | ||||||
|         private ?string $password = null |         private ?string $password = null | ||||||
|     ) { } |     ) { } | ||||||
| 
 | 
 | ||||||
| @@ -36,7 +36,7 @@ final readonly class StoreUpdate extends Dto | |||||||
|         return $this->roles; |         return $this->roles; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function getUsername(): string |     public function getUsername(): ?string | ||||||
|     { |     { | ||||||
|         return $this->username; |         return $this->username; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2; | ||||||
|  | 
 | ||||||
|  | final readonly class AuthorizationConfig | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $serviceName, | ||||||
|  |         private string $privateKeyName, | ||||||
|  |         private string $issuer, | ||||||
|  |         private string $algorithm, | ||||||
|  |         private int    $expiresInSeconds, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getServiceName(): string | ||||||
|  |     { | ||||||
|  |         return $this->serviceName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getPrivateKeyName(): string | ||||||
|  |     { | ||||||
|  |         return $this->privateKeyName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getIssuer(): string | ||||||
|  |     { | ||||||
|  |         return $this->issuer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getAlgorithm(): string | ||||||
|  |     { | ||||||
|  |         return $this->algorithm; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getExpiresInSeconds(): int | ||||||
|  |     { | ||||||
|  |         return $this->expiresInSeconds; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\AuthorizationService; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Dto; | ||||||
|  | use App\Models\User; | ||||||
|  | 
 | ||||||
|  | final readonly class Authorization extends Dto | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $service, | ||||||
|  |         private ?Scope $scope, | ||||||
|  |         private ?User  $user = null, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getService(): string | ||||||
|  |     { | ||||||
|  |         return $this->service; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getScope(): ?Scope | ||||||
|  |     { | ||||||
|  |         return $this->scope; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getUser(): ?User | ||||||
|  |     { | ||||||
|  |         return $this->user; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\AuthorizationService; | ||||||
|  | 
 | ||||||
|  | final readonly class Scope | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $username, | ||||||
|  |         private string $repositoryName, | ||||||
|  |         private array  $actions = [], | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public static function parse(string $scope): Scope | ||||||
|  |     { | ||||||
|  |         [$none, $repository, $actions] = explode(':', $scope, 3); | ||||||
|  |         unset($none); | ||||||
|  |         [$username, $repositoryName] = explode('/', $repository, 2); | ||||||
|  | 
 | ||||||
|  |         return new Scope( | ||||||
|  |             username: $username, | ||||||
|  |             repositoryName: $repositoryName, | ||||||
|  |             actions: explode(',', $actions), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public function getUsername(): string | ||||||
|  |     { | ||||||
|  |         return $this->username; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getRepositoryName(): string | ||||||
|  |     { | ||||||
|  |         return $this->repositoryName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getActions(): array | ||||||
|  |     { | ||||||
|  |         return $this->actions; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\EventService; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Dto; | ||||||
|  | 
 | ||||||
|  | final readonly class DataProcessing extends Dto | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private Events $events | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getEvents(): Events | ||||||
|  |     { | ||||||
|  |         return $this->events; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\EventService; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Dto; | ||||||
|  | 
 | ||||||
|  | final readonly class Event extends Dto | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $timestamp, | ||||||
|  |         private string $action, | ||||||
|  |         private array $target, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getTimestamp(): string | ||||||
|  |     { | ||||||
|  |         return $this->timestamp; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getAction(): string | ||||||
|  |     { | ||||||
|  |         return $this->action; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getTarget(): array | ||||||
|  |     { | ||||||
|  |         return $this->target; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\EventService; | ||||||
|  | 
 | ||||||
|  | final class Events | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @var array [...Event] | ||||||
|  |      */ | ||||||
|  |     private array $events; | ||||||
|  | 
 | ||||||
|  |     public function addEvent(Event $event): void | ||||||
|  |     { | ||||||
|  |         $this->events[] = $event; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getEvents(): array | ||||||
|  |     { | ||||||
|  |         return $this->events; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\EventService; | ||||||
|  | 
 | ||||||
|  | final readonly class ImageDataPull | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $username, | ||||||
|  |         private string $repositoryName, | ||||||
|  |         private string $tag, | ||||||
|  |         private string $digest, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getUsername(): string | ||||||
|  |     { | ||||||
|  |         return $this->username; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getRepositoryName(): string | ||||||
|  |     { | ||||||
|  |         return $this->repositoryName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getTag(): string | ||||||
|  |     { | ||||||
|  |         return $this->tag; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDigest(): string | ||||||
|  |     { | ||||||
|  |         return $this->digest; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Registry\V2\EventService; | ||||||
|  | 
 | ||||||
|  | final readonly class ImageDataPush | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $username, | ||||||
|  |         private string $repositoryName, | ||||||
|  |         private string $tag, | ||||||
|  |         private string $digest, | ||||||
|  |         private int    $size, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getUsername(): string | ||||||
|  |     { | ||||||
|  |         return $this->username; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getRepositoryName(): string | ||||||
|  |     { | ||||||
|  |         return $this->repositoryName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getTag(): string | ||||||
|  |     { | ||||||
|  |         return $this->tag; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDigest(): string | ||||||
|  |     { | ||||||
|  |         return $this->digest; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getSize(): int | ||||||
|  |     { | ||||||
|  |         return $this->size; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								app/application/app/Dto/Service/Site/AccessTokens/Index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/application/app/Dto/Service/Site/AccessTokens/Index.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Site\AccessTokens; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Pages; | ||||||
|  | 
 | ||||||
|  | final readonly class Index extends Pages | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         int $page | ||||||
|  |     ) { | ||||||
|  |         parent::__construct($page); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Site\AccessTokens; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Dto; | ||||||
|  | use App\Enums\AccessTokenPermission; | ||||||
|  | 
 | ||||||
|  | final readonly class StoreUpdate extends Dto | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $name, | ||||||
|  |         private AccessTokenPermission $permission, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getName(): string | ||||||
|  |     { | ||||||
|  |         return $this->name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getPermission(): AccessTokenPermission | ||||||
|  |     { | ||||||
|  |         return $this->permission; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Site\Repository; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Pages; | ||||||
|  | 
 | ||||||
|  | final readonly class Repositories extends Pages | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         int $page | ||||||
|  |     ) { | ||||||
|  |         parent::__construct($page); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Site\Repository; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Dto; | ||||||
|  | 
 | ||||||
|  | final readonly class StoreUpdate extends Dto | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private bool    $isPublic, | ||||||
|  |         private ?string $description, | ||||||
|  |         private ?string $overview, | ||||||
|  |         private ?string $name = null, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getDescription(): ?string | ||||||
|  |     { | ||||||
|  |         return $this->description; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getOverview(): ?string | ||||||
|  |     { | ||||||
|  |         return $this->overview; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function isPublic(): bool | ||||||
|  |     { | ||||||
|  |         return $this->isPublic; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getName(): ?string | ||||||
|  |     { | ||||||
|  |         return $this->name; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								app/application/app/Dto/Service/Site/TagRepository/Tags.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/application/app/Dto/Service/Site/TagRepository/Tags.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Site\TagRepository; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Pages; | ||||||
|  | 
 | ||||||
|  | final readonly class Tags extends Pages | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         int $page | ||||||
|  |     ) { | ||||||
|  |         parent::__construct($page); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								app/application/app/Dto/Service/Site/User/User.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/application/app/Dto/Service/Site/User/User.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\Site\User; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Pages; | ||||||
|  | 
 | ||||||
|  | final readonly class User extends Pages | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         int $page | ||||||
|  |     ) { | ||||||
|  |         parent::__construct($page); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								app/application/app/Enums/AccessTokenPermission.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/application/app/Enums/AccessTokenPermission.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Enums; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Support\Collection; | ||||||
|  | 
 | ||||||
|  | enum AccessTokenPermission: string | ||||||
|  | { | ||||||
|  |     case RepoReadWriteDelete = 'repo:read.write.delete'; | ||||||
|  |     case RepoReadWrite       = 'repo:read.write'; | ||||||
|  |     case RepoRead            = 'repo:read'; | ||||||
|  | 
 | ||||||
|  |     public function getTitle(): string | ||||||
|  |     { | ||||||
|  |         return __('access-token-permission.' . $this->name); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function toArray(): array | ||||||
|  |     { | ||||||
|  |         $choices = []; | ||||||
|  |         foreach (self::cases() as $lang) { | ||||||
|  |             $choices[] = [ | ||||||
|  |                 'name' => $lang->name, | ||||||
|  |                 'value' => $lang->value, | ||||||
|  |                 'title' => $lang->getTitle(), | ||||||
|  |             ]; | ||||||
|  |         } | ||||||
|  |         return $choices; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function toCollection(): Collection | ||||||
|  |     { | ||||||
|  |         return collect(self::toArray()); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								app/application/app/Enums/CacheTag.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/application/app/Enums/CacheTag.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Enums; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Cache\TaggedCache; | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | 
 | ||||||
|  | enum CacheTag: string | ||||||
|  | { | ||||||
|  |     case RegistryV2PrivateKey = 'registry_v2_private_key'; | ||||||
|  | 
 | ||||||
|  |     public function getCache(): TaggedCache | ||||||
|  |     { | ||||||
|  |         return Cache::tags($this->value); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								app/application/app/Enums/Morph.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/application/app/Enums/Morph.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Enums; | ||||||
|  | 
 | ||||||
|  | use App\Models\User; | ||||||
|  | 
 | ||||||
|  | enum Morph: int | ||||||
|  | { | ||||||
|  |     case User = 1; | ||||||
|  | 
 | ||||||
|  |     public function getPathModel(): string | ||||||
|  |     { | ||||||
|  |         return match ($this) { | ||||||
|  |             self::User => User::class, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getFolderName(): string | ||||||
|  |     { | ||||||
|  |         return $this->name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function map(): array | ||||||
|  |     { | ||||||
|  |         $map = []; | ||||||
|  |         foreach (self::cases() as $item) { | ||||||
|  |             $map[$item->value] = $item->getPathModel(); | ||||||
|  |         } | ||||||
|  |         return $map; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Exceptions\Jobs\Registry\V2; | ||||||
|  | 
 | ||||||
|  | final class JobException extends \Exception | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Exceptions\Services\Registry\V2\ArchitectureCommand; | ||||||
|  | 
 | ||||||
|  | final class BlobException extends \Exception | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Exceptions\Services\Registry\V2\ArchitectureCommand; | ||||||
|  | 
 | ||||||
|  | final class ManifestException extends \Exception | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Exceptions\Services\Registry\V2\EventService; | ||||||
|  | 
 | ||||||
|  | final class DataProcessingException extends \Exception | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @@ -32,4 +32,15 @@ final readonly class Helpers | |||||||
|             ->rtrim('.') |             ->rtrim('.') | ||||||
|             ->value(); |             ->value(); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static function shortDigest(string $digest): string | ||||||
|  |     { | ||||||
|  |         $digest = Str::of($digest)->explode(':', 2)->last(); | ||||||
|  |         return Str::of($digest)->take(12)->toString(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function dockerServiceAddress(): string | ||||||
|  |     { | ||||||
|  |         return Str::of(config('registry.service_http'))->remove(['http://', 'https://'])->toString(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								app/application/app/Helpers/Number.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/application/app/Helpers/Number.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Helpers; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Support\Number as LaravelNumber; | ||||||
|  | 
 | ||||||
|  | final readonly class Number | ||||||
|  | { | ||||||
|  |     private const array ABBREVIATE_UNITS = [ | ||||||
|  |         3  => 'K', | ||||||
|  |         6  => 'M', | ||||||
|  |         9  => 'B', | ||||||
|  |         12 => 'T', | ||||||
|  |         15 => 'Q', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Convert the number to its human-readable equivalent. | ||||||
|  |      * | ||||||
|  |      * @param int|float $number | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public static function abbreviate(int|float $number): string | ||||||
|  |     { | ||||||
|  |         if ($number < 0) { | ||||||
|  |             throw new \InvalidArgumentException('Number cannot be negative'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return self::summarize($number); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * $size - Size in bytes | ||||||
|  |      * @param int $size | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public static function sizeForHumans(int $size): string | ||||||
|  |     { | ||||||
|  |         return LaravelNumber::fileSize($size); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static function summarize(int|float $number, bool $hidePlusSymbol = false): string | ||||||
|  |     { | ||||||
|  |         $symbol = '+'; | ||||||
|  |         if ($hidePlusSymbol) { | ||||||
|  |             $symbol = ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($number < 1000) { | ||||||
|  |             return (string) $number; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($number >= 1e15) { | ||||||
|  |             $units = self::ABBREVIATE_UNITS; | ||||||
|  |             return sprintf('%s' . end($units), self::summarize($number / 1e15, hidePlusSymbol: true)) . $symbol; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $numberExponent = \floor(\log10($number)); | ||||||
|  |         $displayExponent = $numberExponent - ($numberExponent % 3); | ||||||
|  |         $number /= \pow(10, $displayExponent); | ||||||
|  |         $number = \explode('.', (string) $number, 2); | ||||||
|  | 
 | ||||||
|  |         $remainder = $number[1] ?? 0; | ||||||
|  |         $number    = $number[0] ?? 0; | ||||||
|  | 
 | ||||||
|  |         if ($remainder === 0) { | ||||||
|  |             $symbol = ''; | ||||||
|  |         } | ||||||
|  |         $abbreviated = self::ABBREVIATE_UNITS[$displayExponent] ?? ''; | ||||||
|  | 
 | ||||||
|  |         return (string) $number . $abbreviated . $symbol; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,7 +2,17 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Http\Controllers; | namespace App\Http\Controllers; | ||||||
| 
 | 
 | ||||||
|  | use App\Contracts\ServiceResultError as ServiceResultErrorContract; | ||||||
|  | use Illuminate\Http\Response; | ||||||
|  | 
 | ||||||
| abstract class Controller | abstract class Controller | ||||||
| { | { | ||||||
|     //
 |     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()); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								app/application/app/Http/Controllers/Registry/Controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/application/app/Http/Controllers/Registry/Controller.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Controllers\Registry; | ||||||
|  | 
 | ||||||
|  | use App\Http\Controllers\Controller as BaseController; | ||||||
|  | 
 | ||||||
|  | abstract class Controller extends BaseController | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								app/application/app/Http/Controllers/Registry/Version2.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/application/app/Http/Controllers/Registry/Version2.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Controllers\Registry; | ||||||
|  | 
 | ||||||
|  | use App\Http\Requests\Registry\Version2\AuthRequest; | ||||||
|  | use App\Http\Requests\Registry\Version2\EventRequest; | ||||||
|  | use App\Services\Registry\V2\AuthorizationService; | ||||||
|  | use App\Services\Registry\V2\EventService; | ||||||
|  | use Illuminate\Http\JsonResponse; | ||||||
|  | use Illuminate\Support\Facades\Log; | ||||||
|  | 
 | ||||||
|  | final class Version2 extends Controller | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly AuthorizationService $authorizationService, | ||||||
|  |         private readonly EventService         $eventService, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function auth(AuthRequest $request): JsonResponse | ||||||
|  |     { | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $result = $this->authorizationService->authorization($data); | ||||||
|  |         if (!$result->isSuccess()) { | ||||||
|  |             return response()->json($result->getData())->setStatusCode($result->getCode()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return \response()->json($result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function event(EventRequest $request): JsonResponse | ||||||
|  |     { | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $result = $this->eventService->dataProcessing($data); | ||||||
|  |         if (!$result->isSuccess()) { | ||||||
|  |             return response()->json($result->getData())->setStatusCode($result->getCode()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return \response()->json(['message' => $result->getMessage()]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Controllers\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Http\Requests\Site\AccessTokens\IndexRequest; | ||||||
|  | use App\Http\Requests\Site\AccessTokens\StoreUpdateRequest; | ||||||
|  | use App\Http\Resources\Site\AccessTokens\Store; | ||||||
|  | use App\Services\Site\AccessTokenService; | ||||||
|  | use Illuminate\Http\JsonResponse; | ||||||
|  | use Illuminate\Http\RedirectResponse; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | 
 | ||||||
|  | final class AccessTokensController extends Controller | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly AccessTokenService $accessTokenService | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function index(IndexRequest $request): View | ||||||
|  |     { | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $querySettingsDto = new QuerySettingsDto( | ||||||
|  |             limit: 20, | ||||||
|  |             page: $data->getPage(), | ||||||
|  |             queryWith: [] | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         $result = $this->accessTokenService->index($querySettingsDto, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.access-tokens.index', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function create(Request $request): View | ||||||
|  |     { | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->accessTokenService->create($user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.access-tokens.create', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function edit(int $id, Request $request): View | ||||||
|  |     { | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->accessTokenService->edit($id, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.access-tokens.edit', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function store(StoreUpdateRequest $request): JsonResponse | ||||||
|  |     { | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->accessTokenService->store($data, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return response()->json($result->getData())->setStatusCode($result->getCode()); | ||||||
|  |         } | ||||||
|  |         return response()->json(new Store($result)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function update(int $id, StoreUpdateRequest $request): RedirectResponse | ||||||
|  |     { | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->accessTokenService->update($id, $data, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return redirect()->route('profile.access-tokens.edit', $result->getModel())->withSuccess($result->getMessage()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function destroy(int $id, Request $request): RedirectResponse | ||||||
|  |     { | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->accessTokenService->destroy($id, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return redirect()->route('profile.access-tokens.index')->withSuccess($result->getMessage()); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,7 +2,9 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Http\Controllers\Site; | namespace App\Http\Controllers\Site; | ||||||
| 
 | 
 | ||||||
| abstract class Controller | use App\Http\Controllers\Controller as BaseController; | ||||||
|  | 
 | ||||||
|  | abstract class Controller extends BaseController | ||||||
| { | { | ||||||
|     //
 | 
 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +0,0 @@ | |||||||
| <?php declare(strict_types=1); |  | ||||||
| 
 |  | ||||||
| namespace App\Http\Controllers\Site; |  | ||||||
| 
 |  | ||||||
| use Illuminate\View\View; |  | ||||||
| 
 |  | ||||||
| final class HomeController extends Controller |  | ||||||
| { |  | ||||||
|     public function index(): View |  | ||||||
|     { |  | ||||||
|         return \view('site.home.index'); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,96 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Controllers\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Http\Requests\Site\Repositories\RepositoriesRequest; | ||||||
|  | use App\Http\Requests\Site\Repositories\StoreUpdateRequest; | ||||||
|  | use App\Services\Site\RepositoryService; | ||||||
|  | use Illuminate\Http\RedirectResponse; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | 
 | ||||||
|  | final class RepositoriesController extends Controller | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly RepositoryService $repositoryService, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function repositories(RepositoriesRequest $request): View | ||||||
|  |     { | ||||||
|  |         $querySettingsDto = new QuerySettingsDto( | ||||||
|  |             limit: 20, | ||||||
|  |             page:  $request->getDto()->getPage(), | ||||||
|  |             queryWith: ['user'] | ||||||
|  |         ); | ||||||
|  |         $result = $this->repositoryService->repositories($querySettingsDto); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.repositories.repositories', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function repository(string $username, string $repositoryName, Request $request): View | ||||||
|  |     { | ||||||
|  |         $result = $this->repositoryService->repository($username, $repositoryName, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.repositories.repository', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function create(string $username, Request $request): View | ||||||
|  |     { | ||||||
|  |         $result = $this->repositoryService->create($username, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.repositories.create', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function edit(string $username, string $repositoryName, Request $request) | ||||||
|  |     { | ||||||
|  |         $result = $this->repositoryService->edit($username, $repositoryName, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.repositories.edit', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function store(string $username, StoreUpdateRequest $request): RedirectResponse | ||||||
|  |     { | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $result = $this->repositoryService->store($username, $data, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return redirect()->route('repository.edit', ['repository_name' => $result->getModel()->name, 'username' => $username])->withSuccess($result->getMessage()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function update(string $username, string $repositoryName, StoreUpdateRequest $request): RedirectResponse | ||||||
|  |     { | ||||||
|  |         $data = $request->getDto(); | ||||||
|  |         $result = $this->repositoryService->update($username, $repositoryName, $data, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return redirect()->route('repository.edit', ['repository_name' => $result->getModel()->name, 'username' => $username])->withSuccess($result->getMessage()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function destroy(string $username, string $repositoryName, Request $request): RedirectResponse | ||||||
|  |     { | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->repositoryService->destroy($username, $repositoryName, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return redirect()->route('user.repositories', ['username' => $username])->withSuccess($result->getMessage()); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,42 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Controllers\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Http\Requests\Site\TagRepositories\TagsRequest; | ||||||
|  | use App\Services\Site\TagRepositoryService; | ||||||
|  | use Illuminate\Http\RedirectResponse; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | 
 | ||||||
|  | final class TagRepositoriesController extends Controller | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly TagRepositoryService $tagRepositoryService, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function tags(string $username, string $repositoryName, TagsRequest $request): View | ||||||
|  |     { | ||||||
|  |         $querySettingsDto = new QuerySettingsDto( | ||||||
|  |             limit: 20, | ||||||
|  |             page:  $request->getDto()->getPage(), | ||||||
|  |             queryWith: ['repository', 'architecture'] | ||||||
|  |         ); | ||||||
|  |         $result = $this->tagRepositoryService->tags($username, $repositoryName, $querySettingsDto, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.repositories.tags', $result->getData()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function destroy(string $username, string $repositoryName, int $tagId, TagsRequest $request): RedirectResponse | ||||||
|  |     { | ||||||
|  |         $user = $request->user(); | ||||||
|  |         $result = $this->tagRepositoryService->destroy($username, $repositoryName, $tagId, $user); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return redirect()->route('repository.tags', ['username' => $username, 'repository_name' => $repositoryName])->withSuccess($result->getMessage()); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								app/application/app/Http/Controllers/Site/UserController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/application/app/Http/Controllers/Site/UserController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Controllers\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Http\Requests\Site\User\UserRequest; | ||||||
|  | use App\Services\Site\UserService; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | 
 | ||||||
|  | final class UserController extends Controller | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly UserService $userService, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function user(string $username, UserRequest $request): View | ||||||
|  |     { | ||||||
|  |         $querySettingsDto = new QuerySettingsDto( | ||||||
|  |             limit: 20, | ||||||
|  |             page:  $request->getDto()->getPage(), | ||||||
|  |             queryWith: ['user'] | ||||||
|  |         ); | ||||||
|  |         $result = $this->userService->repositories($username, $querySettingsDto, $request->user()); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             $this->errors($result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return view('site.user.user', $result->getData()); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								app/application/app/Http/Middleware/RegistryAuth.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								app/application/app/Http/Middleware/RegistryAuth.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Middleware; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Auth\AuthenticationException; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Closure; | ||||||
|  | use Illuminate\Http\Response; | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\RateLimiter; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | 
 | ||||||
|  | final class RegistryAuth | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @throws AuthenticationException | ||||||
|  |      */ | ||||||
|  |     public function handle(Request $request, Closure $next) | ||||||
|  |     { | ||||||
|  |         if (RateLimiter::tooManyAttempts(key: 'registry-auth:' . $request->getClientIp(), maxAttempts: 15)) { | ||||||
|  |             $message = __('http-statuses.' . Response::HTTP_TOO_MANY_REQUESTS, [], 'en'); | ||||||
|  |             \abort( | ||||||
|  |                 new Response($message, Response::HTTP_TOO_MANY_REQUESTS) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($request->hasHeader('authorization') && $this->authenticate($request) === false) { | ||||||
|  |             RateLimiter::increment(key: 'registry-auth:' . $request->getClientIp(), decaySeconds: 3600); | ||||||
|  |             throw new \Illuminate\Auth\AuthenticationException(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $next($request); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function authenticate(Request $request): bool | ||||||
|  |     { | ||||||
|  |         $basicHeader = $this->getHeaderBasic($request); | ||||||
|  |         $authenticated = base64_decode($basicHeader); | ||||||
|  |         if ($authenticated === false) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $authenticated = explode(':', $authenticated, 2); | ||||||
|  |         if (count($authenticated) !== 2) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         [$username, $token] = $authenticated; | ||||||
|  | 
 | ||||||
|  |         $request->headers->set('authorization', 'Bearer ' . $token); | ||||||
|  |         Config::set('auth.defaults.guard', 'sanctum'); | ||||||
|  |         if ($request->user()?->username === Str::lower($username)) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getHeaderBasic(Request $request): string | ||||||
|  |     { | ||||||
|  |         $basicHeader = $request->header('authorization'); | ||||||
|  |         $position = strrpos($basicHeader, 'Basic '); | ||||||
|  | 
 | ||||||
|  |         if ($position !== false) { | ||||||
|  |             return substr($basicHeader, $position + 6); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $basicHeader; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Middleware; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Auth\AuthenticationException; | ||||||
|  | use Illuminate\Http\Response; | ||||||
|  | use Illuminate\Support\Facades\RateLimiter; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Closure; | ||||||
|  | use Illuminate\Routing\Exceptions\MissingRateLimiterException; | ||||||
|  | 
 | ||||||
|  | final class RegistryAuthNotification | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @throws AuthenticationException | MissingRateLimiterException | ||||||
|  |      */ | ||||||
|  |     public function handle(Request $request, Closure $next) | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |         if (RateLimiter::tooManyAttempts(key: 'registry-auth-notification:' . $request->getClientIp(), maxAttempts: 3)) { | ||||||
|  |             $message = __('http-statuses.' . Response::HTTP_TOO_MANY_REQUESTS, [], 'en'); | ||||||
|  |             \abort( | ||||||
|  |                 new Response($message, Response::HTTP_TOO_MANY_REQUESTS) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($request->header('authorization') !== config('registry.token_for_notifications')) { | ||||||
|  |             RateLimiter::increment(key: 'registry-auth-notification:' . $request->getClientIp(), decaySeconds: 600); | ||||||
|  |             throw new \Illuminate\Auth\AuthenticationException(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $next($request); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -20,13 +20,13 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto | |||||||
|         $rules = [ |         $rules = [ | ||||||
|             'name' => ['required', 'max:255'], |             'name' => ['required', 'max:255'], | ||||||
|             'email' => ['required', 'email', 'max:255'], |             'email' => ['required', 'email', 'max:255'], | ||||||
|             'username' => ['required', 'string', new Username()], |  | ||||||
|             'is_active' => ['required', 'boolean'], |             'is_active' => ['required', 'boolean'], | ||||||
|             'roles' => ['array', Rule::exists('roles', 'id')], |             'roles' => ['array', Rule::exists('roles', 'id')], | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         if ($this->getMethod() === 'POST') { |         if ($this->getMethod() === 'POST') { | ||||||
|             $rules['password'] = ['required', Password::default()]; |             $rules['password'] = ['required', Password::default()]; | ||||||
|  |             $rules['username'] = ['required', 'string', new Username()]; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return $rules; |         return $rules; | ||||||
| @@ -38,9 +38,9 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto | |||||||
|         return new StoreUpdate( |         return new StoreUpdate( | ||||||
|             name: $this->input('name'), |             name: $this->input('name'), | ||||||
|             email: $this->input('email'), |             email: $this->input('email'), | ||||||
|             username: $this->input('username'), |  | ||||||
|             isActive: (bool) $this->input('is_active', false), |             isActive: (bool) $this->input('is_active', false), | ||||||
|             roles: new ManyRoleDto($this->input('roles', [])), |             roles: new ManyRoleDto($this->input('roles', [])), | ||||||
|  |             username: $this->input('username', null), | ||||||
|             password: $this->input('password', null), |             password: $this->input('password', null), | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Registry\Version2; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationService\Authorization; | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationService\Scope; | ||||||
|  | use Illuminate\Foundation\Http\FormRequest; | ||||||
|  | 
 | ||||||
|  | final class AuthRequest extends FormRequest implements FormRequestDto | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules that apply to the request. | ||||||
|  |      */ | ||||||
|  |     public function rules(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'service'   => ['required', 'string'], | ||||||
|  |             'scope'     => ['nullable', 'string', 'regex:/^repository\:[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*\:(push,pull|pull,push|pull)$/'], | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDto(): Authorization | ||||||
|  |     { | ||||||
|  |         $scope = $this->input('scope', null); | ||||||
|  |         if (! \is_null($scope)) { | ||||||
|  |             $scope = Scope::parse($scope); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new Authorization( | ||||||
|  |             service: $this->input('service'), | ||||||
|  |             scope:   $scope, | ||||||
|  |             user:    $this->user(), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,42 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Registry\Version2; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\DataProcessing; | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\Event; | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\Events; | ||||||
|  | use Illuminate\Foundation\Http\FormRequest; | ||||||
|  | 
 | ||||||
|  | final class EventRequest extends FormRequest implements FormRequestDto | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules that apply to the request. | ||||||
|  |      */ | ||||||
|  |     public function rules(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'events' => 'required|array', | ||||||
|  |             'events.*.timestamp' => 'required|string', | ||||||
|  |             'events.*.action' => 'required|string', | ||||||
|  |             'events.*.target' => 'required|array', | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDto(): DataProcessing | ||||||
|  |     { | ||||||
|  |         $events = new Events(); | ||||||
|  | 
 | ||||||
|  |         foreach ($this->input('events', []) as $event) { | ||||||
|  |             $events->addEvent( | ||||||
|  |                 new Event( | ||||||
|  |                     timestamp: $event['timestamp'] ?? '', | ||||||
|  |                     action:    $event['action'] ?? '', | ||||||
|  |                     target:    $event['target'] ?? [], | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new DataProcessing($events); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Site\AccessTokens; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Site\AccessTokens\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('profile.access-tokens.index'); | ||||||
|  |         return [ | ||||||
|  |             'page' => ['nullable', 'numeric', 'min:1'] | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDto(): Index | ||||||
|  |     { | ||||||
|  |         return new Index( | ||||||
|  |             page: (int) $this->input('page', 1) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Site\AccessTokens; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Site\AccessTokens\StoreUpdate; | ||||||
|  | use App\Enums\AccessTokenPermission; | ||||||
|  | use Illuminate\Foundation\Http\FormRequest; | ||||||
|  | use Illuminate\Validation\Rules\Enum; | ||||||
|  | 
 | ||||||
|  | final class StoreUpdateRequest extends FormRequest implements FormRequestDto | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules that apply to the request. | ||||||
|  |      */ | ||||||
|  |     public function rules(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'name'        => ['required', 'string', 'max:255'], | ||||||
|  |             'permissions' => ['required', 'string', new Enum(AccessTokenPermission::class)], | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public function getDto(): StoreUpdate | ||||||
|  |     { | ||||||
|  |         return new StoreUpdate( | ||||||
|  |             name: $this->input('name'), | ||||||
|  |             permission: AccessTokenPermission::from( | ||||||
|  |                 $this->input('permissions') | ||||||
|  |             ), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,27 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Site\Repositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Site\Repository\Repositories; | ||||||
|  | use Illuminate\Foundation\Http\FormRequest; | ||||||
|  | 
 | ||||||
|  | final class RepositoriesRequest extends FormRequest implements FormRequestDto | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules that apply to the request. | ||||||
|  |      */ | ||||||
|  |     public function rules(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'page' => ['nullable', 'numeric', 'min:1'] | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDto(): Repositories | ||||||
|  |     { | ||||||
|  |         return new Repositories( | ||||||
|  |             page: (int) $this->input('page', 1) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Site\Repositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Site\Repository\StoreUpdate; | ||||||
|  | use App\Rules\Repository; | ||||||
|  | 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 | ||||||
|  |     { | ||||||
|  |         $rules = [ | ||||||
|  |             'description' => ['nullable', 'string', 'max:300'], | ||||||
|  |             'overview'    => ['nullable', 'string'], | ||||||
|  |             'is_public'   => ['required', 'boolean'], | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         if ($this->getMethod() === 'POST') { | ||||||
|  |             $rules['name'] = ['required', 'string', new Repository()]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $rules; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public function getDto(): StoreUpdate | ||||||
|  |     { | ||||||
|  |         return new StoreUpdate( | ||||||
|  |             isPublic: (bool) $this->input('is_public', false), | ||||||
|  |             description: $this->input('description'), | ||||||
|  |             overview: $this->input('overview'), | ||||||
|  |             name: $this->input('name'), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,27 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Site\TagRepositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Site\TagRepository\Tags; | ||||||
|  | use Illuminate\Foundation\Http\FormRequest; | ||||||
|  | 
 | ||||||
|  | final class TagsRequest extends FormRequest implements FormRequestDto | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules that apply to the request. | ||||||
|  |      */ | ||||||
|  |     public function rules(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'page' => ['nullable', 'numeric', 'min:1'] | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDto(): Tags | ||||||
|  |     { | ||||||
|  |         return new Tags( | ||||||
|  |             page: (int) $this->input('page', 1) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								app/application/app/Http/Requests/Site/User/UserRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/application/app/Http/Requests/Site/User/UserRequest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Requests\Site\User; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\FormRequestDto; | ||||||
|  | use App\Dto\Service\Site\User\User; | ||||||
|  | use Illuminate\Foundation\Http\FormRequest; | ||||||
|  | 
 | ||||||
|  | class UserRequest extends FormRequest implements FormRequestDto | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules that apply to the request. | ||||||
|  |      */ | ||||||
|  |     public function rules(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'page' => ['nullable', 'numeric', 'min:1'] | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getDto(): User | ||||||
|  |     { | ||||||
|  |         return new User( | ||||||
|  |             page: (int) $this->input('page', 1) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Http\Resources\Site\AccessTokens; | ||||||
|  | 
 | ||||||
|  | use App\ServiceResults\Site\AccessTokenService\StoreTokenResult; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Http\Resources\Json\JsonResource; | ||||||
|  | 
 | ||||||
|  | final class Store extends JsonResource | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @var StoreTokenResult | ||||||
|  |      */ | ||||||
|  |     public $resource; | ||||||
|  | 
 | ||||||
|  |     public function toArray(Request $request): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'name'       => $this->resource->getName(), | ||||||
|  |             'permission' => $this->resource->getPermission()->getTitle(), | ||||||
|  |             'token'      => $this->resource->getToken(), | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								app/application/app/Jobs/Registry/V2/ProcessEventPull.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/application/app/Jobs/Registry/V2/ProcessEventPull.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Jobs\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\ImageDataPull; | ||||||
|  | use App\Exceptions\Jobs\Registry\V2\JobException; | ||||||
|  | use App\Services\Registry\V2\EventService; | ||||||
|  | use Illuminate\Bus\Queueable; | ||||||
|  | use Illuminate\Contracts\Queue\ShouldBeEncrypted; | ||||||
|  | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
|  | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
|  | use Illuminate\Queue\InteractsWithQueue; | ||||||
|  | use Illuminate\Queue\SerializesModels; | ||||||
|  | 
 | ||||||
|  | final class ProcessEventPull implements ShouldQueue, ShouldBeEncrypted | ||||||
|  | { | ||||||
|  |     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new job instance. | ||||||
|  |      */ | ||||||
|  |     public function __construct( | ||||||
|  |         public readonly array $target | ||||||
|  |     ) { | ||||||
|  |         //
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Execute the job. | ||||||
|  |      */ | ||||||
|  |     public function handle(EventService $eventService): void | ||||||
|  |     { | ||||||
|  |         if (!isset($this->target['tag'])) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         [$username, $repositoryName] = \explode('/', $this->target['repository'], 2); | ||||||
|  | 
 | ||||||
|  |         $data = new ImageDataPull( | ||||||
|  |             username:       $username, | ||||||
|  |             repositoryName: $repositoryName, | ||||||
|  |             tag:            $this->target['tag'], | ||||||
|  |             digest:         $this->target['digest'], | ||||||
|  |         ); | ||||||
|  |         $result = $eventService->pull($data); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             throw new JobException($result->getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculate the number of seconds to wait before retrying the job. | ||||||
|  |      * | ||||||
|  |      * @return array<int, int> | ||||||
|  |      */ | ||||||
|  |     public function backoff(): array | ||||||
|  |     { | ||||||
|  |         return [10, 60, 600, 1800, 3600]; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										70
									
								
								app/application/app/Jobs/Registry/V2/ProcessEventPush.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/application/app/Jobs/Registry/V2/ProcessEventPush.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Jobs\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\ImageDataPush; | ||||||
|  | use App\Exceptions\Jobs\Registry\V2\JobException; | ||||||
|  | use App\Services\Registry\V2\EventService; | ||||||
|  | use Illuminate\Bus\Queueable; | ||||||
|  | use Illuminate\Contracts\Queue\ShouldBeEncrypted; | ||||||
|  | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
|  | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
|  | use Illuminate\Queue\InteractsWithQueue; | ||||||
|  | use Illuminate\Queue\SerializesModels; | ||||||
|  | 
 | ||||||
|  | final class ProcessEventPush implements ShouldQueue, ShouldBeEncrypted | ||||||
|  | { | ||||||
|  |     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The number of times the job may be attempted. | ||||||
|  |      * | ||||||
|  |      * @var int | ||||||
|  |      */ | ||||||
|  |     public $tries = 1000; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new job instance. | ||||||
|  |      */ | ||||||
|  |     public function __construct( | ||||||
|  |         public readonly array $target | ||||||
|  |     ) { | ||||||
|  |         //
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculate the number of seconds to wait before retrying the job. | ||||||
|  |      * | ||||||
|  |      * @return array<int, int> | ||||||
|  |      */ | ||||||
|  |     public function backoff(): array | ||||||
|  |     { | ||||||
|  |         return [10, 60, 600, 1800, 3600]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Execute the job. | ||||||
|  |      */ | ||||||
|  |     public function handle(EventService $eventService): void | ||||||
|  |     { | ||||||
|  |         [$username, $repositoryName] = \explode('/', $this->target['repository'], 2); | ||||||
|  | 
 | ||||||
|  |         $size = (int) $this->target['size'] ?? 0; | ||||||
|  |         $references = $this->target['references'] ?? []; | ||||||
|  |         foreach ($references as $reference) { | ||||||
|  |             $size += (int) $reference['size'] ?? 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $data = new ImageDataPush( | ||||||
|  |             username:       $username, | ||||||
|  |             repositoryName: $repositoryName, | ||||||
|  |             tag:            $this->target['tag'], | ||||||
|  |             digest:         $this->target['digest'], | ||||||
|  |             size:           $size, | ||||||
|  |         ); | ||||||
|  |         $result = $eventService->push($data); | ||||||
|  |         if ($result->isError()) { | ||||||
|  |             throw new JobException($result->getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								app/application/app/Models/Architecture.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/application/app/Models/Architecture.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Models; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||||
|  | 
 | ||||||
|  | final class Architecture extends Model | ||||||
|  | { | ||||||
|  |     use HasFactory; | ||||||
|  | 
 | ||||||
|  |     protected $table = 'architectures'; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     public function os(): BelongsTo | ||||||
|  |     { | ||||||
|  |         return $this->belongsTo(OperatingSystem::class, 'operating_system_id'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								app/application/app/Models/OperatingSystem.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/application/app/Models/OperatingSystem.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Models; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\HasMany; | ||||||
|  | 
 | ||||||
|  | final class OperatingSystem extends Model | ||||||
|  | { | ||||||
|  |     use HasFactory; | ||||||
|  | 
 | ||||||
|  |     protected $table = 'operating_systems'; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     public function architectures(): HasMany | ||||||
|  |     { | ||||||
|  |         return $this->hasMany(Architecture::class, 'operating_system_id'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										71
									
								
								app/application/app/Models/Repository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/application/app/Models/Repository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Models; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Database\Eloquent\Casts\Attribute; | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\HasMany; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\HasOne; | ||||||
|  | use Illuminate\Database\Eloquent\SoftDeletes; | ||||||
|  | 
 | ||||||
|  | final class Repository extends Model | ||||||
|  | { | ||||||
|  |     use HasFactory, SoftDeletes; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The model's default values for attributes. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $attributes = [ | ||||||
|  |         'is_public' => true, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |         'is_public', | ||||||
|  |         'description', | ||||||
|  |         'overview', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the attributes that should be cast. | ||||||
|  |      * | ||||||
|  |      * @return array<string, string> | ||||||
|  |      */ | ||||||
|  |     protected function casts(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'is_public' => 'boolean', | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected function repository(): Attribute | ||||||
|  |     { | ||||||
|  |         return Attribute::make( | ||||||
|  |             get: fn () => $this->user->username . '/' . $this->name, | ||||||
|  |         )->shouldCache(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function user(): BelongsTo | ||||||
|  |     { | ||||||
|  |         return $this->belongsTo(User::class); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function tags(): HasMany | ||||||
|  |     { | ||||||
|  |         return $this->hasMany(TagRepository::class, 'repository_id'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function tag(): HasOne | ||||||
|  |     { | ||||||
|  |         return $this->hasOne(TagRepository::class, 'repository_id')->orderByDesc('updated_at'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								app/application/app/Models/TagRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/application/app/Models/TagRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Models; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Database\Eloquent\Casts\Attribute; | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||||
|  | use Illuminate\Database\Eloquent\SoftDeletes; | ||||||
|  | 
 | ||||||
|  | final class TagRepository extends Model | ||||||
|  | { | ||||||
|  |     use HasFactory, SoftDeletes; | ||||||
|  | 
 | ||||||
|  |     protected $table = 'tags_repository'; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * All of the relationships to be touched. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $touches = ['repository']; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The model's default values for attributes. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $attributes = [ | ||||||
|  |         'quantity_pulls' => 0, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |         'digest', | ||||||
|  |         'size', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     public function repository(): BelongsTo | ||||||
|  |     { | ||||||
|  |         return $this->belongsTo(Repository::class); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function architecture(): BelongsTo | ||||||
|  |     { | ||||||
|  |         return $this->belongsTo(Architecture::class)->with('os'); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -8,13 +8,15 @@ use App\Enums\SystemRole; | |||||||
| use Illuminate\Database\Eloquent\Casts\Attribute; | use Illuminate\Database\Eloquent\Casts\Attribute; | ||||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
| use Illuminate\Database\Eloquent\Relations\BelongsToMany; | use Illuminate\Database\Eloquent\Relations\BelongsToMany; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\HasMany; | ||||||
| use Illuminate\Database\Eloquent\SoftDeletes; | use Illuminate\Database\Eloquent\SoftDeletes; | ||||||
| use Illuminate\Foundation\Auth\User as Authenticatable; | use Illuminate\Foundation\Auth\User as Authenticatable; | ||||||
| use Illuminate\Notifications\Notifiable; | use Illuminate\Notifications\Notifiable; | ||||||
|  | use Laravel\Sanctum\HasApiTokens; | ||||||
| 
 | 
 | ||||||
| final class User extends Authenticatable | final class User extends Authenticatable | ||||||
| { | { | ||||||
|     use HasFactory, Notifiable, SoftDeletes; |     use HasApiTokens, HasFactory, Notifiable, SoftDeletes; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The model's default values for attributes. |      * The model's default values for attributes. | ||||||
| @@ -65,6 +67,11 @@ final class User extends Authenticatable | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function repositories(): HasMany | ||||||
|  |     { | ||||||
|  |         return $this->hasMany(Repository::class, 'user_id', 'id'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Return the user's roles |      * Return the user's roles | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Policies; | namespace App\Policies; | ||||||
| 
 | 
 | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
| use App\Models\User; | use App\Models\User; | ||||||
| use Illuminate\Auth\Access\HandlesAuthorization; | use Illuminate\Auth\Access\HandlesAuthorization; | ||||||
| 
 | 
 | ||||||
| @@ -9,13 +11,23 @@ abstract readonly class Policy | |||||||
| { | { | ||||||
|     use HandlesAuthorization; |     use HandlesAuthorization; | ||||||
| 
 | 
 | ||||||
|     final public function before(User $user): ?bool |     final public function before(User $user, string $ability = '', string|object $model = ''): ?bool | ||||||
|     { |     { | ||||||
|         if ($user->is_active !== true) { |         if ($user->is_active !== true) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if ($user->is_admin) { |         if ( | ||||||
|  |             is_string($model) && $model === Repository::class | ||||||
|  |             || is_object($model) && $model::class === Repository::class | ||||||
|  | 
 | ||||||
|  |             || is_string($model) && $model === TagRepository::class | ||||||
|  |             || is_object($model) && $model::class === TagRepository::class | ||||||
|  |         ) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->is_admin && $user->currentAccessToken() === null) { | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								app/application/app/Policies/RepositoryPolicy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								app/application/app/Policies/RepositoryPolicy.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Policies; | ||||||
|  | 
 | ||||||
|  | use App\Enums\AccessTokenPermission; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\User; | ||||||
|  | use Laravel\Sanctum\Contracts\HasAbilities; | ||||||
|  | 
 | ||||||
|  | final readonly class RepositoryPolicy extends Policy | ||||||
|  | { | ||||||
|  |     public function create(User $user, User $repositoryUser): bool | ||||||
|  |     { | ||||||
|  |         return $user->id === $repositoryUser->id; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function view(User $user, Repository $repository): bool | ||||||
|  |     { | ||||||
|  |         if ($repository->is_public) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $user->id === $repository->user_id; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function update(User $user, Repository $repository): bool | ||||||
|  |     { | ||||||
|  |         return $user->id === $repository->user_id; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function delete(User $user, Repository $repository): bool | ||||||
|  |     { | ||||||
|  |         return $user->id === $repository->user_id; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function pull(User $user, Repository $repository): bool | ||||||
|  |     { | ||||||
|  |         if ($repository->is_public) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($repository->user_id !== $user->id) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->currentAccessToken()) { | ||||||
|  |             return $this->tokenCan( | ||||||
|  |                 [ | ||||||
|  |                     AccessTokenPermission::RepoReadWriteDelete->value, | ||||||
|  |                     AccessTokenPermission::RepoReadWrite->value, | ||||||
|  |                     AccessTokenPermission::RepoRead->value, | ||||||
|  |                 ], | ||||||
|  |                 $user->currentAccessToken() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function push(User $user, Repository $repository): bool | ||||||
|  |     { | ||||||
|  |         if ($repository->user_id !== $user->id) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->currentAccessToken()) { | ||||||
|  |             return $this->tokenCan( | ||||||
|  |                 [ | ||||||
|  |                     AccessTokenPermission::RepoReadWriteDelete->value, | ||||||
|  |                     AccessTokenPermission::RepoReadWrite->value, | ||||||
|  |                 ], | ||||||
|  |                 $user->currentAccessToken() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function tokenCan(array $permissions, HasAbilities $abilities): bool | ||||||
|  |     { | ||||||
|  |         foreach ($permissions as $permission) { | ||||||
|  |             if ($abilities->can($permission)) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								app/application/app/Policies/TagRepositoryPolicy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/application/app/Policies/TagRepositoryPolicy.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Policies; | ||||||
|  | 
 | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
|  | use App\Models\User; | ||||||
|  | 
 | ||||||
|  | final readonly class TagRepositoryPolicy extends Policy | ||||||
|  | { | ||||||
|  |     public function delete(User $user, TagRepository $tag): bool | ||||||
|  |     { | ||||||
|  |         return $user->id === $tag->repository?->user_id; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,17 +2,22 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\Providers; | namespace App\Providers; | ||||||
| 
 | 
 | ||||||
|  | use App\Enums\Morph; | ||||||
| use App\Services\Search\CreateSearchInstanceCommand; | use App\Services\Search\CreateSearchInstanceCommand; | ||||||
| use App\Services\Search\Search; | use App\Services\Search\Search; | ||||||
| use Illuminate\Cache\RateLimiting\Limit; | use Illuminate\Cache\RateLimiting\Limit; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\Relation; | ||||||
| use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||||
| use Illuminate\Support\Facades\Gate; | use Illuminate\Support\Facades\Gate; | ||||||
| use Illuminate\Support\Facades\RateLimiter; | use Illuminate\Support\Facades\RateLimiter; | ||||||
|  | use Illuminate\Support\Facades\Route; | ||||||
| use Illuminate\Support\Facades\URL; | use Illuminate\Support\Facades\URL; | ||||||
| use Illuminate\Support\ServiceProvider; | use Illuminate\Support\ServiceProvider; | ||||||
| use Illuminate\Validation\Rules\Password; | use Illuminate\Validation\Rules\Password; | ||||||
|  | use Laravel\Sanctum\PersonalAccessToken; | ||||||
|  | use Laravel\Sanctum\Sanctum; | ||||||
| 
 | 
 | ||||||
| class AppServiceProvider extends ServiceProvider | final class AppServiceProvider extends ServiceProvider | ||||||
| { | { | ||||||
|     /** |     /** | ||||||
|      * Register any application services. |      * Register any application services. | ||||||
| @@ -35,8 +40,25 @@ class AppServiceProvider extends ServiceProvider | |||||||
| 
 | 
 | ||||||
|         $this->passwordDefaults(); |         $this->passwordDefaults(); | ||||||
| 
 | 
 | ||||||
|  |         Relation::enforceMorphMap(Morph::map()); | ||||||
|  | 
 | ||||||
|  |         Route::pattern('username', '[a-z0-9]+(?:[._-][a-z0-9]+)*'); | ||||||
|  |         Route::pattern('repository_name', '[a-z0-9]+(?:[._-][a-z0-9]+)*'); | ||||||
|  |         Route::pattern('tag_name', '[a-zA-Z0-9]+(?:[._-][a-zA-Z0-9]+)*'); | ||||||
|  |         Route::pattern('tag_id', '[0-9]+'); | ||||||
|  | 
 | ||||||
|         $this->configureRateLimiting(); |         $this->configureRateLimiting(); | ||||||
|         Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']); |         Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']); | ||||||
|  | 
 | ||||||
|  |         Sanctum::authenticateAccessTokensUsing( | ||||||
|  |             static function (PersonalAccessToken $accessToken, bool $is_valid) { | ||||||
|  |                 if ($accessToken->tokenable?->is_active === true) { | ||||||
|  |                     return $is_valid; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								app/application/app/Providers/BladeProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/application/app/Providers/BladeProvider.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Providers; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Support\Facades\Blade; | ||||||
|  | use Illuminate\Support\ServiceProvider; | ||||||
|  | 
 | ||||||
|  | final class BladeProvider extends ServiceProvider | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Register any application services. | ||||||
|  |      */ | ||||||
|  |     public function register(): void | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Bootstrap any application services. | ||||||
|  |      */ | ||||||
|  |     public function boot(): void | ||||||
|  |     { | ||||||
|  |         Blade::directive('abbreviate_int', function ($expression) { | ||||||
|  |             return sprintf('<?php echo \App\Helpers\Number::abbreviate(%s) ; ?>', $expression); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         Blade::directive('short_digest', function ($expression) { | ||||||
|  |             return sprintf('<?php echo \App\Helpers\Helpers::shortDigest(%s) ; ?>', $expression); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         Blade::directive('size_for_humans', function ($expression) { | ||||||
|  |             return sprintf('<?php echo \App\Helpers\Number::sizeForHumans(%s) ; ?>', $expression); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								app/application/app/Providers/RegistryServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/application/app/Providers/RegistryServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Providers; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Services\Registry\Base32; | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationConfig; | ||||||
|  | use App\Services\Registry\V2\Api\RequestCommandHandler; | ||||||
|  | use App\Services\Registry\V2\Base32CommandHandler; | ||||||
|  | use Illuminate\Contracts\Foundation\Application; | ||||||
|  | use Illuminate\Support\ServiceProvider; | ||||||
|  | 
 | ||||||
|  | final class RegistryServiceProvider extends ServiceProvider | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Register any application services. | ||||||
|  |      */ | ||||||
|  |     public function register(): void | ||||||
|  |     { | ||||||
|  |         $this->app->bind(Base32::class, Base32CommandHandler::class); | ||||||
|  | 
 | ||||||
|  |         $this->app->bind(AuthorizationConfig::class, function () { | ||||||
|  |             return new AuthorizationConfig( | ||||||
|  |                 serviceName: \config('registry.service_name' , ''), | ||||||
|  |                 privateKeyName: \config('registry.private_key_name' , ''), | ||||||
|  |                 issuer: \config('registry.issuer' , ''), | ||||||
|  |                 algorithm: \config('registry.algorithm' , 'RS256'), | ||||||
|  |                 expiresInSeconds: (int) \config('registry.expires_in_seconds' , 600), | ||||||
|  |             ); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->app->bind(RequestCommandHandler::class, function (Application $app) { | ||||||
|  |             return new RequestCommandHandler( | ||||||
|  |                 baseUrl: \config('registry.service_http') . '/v2' | ||||||
|  |             ); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Bootstrap any application services. | ||||||
|  |      */ | ||||||
|  |     public function boot(): void | ||||||
|  |     { | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								app/application/app/Repositories/AccessTokenRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/application/app/Repositories/AccessTokenRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Repositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Search; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Services\Search\CreateSearchInstanceCommand; | ||||||
|  | use Laravel\Sanctum\PersonalAccessToken; | ||||||
|  | 
 | ||||||
|  | final readonly class AccessTokenRepository | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private CreateSearchInstanceCommand $createSearchInstanceCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getUserTokens(User $user, array $with = []): Search | ||||||
|  |     { | ||||||
|  |         $query = $user->tokens()->with($with); | ||||||
|  | 
 | ||||||
|  |         return $this->createSearchInstanceCommand->execute($query); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getTokenById(User $user, int $id): ?PersonalAccessToken | ||||||
|  |     { | ||||||
|  |         return $user->tokens()->where('id', $id)->first(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								app/application/app/Repositories/RepositoryRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								app/application/app/Repositories/RepositoryRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Repositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Search; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Dto\Builder\Repository as RepositoryBuilderDto; | ||||||
|  | use App\Services\Repository\BuilderCommand; | ||||||
|  | use App\Services\Search\CreateSearchInstanceCommand; | ||||||
|  | use Illuminate\Database\Eloquent\Builder; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | 
 | ||||||
|  | final readonly class RepositoryRepository | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private CreateSearchInstanceCommand $createSearchInstanceCommand, | ||||||
|  |         private BuilderCommand $builderCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getRepositoryByName(User $user, string $name, bool $withTrashed = false): ?Repository | ||||||
|  |     { | ||||||
|  |         $query = $user->repositories(); | ||||||
|  |         if ($withTrashed) { | ||||||
|  |             $query->withTrashed(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $query->where('name', $name)->first(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getRepositories(RepositoryBuilderDto $builderDto, array $with = []): Search | ||||||
|  |     { | ||||||
|  |         $query = $this->builderCommand->execute( | ||||||
|  |             query: Repository::query()->with($with)->orderByDesc('updated_at'), | ||||||
|  |             builderDto: $builderDto | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return $this->createSearchInstanceCommand->execute($query); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getUserRepositories(User $user, RepositoryBuilderDto $builderDto, array $with = []): Search | ||||||
|  |     { | ||||||
|  |         $query = $this->builderCommand->execute( | ||||||
|  |             query: $user->repositories()->with($with)->orderByDesc('updated_at'), | ||||||
|  |             builderDto: $builderDto | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return $this->createSearchInstanceCommand->execute($query); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function isExistsName(User $user, string $name, ?int $exceptId = null): bool | ||||||
|  |     { | ||||||
|  |         return $user->repositories() | ||||||
|  |             ->where('name', Str::lower($name)) | ||||||
|  |             ->when($exceptId, function (Builder $query, int $exceptId) { | ||||||
|  |                 $query->where('id', '!=', $exceptId); | ||||||
|  |             }) | ||||||
|  |             ->withTrashed() | ||||||
|  |             ->exists(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								app/application/app/Repositories/TagRepositoryRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/application/app/Repositories/TagRepositoryRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Repositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Search; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
|  | use App\Services\Search\CreateSearchInstanceCommand; | ||||||
|  | 
 | ||||||
|  | final readonly class TagRepositoryRepository | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private CreateSearchInstanceCommand $createSearchInstanceCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getTagById(int $id): ?TagRepository | ||||||
|  |     { | ||||||
|  |         return TagRepository::query()->where("id", $id)->first(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getTagsRepository(Repository $repository, array $with = []): Search | ||||||
|  |     { | ||||||
|  |         $query = $repository->tags()->with($with)->orderByDesc('updated_at'); | ||||||
|  | 
 | ||||||
|  |         return $this->createSearchInstanceCommand->execute($query); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -27,6 +27,16 @@ final readonly class UserRepository | |||||||
|         return User::query()->where('email', Str::lower($email))->first(); |         return User::query()->where('email', Str::lower($email))->first(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function getUserByUsername(string $username, bool $withTrashed = false): ?User | ||||||
|  |     { | ||||||
|  |         $query = User::query(); | ||||||
|  |         if ($withTrashed) { | ||||||
|  |             $query->withTrashed(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $query->where('username', $username)->first(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function getUsers(UserBuilderDto $userBuilderDto, array $with = []): Search |     public function getUsers(UserBuilderDto $userBuilderDto, array $with = []): Search | ||||||
|     { |     { | ||||||
|         $query = $this->builderCommand->execute( |         $query = $this->builderCommand->execute( | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ final readonly class Repository implements ValidationRule | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         $validator = Validator::make([ |         $validator = Validator::make([ | ||||||
|             'slug' => $value, |             'name' => $value, | ||||||
|         ], [ |         ], [ | ||||||
|             'slug' => 'min:1|max:150|regex:/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/', |             'name' => 'min:1|max:150|regex:/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/', | ||||||
|         ]); |         ]); | ||||||
|         if ($validator->fails()) { |         if ($validator->fails()) { | ||||||
|             foreach ($validator->errors()->all() as $error) { |             foreach ($validator->errors()->all() as $error) { | ||||||
|   | |||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\ServiceResults\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Models\Architecture; | ||||||
|  | use App\ServiceResults\ServiceResult; | ||||||
|  | 
 | ||||||
|  | final class ArchitectureResult extends ServiceResult | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly Architecture $architecture, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getArchitecture(): Architecture | ||||||
|  |     { | ||||||
|  |         return $this->architecture; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\ServiceResults\Site\AccessTokenService; | ||||||
|  | 
 | ||||||
|  | use App\Enums\AccessTokenPermission; | ||||||
|  | use App\ServiceResults\ServiceResult; | ||||||
|  | 
 | ||||||
|  | final class StoreTokenResult extends ServiceResult | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly string                $name, | ||||||
|  |         private readonly AccessTokenPermission $permission, | ||||||
|  |         private readonly string                $token, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getName(): string | ||||||
|  |     { | ||||||
|  |         return $this->name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getPermission(): AccessTokenPermission | ||||||
|  |     { | ||||||
|  |         return $this->permission; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getToken(): string | ||||||
|  |     { | ||||||
|  |         return $this->token; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\ServiceResults\TagRepository; | ||||||
|  | 
 | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
|  | use App\ServiceResults\ServiceResult; | ||||||
|  | 
 | ||||||
|  | final class IncrementPullResult extends ServiceResult | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly Repository    $repository, | ||||||
|  |         private readonly TagRepository $tagRepository, | ||||||
|  |     ) { } | ||||||
|  | } | ||||||
| @@ -62,7 +62,7 @@ final class UserService extends Service | |||||||
|     { |     { | ||||||
|         $modelUser = $this->userRepository->getUserById($id); |         $modelUser = $this->userRepository->getUserById($id); | ||||||
| 
 | 
 | ||||||
|         if (is_null($modelUser)) { |         if (\is_null($modelUser)) { | ||||||
|             return $this->errNotFound(__('Not Found')); |             return $this->errNotFound(__('Not Found')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -101,14 +101,14 @@ final class UserService extends Service | |||||||
|         try { |         try { | ||||||
|             $modelUser = DB::transaction(function () use ($data) { |             $modelUser = DB::transaction(function () use ($data) { | ||||||
|                 $dataUser = $this->getDataUser($data); |                 $dataUser = $this->getDataUser($data); | ||||||
| 
 |                 $dataUser['username'] = $data->getUsername(); | ||||||
|                 $modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword()); |                 $modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword()); | ||||||
|                 $this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles()); |                 $this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles()); | ||||||
| 
 | 
 | ||||||
|                 return $modelUser; |                 return $modelUser; | ||||||
|             }); |             }); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             report($e); |             \report($e); | ||||||
|             return $this->errService(__('Server Error')); |             return $this->errService(__('Server Error')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -119,7 +119,7 @@ final class UserService extends Service | |||||||
|     { |     { | ||||||
|         $modelUser = $this->userRepository->getUserById($id); |         $modelUser = $this->userRepository->getUserById($id); | ||||||
| 
 | 
 | ||||||
|         if (is_null($modelUser)) { |         if (\is_null($modelUser)) { | ||||||
|             return $this->errNotFound(__('Not Found')); |             return $this->errNotFound(__('Not Found')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -134,13 +134,6 @@ final class UserService extends Service | |||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if ($this->userRepository->isExistsUsername($data->getUsername(), $modelUser->id)) { |  | ||||||
|             return $this->errValidate( |  | ||||||
|                 __('validation.unique', ['attribute' => __('validation.attributes.username')]), |  | ||||||
|                 ['code' => __('validation.unique', ['attribute' => __('validation.attributes.username')])] |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         try { |         try { | ||||||
|             $modelUser = DB::transaction(function () use ($data, $modelUser) { |             $modelUser = DB::transaction(function () use ($data, $modelUser) { | ||||||
|                 $dataUser = $this->getDataUser($data); |                 $dataUser = $this->getDataUser($data); | ||||||
| @@ -151,7 +144,7 @@ final class UserService extends Service | |||||||
|                 return $modelUser; |                 return $modelUser; | ||||||
|             }); |             }); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             report($e); |             \report($e); | ||||||
|             return $this->errService(__('Server Error')); |             return $this->errService(__('Server Error')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -162,7 +155,7 @@ final class UserService extends Service | |||||||
|     { |     { | ||||||
|         $modelUser = $this->userRepository->getUserById($id); |         $modelUser = $this->userRepository->getUserById($id); | ||||||
| 
 | 
 | ||||||
|         if (is_null($modelUser)) { |         if (\is_null($modelUser)) { | ||||||
|             return $this->errNotFound(__('Not Found')); |             return $this->errNotFound(__('Not Found')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -173,7 +166,7 @@ final class UserService extends Service | |||||||
|         try { |         try { | ||||||
|             $this->userCommandHandler->handleUpdatePassword($modelUser, $data->getPassword()); |             $this->userCommandHandler->handleUpdatePassword($modelUser, $data->getPassword()); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             report($e->getMessage()); |             \report($e->getMessage()); | ||||||
|             return $this->errService($e->getMessage()); |             return $this->errService($e->getMessage()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -184,7 +177,7 @@ final class UserService extends Service | |||||||
|     { |     { | ||||||
|         $modelUser = $this->userRepository->getUserById($id); |         $modelUser = $this->userRepository->getUserById($id); | ||||||
| 
 | 
 | ||||||
|         if (is_null($modelUser)) { |         if (\is_null($modelUser)) { | ||||||
|             return $this->errNotFound(__('Not Found')); |             return $this->errNotFound(__('Not Found')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -197,7 +190,7 @@ final class UserService extends Service | |||||||
|                 $this->userCommandHandler->handleDestroy($modelUser); |                 $this->userCommandHandler->handleDestroy($modelUser); | ||||||
|             }); |             }); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
|             report($e); |             \report($e); | ||||||
|             return $this->errService(__('Server Error')); |             return $this->errService(__('Server Error')); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -209,7 +202,6 @@ final class UserService extends Service | |||||||
|         return [ |         return [ | ||||||
|             'name'      => $data->getName(), |             'name'      => $data->getName(), | ||||||
|             'email'     => $data->getEmail(), |             'email'     => $data->getEmail(), | ||||||
|             'username'  => $data->getUsername(), |  | ||||||
|             'is_active' => $data->isActive(), |             'is_active' => $data->isActive(), | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								app/application/app/Services/Registry/V2/AccessCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/application/app/Services/Registry/V2/AccessCommand.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationService\Scope; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Repositories\RepositoryRepository; | ||||||
|  | use App\Repositories\UserRepository; | ||||||
|  | 
 | ||||||
|  | final readonly class AccessCommand | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private RepositoryRepository $repositoryRepository, | ||||||
|  |         private UserRepository       $userRepository, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(?Scope $scope, ?User $user): array | ||||||
|  |     { | ||||||
|  |         if (\is_null($scope)) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $userScope = $this->userRepository->getUserByUsername($scope->getUsername()); | ||||||
|  |         if (!$userScope) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($userScope, $scope->getRepositoryName()); | ||||||
|  |         if (!$repository) { | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             [ | ||||||
|  |                 'type'    => 'repository', | ||||||
|  |                 'name'    => $scope->getUsername() . '/' . $scope->getRepositoryName(), | ||||||
|  |                 'actions' => $this->actions($userScope, $repository, $scope, $user), | ||||||
|  |             ] | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function actions(User $userScope, Repository $repository, Scope $scope, ?User $user): array | ||||||
|  |     { | ||||||
|  |         $actions = []; | ||||||
|  |         if ($repository->is_public) { | ||||||
|  |             $actions[] = 'pull'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (\is_null($user)) { | ||||||
|  |             return $actions; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($userScope->id !== $user->id) { | ||||||
|  |             return $actions; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         foreach ($scope->getActions() as $action) { | ||||||
|  |             if ($action === 'pull' && $repository->is_public) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ($user->can($action, $repository)) { | ||||||
|  |                 $actions[] = $action; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $actions; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								app/application/app/Services/Registry/V2/Api/ApiService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								app/application/app/Services/Registry/V2/Api/ApiService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2\Api; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\ServiceResultError; | ||||||
|  | use App\ServiceResults\ServiceResultArray; | ||||||
|  | use App\Services\Service; | ||||||
|  | use Illuminate\Support\Facades\Log; | ||||||
|  | 
 | ||||||
|  | final class ApiService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly RequestCommandHandler   $requestCommandHandler, | ||||||
|  |         private readonly GenerateJwtTokenCommand $generateJwtTokenCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getManifest(string $repositoryRegistry, string $reference): ServiceResultError | ServiceResultArray | ||||||
|  |     { | ||||||
|  |         $access = [ | ||||||
|  |             $this->getAccessRepository($repositoryRegistry, ['pull']), | ||||||
|  |         ]; | ||||||
|  |         $username = ''; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $token = $this->generateJwtTokenCommand->execute($access, $username); | ||||||
|  | 
 | ||||||
|  |             $path = $repositoryRegistry . '/manifests/' . $reference; | ||||||
|  |             $result = $this->requestCommandHandler->handleGet($path, $token); | ||||||
|  |             Log::info($result); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService($e->getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result($result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getBlob(string $repositoryRegistry, string $digest): ServiceResultError | ServiceResultArray | ||||||
|  |     { | ||||||
|  |         $access = [ | ||||||
|  |             $this->getAccessRepository($repositoryRegistry, ['pull']), | ||||||
|  |         ]; | ||||||
|  |         $username = ''; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $token = $this->generateJwtTokenCommand->execute($access, $username); | ||||||
|  | 
 | ||||||
|  |             $path = $repositoryRegistry . '/blobs/' . $digest; | ||||||
|  |             $result = $this->requestCommandHandler->handleGet($path, $token); | ||||||
|  |             Log::info($result); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService($e->getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result($result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getAccessRepository(string $repository, array $actions): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'type'    => 'repository', | ||||||
|  |             'name'    => $repository, | ||||||
|  |             'actions' => $actions, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2\Api; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationConfig; | ||||||
|  | use App\Services\Registry\V2\JwtCommand; | ||||||
|  | use App\Services\Registry\V2\PayloadCommand; | ||||||
|  | 
 | ||||||
|  | final readonly class GenerateJwtTokenCommand | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private AuthorizationConfig  $authorizationConfig, | ||||||
|  |         private JwtCommand           $jwtCommand, | ||||||
|  |         private PayloadCommand       $payloadCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute($access, $username): string | ||||||
|  |     { | ||||||
|  |         $payload = $this->payloadCommand->execute($this->authorizationConfig, $access, $username); | ||||||
|  |         return $this->jwtCommand->execute($this->authorizationConfig, $payload); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,31 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2\Api; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Http\Client\PendingRequest; | ||||||
|  | use Illuminate\Support\Facades\Http; | ||||||
|  | 
 | ||||||
|  | final readonly class RequestCommandHandler | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private string $baseUrl, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function handleGet(string $path, string $token): array | ||||||
|  |     { | ||||||
|  |         $response = $this->beforeRequest($token)->get($path); | ||||||
|  | 
 | ||||||
|  |         return $response->json(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function beforeRequest(string $token): PendingRequest | ||||||
|  |     { | ||||||
|  |         return Http::withHeaders([ | ||||||
|  |                 'Accept'        => 'application/vnd.docker.distribution.manifest.v2+json', | ||||||
|  |                 'Authorization' => 'Bearer ' . $token | ||||||
|  |             ]) | ||||||
|  |             ->timeout(15) | ||||||
|  |             ->acceptJson() | ||||||
|  |             ->baseUrl($this->baseUrl); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,62 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Exceptions\Services\Registry\V2\ArchitectureCommand\BlobException; | ||||||
|  | use App\Exceptions\Services\Registry\V2\ArchitectureCommand\ManifestException; | ||||||
|  | use App\Models\Architecture; | ||||||
|  | use App\Models\OperatingSystem; | ||||||
|  | use App\ServiceResults\Registry\V2\ArchitectureResult; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\Services\Registry\V2\Api\ApiService; | ||||||
|  | use App\Services\Service; | ||||||
|  | 
 | ||||||
|  | final class ArchitectureCommand extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly ApiService $apiService, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(string $repositoryRegistry, string $digest): ServiceResultError | ArchitectureResult | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $manifest = $this->apiService->getManifest($repositoryRegistry, $digest); | ||||||
|  |             $manifest = $manifest->getData(); | ||||||
|  |             if (!isset($manifest['config']) || !isset($manifest['config']['digest'])) { | ||||||
|  |                 throw new ManifestException('Got an empty result'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $manifestDigest = $manifest['config']['digest']; | ||||||
|  |             unset($manifest); | ||||||
|  |             $data = $this->apiService->getBlob($repositoryRegistry, $manifestDigest); | ||||||
|  |             $data = $data->getData(); | ||||||
|  | 
 | ||||||
|  |             if (!isset($data['architecture']) || !isset($data['os'])) { | ||||||
|  |                 throw new BlobException('Didn\'t receive information about architecture and OS'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $architecture = $data['architecture']; | ||||||
|  |             $os = $data['os']; | ||||||
|  |             unset($data); | ||||||
|  | 
 | ||||||
|  |             /** | ||||||
|  |              * @var OperatingSystem $osModel | ||||||
|  |              */ | ||||||
|  |             $osModel = OperatingSystem::firstOrCreate([ | ||||||
|  |                 'name' => $os, | ||||||
|  |             ]); | ||||||
|  | 
 | ||||||
|  |             /** | ||||||
|  |              * @var Architecture $architectureModel | ||||||
|  |              */ | ||||||
|  |             $architectureModel = $osModel->architectures()->firstOrCreate([ | ||||||
|  |                 'name' => $architecture, | ||||||
|  |             ]); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService($e->getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new ArchitectureResult($architectureModel); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\ServiceResultError; | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationConfig; | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationService\Authorization; | ||||||
|  | use App\ServiceResults\ServiceResultArray; | ||||||
|  | use App\Services\Service; | ||||||
|  | 
 | ||||||
|  | final class AuthorizationService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly AuthorizationConfig  $authorizationConfig, | ||||||
|  |         private readonly JwtCommand           $jwtCommand, | ||||||
|  |         private readonly PayloadCommand       $payloadCommand, | ||||||
|  |         private readonly AccessCommand        $accessCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function authorization(Authorization $authorization): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         if ($authorization->getService() !== $this->authorizationConfig->getServiceName()) { | ||||||
|  |             return $this->errUnauthorized(__('Service name does not match')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $access   = $this->accessCommand->execute($authorization->getScope(), $authorization->getUser()); | ||||||
|  |             $username = $authorization->getUser()?->username ?? ''; | ||||||
|  |             $payload  = $this->payloadCommand->execute($this->authorizationConfig, $access, $username); | ||||||
|  | 
 | ||||||
|  |             $jwt      = $this->jwtCommand->execute($this->authorizationConfig, $payload); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'token' => $jwt | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,239 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Services\Registry\Base32; | ||||||
|  | use InvalidArgumentException; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Encode in Base32 based on RFC 4648. | ||||||
|  |  * | ||||||
|  |  * https://github.com/selective-php/base32/blob/master/src/Base32.php | ||||||
|  |  */ | ||||||
|  | final class Base32CommandHandler implements Base32 | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @var array<string> | ||||||
|  |      */ | ||||||
|  |     private $map = [ | ||||||
|  |         'A', | ||||||
|  |         'B', | ||||||
|  |         'C', | ||||||
|  |         'D', | ||||||
|  |         'E', | ||||||
|  |         'F', | ||||||
|  |         'G', | ||||||
|  |         'H', //  7
 | ||||||
|  |         'I', | ||||||
|  |         'J', | ||||||
|  |         'K', | ||||||
|  |         'L', | ||||||
|  |         'M', | ||||||
|  |         'N', | ||||||
|  |         'O', | ||||||
|  |         'P', // 15
 | ||||||
|  |         'Q', | ||||||
|  |         'R', | ||||||
|  |         'S', | ||||||
|  |         'T', | ||||||
|  |         'U', | ||||||
|  |         'V', | ||||||
|  |         'W', | ||||||
|  |         'X', // 23
 | ||||||
|  |         'Y', | ||||||
|  |         'Z', | ||||||
|  |         '2', | ||||||
|  |         '3', | ||||||
|  |         '4', | ||||||
|  |         '5', | ||||||
|  |         '6', | ||||||
|  |         '7', // 31
 | ||||||
|  |         '=',  // padding char
 | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @var array<string> | ||||||
|  |      */ | ||||||
|  |     private $flippedMap = [ | ||||||
|  |         'A' => '0', | ||||||
|  |         'B' => '1', | ||||||
|  |         'C' => '2', | ||||||
|  |         'D' => '3', | ||||||
|  |         'E' => '4', | ||||||
|  |         'F' => '5', | ||||||
|  |         'G' => '6', | ||||||
|  |         'H' => '7', | ||||||
|  |         'I' => '8', | ||||||
|  |         'J' => '9', | ||||||
|  |         'K' => '10', | ||||||
|  |         'L' => '11', | ||||||
|  |         'M' => '12', | ||||||
|  |         'N' => '13', | ||||||
|  |         'O' => '14', | ||||||
|  |         'P' => '15', | ||||||
|  |         'Q' => '16', | ||||||
|  |         'R' => '17', | ||||||
|  |         'S' => '18', | ||||||
|  |         'T' => '19', | ||||||
|  |         'U' => '20', | ||||||
|  |         'V' => '21', | ||||||
|  |         'W' => '22', | ||||||
|  |         'X' => '23', | ||||||
|  |         'Y' => '24', | ||||||
|  |         'Z' => '25', | ||||||
|  |         '2' => '26', | ||||||
|  |         '3' => '27', | ||||||
|  |         '4' => '28', | ||||||
|  |         '5' => '29', | ||||||
|  |         '6' => '30', | ||||||
|  |         '7' => '31', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Encodes data with base32. | ||||||
|  |      * | ||||||
|  |      * @param string $input The original data, as a string | ||||||
|  |      * @param bool $padding Use padding false when encoding for urls | ||||||
|  |      * | ||||||
|  |      * @return string The Base32 encoded string | ||||||
|  |      */ | ||||||
|  |     public function encode(string $input, bool $padding = true): string | ||||||
|  |     { | ||||||
|  |         if ($input === '') { | ||||||
|  |             return ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $input = str_split($input); | ||||||
|  |         $binaryString = ''; | ||||||
|  | 
 | ||||||
|  |         $inputCount = count($input); | ||||||
|  |         for ($i = 0; $i < $inputCount; $i++) { | ||||||
|  |             $binaryString .= str_pad(base_convert((string)ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $fiveBitBinaryArray = str_split($binaryString, 5); | ||||||
|  |         $base32 = ''; | ||||||
|  |         $i = 0; | ||||||
|  |         $fiveCount = count($fiveBitBinaryArray); | ||||||
|  | 
 | ||||||
|  |         while ($i < $fiveCount) { | ||||||
|  |             $base32 .= $this->map[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; | ||||||
|  |             $i++; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $x = strlen($binaryString) % 40; | ||||||
|  |         if ($padding && $x !== 0) { | ||||||
|  |             if ($x === 8) { | ||||||
|  |                 return $base32 . str_repeat($this->map[32], 6); | ||||||
|  |             } | ||||||
|  |             if ($x === 16) { | ||||||
|  |                 return $base32 . str_repeat($this->map[32], 4); | ||||||
|  |             } | ||||||
|  |             if ($x === 24) { | ||||||
|  |                 return $base32 . str_repeat($this->map[32], 3); | ||||||
|  |             } | ||||||
|  |             if ($x === 32) { | ||||||
|  |                 return $base32 . $this->map[32]; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $base32; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Decodes data encoded with base32. | ||||||
|  |      * | ||||||
|  |      * @param string $input The encoded data | ||||||
|  |      * | ||||||
|  |      * @throws InvalidArgumentException | ||||||
|  |      * | ||||||
|  |      * @return string The original data or false on failure | ||||||
|  |      */ | ||||||
|  |     public function decode(string $input): string | ||||||
|  |     { | ||||||
|  |         if ($input === '') { | ||||||
|  |             return ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $input = strtoupper($input); | ||||||
|  |         $paddingCharCount = substr_count($input, $this->map[32]); | ||||||
|  |         $allowedValues = [6, 4, 3, 1, 0]; | ||||||
|  | 
 | ||||||
|  |         if (!in_array($paddingCharCount, $allowedValues)) { | ||||||
|  |             throw new InvalidArgumentException('Invalid base32 data'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for ($i = 0; $i < 4; $i++) { | ||||||
|  |             if ( | ||||||
|  |                 $paddingCharCount === $allowedValues[$i] | ||||||
|  |                 && substr($input, -$allowedValues[$i]) !== str_repeat($this->map[32], $allowedValues[$i]) | ||||||
|  |             ) { | ||||||
|  |                 throw new InvalidArgumentException('Invalid base32 data'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $input = str_replace('=', '', $input); | ||||||
|  |         $input = str_split($input); | ||||||
|  |         $binaryString = ''; | ||||||
|  |         $count = count($input); | ||||||
|  | 
 | ||||||
|  |         for ($i = 0; $i < $count; $i += 8) { | ||||||
|  |             $x = ''; | ||||||
|  | 
 | ||||||
|  |             if (!in_array($input[$i], $this->map)) { | ||||||
|  |                 throw new InvalidArgumentException('Invalid base32 data'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $x .= $this->decodeFlippedMap($i, $input); | ||||||
|  | 
 | ||||||
|  |             $eightBits = str_split($x, 8); | ||||||
|  |             $bitCount = count($eightBits); | ||||||
|  | 
 | ||||||
|  |             $binaryString .= $this->decodeEightBits($bitCount, $eightBits); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Converting a binary (\0 terminated) string to a PHP string
 | ||||||
|  |         return rtrim($binaryString, "\0"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Decode data with flipped map. | ||||||
|  |      * | ||||||
|  |      * @param int $i The encoded data index | ||||||
|  |      * @param array<string> $input The encoded data array | ||||||
|  |      * | ||||||
|  |      * @return string parted decoded string | ||||||
|  |      */ | ||||||
|  |     private function decodeFlippedMap(int $i, array $input) | ||||||
|  |     { | ||||||
|  |         $x = ''; | ||||||
|  | 
 | ||||||
|  |         for ($j = 0; $j < 8; $j++) { | ||||||
|  |             if (!isset($input[$i + $j])) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             $x .= str_pad(base_convert($this->flippedMap[$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $x; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Decode data with eight bits. | ||||||
|  |      * | ||||||
|  |      * @param int $bitCount The eight bits count | ||||||
|  |      * @param array<string> $eightBits The eight bits | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     private function decodeEightBits(int $bitCount, array $eightBits) | ||||||
|  |     { | ||||||
|  |         $binaryString = ''; | ||||||
|  | 
 | ||||||
|  |         for ($z = 0; $z < $bitCount; $z++) { | ||||||
|  |             $binaryString .= (($y = chr((int)base_convert($eightBits[$z], 2, 10))) || ord($y) === 48) ? $y : ''; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $binaryString; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Enums\CacheTag; | ||||||
|  | 
 | ||||||
|  | final readonly class ContentPrivateKeyCommand | ||||||
|  | { | ||||||
|  |     public function execute(string $privateKeyName): string | ||||||
|  |     { | ||||||
|  |         $seconds = 3600; | ||||||
|  | 
 | ||||||
|  |         return CacheTag::RegistryV2PrivateKey->getCache() | ||||||
|  |             ->remember(self::class . $privateKeyName, $seconds, function () use ($privateKeyName) { | ||||||
|  |                 return \file_get_contents( \resource_path('certs/' . $privateKeyName) ); | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										120
									
								
								app/application/app/Services/Registry/V2/EventService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								app/application/app/Services/Registry/V2/EventService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\DataProcessing; | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\Event; | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\ImageDataPull; | ||||||
|  | use App\Dto\Service\Registry\V2\EventService\ImageDataPush; | ||||||
|  | use App\Exceptions\Services\Registry\V2\EventService\DataProcessingException; | ||||||
|  | use App\Jobs\Registry\V2\ProcessEventPull; | ||||||
|  | use App\Jobs\Registry\V2\ProcessEventPush; | ||||||
|  | use App\Repositories\RepositoryRepository; | ||||||
|  | use App\Repositories\UserRepository; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\ServiceResults\ServiceResultSuccess; | ||||||
|  | use App\Services\Service; | ||||||
|  | use App\Services\TagRepository\IncrementPullCommand; | ||||||
|  | use App\Services\TagRepository\UpdateOrCreateCommand; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | 
 | ||||||
|  | final class EventService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly UserRepository        $userRepository, | ||||||
|  |         private readonly RepositoryRepository  $repositoryRepository, | ||||||
|  |         private readonly ArchitectureCommand   $architectureCommand, | ||||||
|  |         private readonly UpdateOrCreateCommand $updateOrCreateCommand, | ||||||
|  |         private readonly IncrementPullCommand  $incrementPullCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function dataProcessing(DataProcessing $dataProcessing): ServiceResultError | ServiceResultSuccess | ||||||
|  |     { | ||||||
|  |         $events = $dataProcessing->getEvents()->getEvents(); | ||||||
|  | 
 | ||||||
|  |         foreach ($events as $event) { | ||||||
|  |             /** | ||||||
|  |              * @var Event $event | ||||||
|  |              */ | ||||||
|  | 
 | ||||||
|  |             if ($event->getAction() === 'pull') { | ||||||
|  |                 ProcessEventPull::dispatch($event->getTarget())->onQueue('normal'); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ($event->getAction() === 'push') { | ||||||
|  |                 ProcessEventPush::dispatch($event->getTarget())->onQueue('high'); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $exception = new DataProcessingException('Unknown action: ' . $event->getAction() . ' target: ' . print_r($event->getTarget(), true)); | ||||||
|  |             \report($exception); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok('OK!'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function push(ImageDataPush $data): ServiceResultError | ServiceResultSuccess | ||||||
|  |     { | ||||||
|  |         $user = $this->userRepository->getUserByUsername($data->getUsername(), withTrashed: true); | ||||||
|  |         if (\is_null($user)) { | ||||||
|  |             return $this->errNotFound(__('Not Found') . ' User: ' . $data->getUsername()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($user, $data->getRepositoryName(), withTrashed: true); | ||||||
|  |         if (\is_null($repository)) { | ||||||
|  |             return $this->errNotFound(__('Not Found') . ' Repository: ' . $data->getRepositoryName()); | ||||||
|  |         } | ||||||
|  |         unset($user); | ||||||
|  | 
 | ||||||
|  |         $repositoryRegistry = $data->getUsername() . '/' . $data->getRepositoryName(); | ||||||
|  |         $architecture = $this->architectureCommand->execute($repositoryRegistry, $data->getDigest()); | ||||||
|  |         if ($architecture->isError()) { | ||||||
|  |             return $architecture; | ||||||
|  |         } | ||||||
|  |         $architecture = $architecture->getArchitecture(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             DB::transaction(function () use ($repository, $architecture, $data) { | ||||||
|  |                 $dataTag = [ | ||||||
|  |                     'digest' => $data->getDigest(), | ||||||
|  |                     'size'   => $data->getSize(), | ||||||
|  |                 ]; | ||||||
|  |                 $this->updateOrCreateCommand->execute($repository, $architecture, $data->getTag(), $dataTag); | ||||||
|  |             }); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService($e->getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok('OK!'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function pull(ImageDataPull $data): ServiceResultError | ServiceResultSuccess | ||||||
|  |     { | ||||||
|  |         $user = $this->userRepository->getUserByUsername($data->getUsername(), withTrashed: true); | ||||||
|  |         if (\is_null($user)) { | ||||||
|  |             return $this->errNotFound(__('Not Found') . ' User: ' . $data->getUsername()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($user, $data->getRepositoryName(), withTrashed: true); | ||||||
|  |         if (\is_null($repository)) { | ||||||
|  |             return $this->errNotFound(__('Not Found') . ' Repository: ' . $data->getRepositoryName()); | ||||||
|  |         } | ||||||
|  |         unset($user); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $result = DB::transaction(function () use ($repository, $data) { | ||||||
|  |                 return $this->incrementPullCommand->execute($repository, $data->getTag(), $data->getDigest()); | ||||||
|  |             }); | ||||||
|  |             if ($result->isError()) { | ||||||
|  |                 return $result; | ||||||
|  |             } | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService($e->getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok('OK!'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								app/application/app/Services/Registry/V2/JwtCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/application/app/Services/Registry/V2/JwtCommand.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationConfig; | ||||||
|  | use Firebase\JWT\JWT; | ||||||
|  | 
 | ||||||
|  | final readonly class JwtCommand | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private KidCommand               $kidCommand, | ||||||
|  |         private ContentPrivateKeyCommand $contentPrivateKeyCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(AuthorizationConfig $authorizationConfig, array $payload): string | ||||||
|  |     { | ||||||
|  |         $contentPrivateKey = $this->contentPrivateKeyCommand->execute($authorizationConfig->getPrivateKeyName()); | ||||||
|  | 
 | ||||||
|  |         return JWT::encode( | ||||||
|  |             $payload, | ||||||
|  |             $contentPrivateKey, | ||||||
|  |             $authorizationConfig->getAlgorithm(), | ||||||
|  |             $this->kidCommand->execute($authorizationConfig->getPrivateKeyName(), $contentPrivateKey) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								app/application/app/Services/Registry/V2/KidCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/application/app/Services/Registry/V2/KidCommand.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Services\Registry\Base32; | ||||||
|  | use App\Enums\CacheTag; | ||||||
|  | 
 | ||||||
|  | final readonly class KidCommand | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private Base32 $base32CommandHandler, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(string $privateKeyName, string $privateKeyContent): string | ||||||
|  |     { | ||||||
|  |         $seconds = 3600; | ||||||
|  | 
 | ||||||
|  |         return CacheTag::RegistryV2PrivateKey->getCache() | ||||||
|  |             ->remember(self::class . $privateKeyName, $seconds, function () use ($privateKeyContent) { | ||||||
|  |                 return $this->generate($privateKeyContent); | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function generate(string $privateKeyContent): string | ||||||
|  |     { | ||||||
|  |         // Extract public key from private key
 | ||||||
|  |         $privateKey = openssl_pkey_get_private($privateKeyContent); | ||||||
|  |         $publicKeyContent = openssl_pkey_get_details($privateKey)['key']; | ||||||
|  | 
 | ||||||
|  |         // Clean pem header and footer
 | ||||||
|  |         $pattern = '/-----BEGIN [^-]*-----\r?\n?|-----END [^-]*-----\r?\n?/'; | ||||||
|  |         $cleanedPem = trim(preg_replace($pattern, '', $publicKeyContent)); | ||||||
|  | 
 | ||||||
|  |         // Convert to der
 | ||||||
|  |         $der = base64_decode(preg_replace('/\s+/', '', $cleanedPem)); | ||||||
|  | 
 | ||||||
|  |         // Calculate digest
 | ||||||
|  |         $algorithm = hash_init('sha256'); | ||||||
|  |         hash_update($algorithm, $der); | ||||||
|  |         $digest = hash_final($algorithm, true); | ||||||
|  | 
 | ||||||
|  |         // Shorten digest to 30 bytes
 | ||||||
|  |         $digest = substr($digest, 0, 30); | ||||||
|  | 
 | ||||||
|  |         $source = $this->base32CommandHandler->encode($digest); | ||||||
|  |         $source = str_replace('=', '', $source); | ||||||
|  | 
 | ||||||
|  |         // Format with :
 | ||||||
|  |         $result = []; | ||||||
|  |         for ($i = 0; $i < strlen($source); $i += 4) { | ||||||
|  |             $result[] = substr($source, $i, 4); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return implode(':', $result); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								app/application/app/Services/Registry/V2/PayloadCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/application/app/Services/Registry/V2/PayloadCommand.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Registry\V2; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\Registry\V2\AuthorizationConfig; | ||||||
|  | 
 | ||||||
|  | final readonly class PayloadCommand | ||||||
|  | { | ||||||
|  |     public function execute(AuthorizationConfig $authorizationConfig, array $access, string $username): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'iss'    => $authorizationConfig->getIssuer(), | ||||||
|  |             'aud'    => $authorizationConfig->getServiceName(), | ||||||
|  |             'iat'    => \now()->getTimestamp(), | ||||||
|  |             'nbf'    => \now()->getTimestamp(), | ||||||
|  |             'exp'    => \now()->addSeconds($authorizationConfig->getExpiresInSeconds())->getTimestamp(), | ||||||
|  |             'sub'    => $username, | ||||||
|  |             'jti'    => \strtoupper(\bin2hex(\openssl_random_pseudo_bytes(16))), | ||||||
|  |             'access' => $access, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								app/application/app/Services/Repository/BuilderCommand.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/application/app/Services/Repository/BuilderCommand.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Repository; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Builder\Repository; | ||||||
|  | use Illuminate\Database\Eloquent\Builder; | ||||||
|  | use Illuminate\Database\Eloquent\Relations\Relation; | ||||||
|  | 
 | ||||||
|  | final readonly class BuilderCommand | ||||||
|  | { | ||||||
|  |     public function execute(Relation | Builder $query, Repository $builderDto): Relation | Builder | ||||||
|  |     { | ||||||
|  |         return $query->when($builderDto->getIsPublic(), function (Builder $query) use ($builderDto) { | ||||||
|  |             $query->where('is_public', $builderDto->getIsPublic()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Repository; | ||||||
|  | 
 | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\User; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | 
 | ||||||
|  | final readonly class RepositoryCommandHandler | ||||||
|  | { | ||||||
|  |     public function handleStore(User $user, array $data, int|string $name): Repository | ||||||
|  |     { | ||||||
|  |         $data['name'] = Str::lower($name); | ||||||
|  |         $repository = $user->repositories()->create($data); | ||||||
|  | 
 | ||||||
|  |         return $repository; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleUpdate(Repository $repository, array $data): Repository | ||||||
|  |     { | ||||||
|  |         if (isset($data['name'])) { | ||||||
|  |             // It is not yet clear how to rename repositories in the registry service.
 | ||||||
|  |             unset($data['name']); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository->update($data); | ||||||
|  |         return $repository; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleDestroy(Repository $repository): void | ||||||
|  |     { | ||||||
|  |         $repository->delete(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								app/application/app/Services/Site/AccessTokenService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								app/application/app/Services/Site/AccessTokenService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Dto\Service\Site\AccessTokens\StoreUpdate; | ||||||
|  | use App\Enums\AccessTokenPermission; | ||||||
|  | use App\Helpers\Helpers; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Repositories\AccessTokenRepository; | ||||||
|  | use App\ServiceResults\ServiceResultArray; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\ServiceResults\ServiceResultSuccess; | ||||||
|  | use App\ServiceResults\Site\AccessTokenService\StoreTokenResult; | ||||||
|  | use App\ServiceResults\StoreUpdateResult; | ||||||
|  | use App\Services\Service; | ||||||
|  | 
 | ||||||
|  | final class AccessTokenService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly AccessTokenRepository $accessTokenRepository, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function index(QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray | ||||||
|  |     { | ||||||
|  |         $tokens = $this->accessTokenRepository->getUserTokens( | ||||||
|  |             $user, | ||||||
|  |             $querySettingsDto->getQueryWith() | ||||||
|  |         )->pagination( | ||||||
|  |             $querySettingsDto->getLimit(), | ||||||
|  |             $querySettingsDto->getPage() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'tokens' => $tokens | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function create(User $user): ServiceResultError | ServiceResultArray | ||||||
|  |     { | ||||||
|  |         return $this->result([ | ||||||
|  |             'user'           => $user, | ||||||
|  |             'serviceAddress' => Helpers::dockerServiceAddress(), | ||||||
|  |             'permissions'    => AccessTokenPermission::toCollection()->pluck('title', 'value')->toArray(), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function edit(int $id, User $user): ServiceResultError | ServiceResultArray | ||||||
|  |     { | ||||||
|  |         $token = $this->accessTokenRepository->getTokenById($user, $id); | ||||||
|  |         if (\is_null($token)) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'token'       => $token, | ||||||
|  |             'user'        => $user, | ||||||
|  |             'permissions' => AccessTokenPermission::toCollection()->pluck('title', 'value')->toArray(), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function store(StoreUpdate $data, User $user): ServiceResultError | StoreTokenResult | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $token = $user->createToken($data->getName(), [$data->getPermission()->value])->plainTextToken; | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new StoreTokenResult( | ||||||
|  |             name:       $data->getName(), | ||||||
|  |             permission: $data->getPermission(), | ||||||
|  |             token:      $token, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult | ||||||
|  |     { | ||||||
|  |         $token = $this->accessTokenRepository->getTokenById($user, $id); | ||||||
|  |         if (\is_null($token)) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $token->update([ | ||||||
|  |                 'name'      => $data->getName(), | ||||||
|  |                 'abilities' => [$data->getPermission()->value], | ||||||
|  |             ]); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->resultStoreUpdateModel($token, __('site.Token updated successfully')); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function destroy(int $id, User $user): ServiceResultError|ServiceResultSuccess | ||||||
|  |     { | ||||||
|  |         $token = $this->accessTokenRepository->getTokenById($user, $id); | ||||||
|  |         if (\is_null($token)) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $token->delete(); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok(__('site.The token has been deleted')); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										210
									
								
								app/application/app/Services/Site/RepositoryService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								app/application/app/Services/Site/RepositoryService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Builder\Repository as RepositoryBuilder; | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Dto\Service\Site\Repository\StoreUpdate; | ||||||
|  | use App\Helpers\Helpers; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Repositories\RepositoryRepository; | ||||||
|  | use App\Repositories\TagRepositoryRepository; | ||||||
|  | use App\Repositories\UserRepository; | ||||||
|  | use App\ServiceResults\ServiceResultArray; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\ServiceResults\ServiceResultSuccess; | ||||||
|  | use App\ServiceResults\StoreUpdateResult; | ||||||
|  | use App\Services\Repository\RepositoryCommandHandler; | ||||||
|  | use App\Services\Service; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | 
 | ||||||
|  | final class RepositoryService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly RepositoryRepository     $repositoryRepository, | ||||||
|  |         private readonly RepositoryCommandHandler $repositoryCommandHandler, | ||||||
|  |         private readonly UserRepository           $userRepository, | ||||||
|  |         private readonly TagRepositoryRepository  $tagRepositoryRepository, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function repositories(QuerySettingsDto $querySettingsDto): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $repositories = $this->repositoryRepository->getRepositories( | ||||||
|  |             new RepositoryBuilder(isPublic: true), | ||||||
|  |             $querySettingsDto->getQueryWith() | ||||||
|  |         )->pagination( | ||||||
|  |             $querySettingsDto->getLimit(), | ||||||
|  |             $querySettingsDto->getPage() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if ($querySettingsDto->getPage() > 1 && $repositories->count() === 0) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'repositories' => $repositories, | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function repository(string $username, string $repositoryName, ?User $user = null): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($pageUser, $repositoryName); | ||||||
|  |         if (!$repository || $user === null && $repository->is_public === false) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user?->cannot('view', $repository)) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'repository'     => $repository, | ||||||
|  |             'pageUser'       => $pageUser, | ||||||
|  |             'serviceAddress' => Helpers::dockerServiceAddress(), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function create(string $username, User $user): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->cannot('create', [Repository::class, $pageUser])) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'user'       => $user, | ||||||
|  |             'repository' => new Repository(), | ||||||
|  |             'pageUser'   => $pageUser, | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function edit(string $username, string $name, User $user): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($pageUser, $name); | ||||||
|  |         if (!$repository) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->cannot('update', $repository)) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'user'       => $user, | ||||||
|  |             'repository' => $repository, | ||||||
|  |             'pageUser'   => $pageUser, | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function store(string $username, StoreUpdate $data, User $user): StoreUpdateResult | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->cannot('create', [Repository::class, $pageUser])) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($this->repositoryRepository->isExistsName($pageUser, $data->getName())) { | ||||||
|  |             return $this->errValidate( | ||||||
|  |                 __('validation.unique', ['attribute' => __('validation.attributes.name')]), | ||||||
|  |                 ['name' => __('validation.unique', ['attribute' => __('validation.attributes.name')])] | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $modelRepository = DB::transaction(function () use ($data, $pageUser) { | ||||||
|  |                 $dataRepository = $this->getDataRepository($data); | ||||||
|  |                 return $this->repositoryCommandHandler->handleStore($pageUser, $dataRepository, $data->getName()); | ||||||
|  |             }); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             \report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->resultStoreUpdateModel($modelRepository, __('site.The repository has been successfully created')); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function update(string $username, string $name, StoreUpdate $data, User $user): StoreUpdateResult | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($pageUser, $name); | ||||||
|  |         if (!$repository) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->cannot('update', $repository)) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $modelRepository = DB::transaction(function () use ($data, $repository) { | ||||||
|  |                 $dataRepository = $this->getDataRepository($data); | ||||||
|  |                 return $this->repositoryCommandHandler->handleUpdate($repository, $dataRepository); | ||||||
|  |             }); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             \report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->resultStoreUpdateModel($modelRepository, __('site.The repository has been successfully updated')); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function destroy(string $username, string $name, User $user): ServiceResultError | ServiceResultSuccess | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($pageUser, $name); | ||||||
|  |         if (!$repository) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->cannot('delete', $repository)) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             DB::transaction(function () use ($repository) { | ||||||
|  |                 $this->repositoryCommandHandler->handleDestroy($repository); | ||||||
|  |             }); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             \report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok(__('site.Repository deleted successfully')); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getDataRepository(StoreUpdate $data): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'is_public'   => $data->isPublic(), | ||||||
|  |             'description' => $data->getDescription(), | ||||||
|  |             'overview'    => $data->getOverview(), | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										95
									
								
								app/application/app/Services/Site/TagRepositoryService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								app/application/app/Services/Site/TagRepositoryService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Helpers\Helpers; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Repositories\RepositoryRepository; | ||||||
|  | use App\Repositories\TagRepositoryRepository; | ||||||
|  | use App\Repositories\UserRepository; | ||||||
|  | use App\ServiceResults\ServiceResultArray; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\ServiceResults\ServiceResultSuccess; | ||||||
|  | use App\Services\Service; | ||||||
|  | use App\Services\TagRepository\TagRepositoryCommandHandler; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | 
 | ||||||
|  | final class TagRepositoryService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly RepositoryRepository        $repositoryRepository, | ||||||
|  |         private readonly UserRepository              $userRepository, | ||||||
|  |         private readonly TagRepositoryRepository     $tagRepositoryRepository, | ||||||
|  |         private readonly TagRepositoryCommandHandler $tagRepositoryCommandHandler, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function tags(string $username, string $repositoryName, QuerySettingsDto $querySettingsDto, ?User $user = null): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($pageUser, $repositoryName); | ||||||
|  |         if (!$repository || $user === null && $repository->is_public === false) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user?->cannot('view', $repository)) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $tags = $this->tagRepositoryRepository->getTagsRepository( | ||||||
|  |             $repository, | ||||||
|  |             $querySettingsDto->getQueryWith() | ||||||
|  |         )->pagination( | ||||||
|  |             $querySettingsDto->getLimit(), | ||||||
|  |             $querySettingsDto->getPage() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if ($querySettingsDto->getPage() > 1 && $tags->count() === 0) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'tags'           => $tags, | ||||||
|  |             'repository'     => $repository, | ||||||
|  |             'pageUser'       => $pageUser, | ||||||
|  |             'serviceAddress' => Helpers::dockerServiceAddress(), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function destroy(string $username, string $repositoryName, int $tagId, User $user): ServiceResultError | ServiceResultSuccess | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repository = $this->repositoryRepository->getRepositoryByName($pageUser, $repositoryName); | ||||||
|  |         if (!$repository) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $tag = $this->tagRepositoryRepository->getTagById($tagId); | ||||||
|  |         if (!$tag || $tag->repository_id !== $repository->id) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->cannot('delete', $tag)) { | ||||||
|  |             return $this->errFobidden(__('Access is denied')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             DB::transaction(function () use ($tag) { | ||||||
|  |                 $this->tagRepositoryCommandHandler->handleDestroy($tag); | ||||||
|  |             }); | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             \report($e); | ||||||
|  |             return $this->errService(__('Server Error')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok(__('site.Tag removed successfully')); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								app/application/app/Services/Site/UserService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/application/app/Services/Site/UserService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Site; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Builder\Repository as RepositoryBuilder; | ||||||
|  | use App\Dto\QuerySettingsDto; | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Repositories\RepositoryRepository; | ||||||
|  | use App\Repositories\UserRepository; | ||||||
|  | use App\ServiceResults\ServiceResultArray; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\Services\Service; | ||||||
|  | 
 | ||||||
|  | final class UserService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly UserRepository       $userRepository, | ||||||
|  |         private readonly RepositoryRepository $repositoryRepository, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function repositories(string $username, QuerySettingsDto $querySettingsDto, ?User $user = null): ServiceResultArray | ServiceResultError | ||||||
|  |     { | ||||||
|  |         $pageUser = $this->userRepository->getUserByUsername($username); | ||||||
|  |         if ($pageUser === null) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $isPublic = true; | ||||||
|  |         if ($user?->id === $pageUser->id) { | ||||||
|  |             $isPublic = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $repositories = $this->repositoryRepository->getUserRepositories( | ||||||
|  |             $pageUser, | ||||||
|  |             new RepositoryBuilder(isPublic: $isPublic), | ||||||
|  |             $querySettingsDto->getQueryWith() | ||||||
|  |         )->pagination( | ||||||
|  |             $querySettingsDto->getLimit(), | ||||||
|  |             $querySettingsDto->getPage() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if ($querySettingsDto->getPage() > 1 && $repositories->count() === 0) { | ||||||
|  |             return $this->errNotFound(__('Not Found')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->result([ | ||||||
|  |             'pageUser'     => $pageUser, | ||||||
|  |             'repositories' => $repositories, | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\TagRepository; | ||||||
|  | 
 | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\ServiceResults\TagRepository\IncrementPullResult; | ||||||
|  | use App\Services\Service; | ||||||
|  | 
 | ||||||
|  | final class IncrementPullCommand extends Service | ||||||
|  | { | ||||||
|  |     public function execute(Repository $repository, string $tagName, string $digest): ServiceResultError | IncrementPullResult | ||||||
|  |     { | ||||||
|  |         $tag = $repository->tags() | ||||||
|  |                 ->withTrashed() | ||||||
|  |                 ->where('name', $tagName) | ||||||
|  |                 ->where('digest', $digest) | ||||||
|  |                 ->first(); | ||||||
|  | 
 | ||||||
|  |         if (null === $tag) { | ||||||
|  |             return $this->errNotFound(__('Not Found') . ' Tag: ' . $tagName . ':' . $digest); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Repository::withTrashed()->where('id', $repository->id) | ||||||
|  |             // So that the updated_at field is not updated
 | ||||||
|  |             ->where(fn($q) => $q->getModel()->timestamps = false) | ||||||
|  |             ->increment('quantity_pulls'); | ||||||
|  | 
 | ||||||
|  |         TagRepository::withTrashed()->where('id', $tag->id) | ||||||
|  |             // So that the updated_at field is not updated
 | ||||||
|  |             ->where(fn($q) => $q->getModel()->timestamps = false) | ||||||
|  |             ->increment('quantity_pulls'); | ||||||
|  | 
 | ||||||
|  |         return new IncrementPullResult($repository, $tag); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\TagRepository; | ||||||
|  | 
 | ||||||
|  | use App\Models\Architecture; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
|  | 
 | ||||||
|  | final readonly class TagRepositoryCommandHandler | ||||||
|  | { | ||||||
|  |     public function handleStore(Repository $repository, Architecture $architecture, string $name, array $data): TagRepository | ||||||
|  |     { | ||||||
|  |         $tag = new TagRepository(); | ||||||
|  |         $tag->repository_id   = $repository->id; | ||||||
|  |         $tag->architecture_id = $architecture->id; | ||||||
|  |         $tag->name            = $name; | ||||||
|  | 
 | ||||||
|  |         $tag->fill($data); | ||||||
|  |         $tag->save(); | ||||||
|  | 
 | ||||||
|  |         return $tag; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleUpdate(TagRepository $tag, array $data): TagRepository | ||||||
|  |     { | ||||||
|  |         $tag->update($data); | ||||||
|  |         return $tag; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleDestroy(TagRepository $tag): void | ||||||
|  |     { | ||||||
|  |         $tag->delete(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\TagRepository; | ||||||
|  | 
 | ||||||
|  | use App\Models\Architecture; | ||||||
|  | use App\Models\Repository; | ||||||
|  | use App\Models\TagRepository; | ||||||
|  | 
 | ||||||
|  | final readonly class UpdateOrCreateCommand | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private TagRepositoryCommandHandler $tagRepositoryCommandHandler, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(Repository $repository, Architecture $architecture, string $name, array $data): TagRepository | ||||||
|  |     { | ||||||
|  |         $tag = TagRepository::withTrashed() | ||||||
|  |             ->where('repository_id', $repository->id) | ||||||
|  |             ->where('architecture_id', $architecture->id) | ||||||
|  |             ->where('name', $name) | ||||||
|  |             ->first(); | ||||||
|  | 
 | ||||||
|  |         if (null === $tag) { | ||||||
|  |             return $this->tagRepositoryCommandHandler->handleStore($repository, $architecture, $name, $data); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->tagRepositoryCommandHandler->handleUpdate($tag, $data); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -28,7 +28,8 @@ final readonly class UserCommandHandler | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (isset($data['username'])) { |         if (isset($data['username'])) { | ||||||
|             $data['username'] = Str::lower($data['username']); |             // It is not yet clear how to rename repositories in the registry service.
 | ||||||
|  |             unset($data['username']); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (isset($data['password'])) { |         if (isset($data['password'])) { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| namespace App\View\Components\Layout; | namespace App\View\Components\Layout; | ||||||
| 
 | 
 | ||||||
|  | use Illuminate\Pagination\Paginator; | ||||||
| use Illuminate\View\Component; | use Illuminate\View\Component; | ||||||
| use Illuminate\View\View; | use Illuminate\View\View; | ||||||
| 
 | 
 | ||||||
| @@ -13,6 +14,8 @@ final class Site extends Component | |||||||
|      */ |      */ | ||||||
|     public function render(): View |     public function render(): View | ||||||
|     { |     { | ||||||
|  |         Paginator::defaultView('pagination.site'); | ||||||
|  | 
 | ||||||
|         return view('layout.site'); |         return view('layout.site'); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ final class Input extends Form | |||||||
|         private readonly string $name, |         private readonly string $name, | ||||||
|         private readonly string $type = 'text', |         private readonly string $type = 'text', | ||||||
|         private readonly ?string $value = '', |         private readonly ?string $value = '', | ||||||
|  |         private readonly ?string $example = null, | ||||||
|  |         private readonly ?string $allowedCharacters = null, | ||||||
|     ) { } |     ) { } | ||||||
| 
 | 
 | ||||||
|     protected function getName(): string |     protected function getName(): string | ||||||
| @@ -34,6 +36,16 @@ final class Input extends Form | |||||||
|         return (string) old($this->getRequestName(), $this->value); |         return (string) old($this->getRequestName(), $this->value); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private function getExample(): ?string | ||||||
|  |     { | ||||||
|  |         return $this->example; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getAllowedCharacters(): ?string | ||||||
|  |     { | ||||||
|  |         return $this->allowedCharacters; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritDoc |      * @inheritDoc | ||||||
|      */ |      */ | ||||||
| @@ -45,6 +57,8 @@ final class Input extends Form | |||||||
|             'requestName' => $this->getRequestName(), |             'requestName' => $this->getRequestName(), | ||||||
|             'type' => $this->getType(), |             'type' => $this->getType(), | ||||||
|             'value' => $this->getValue(), |             'value' => $this->getValue(), | ||||||
|  |             'example' => $this->getExample(), | ||||||
|  |             'allowedCharacters' => $this->getAllowedCharacters(), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								app/application/app/View/Components/Site/Forms/Textarea.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/application/app/View/Components/Site/Forms/Textarea.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\View\Components\Site\Forms; | ||||||
|  | 
 | ||||||
|  | use App\View\Components\Volt\Forms\Form; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | 
 | ||||||
|  | final class Textarea extends Form | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly string $title, | ||||||
|  |         private readonly string $name, | ||||||
|  |         private readonly ?string $value = '', | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     protected function getName(): string | ||||||
|  |     { | ||||||
|  |         return $this->name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getTitle(): string | ||||||
|  |     { | ||||||
|  |         return Str::ucfirst($this->title); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getValue(): string | ||||||
|  |     { | ||||||
|  |         return (string) old($this->getRequestName(), $this->value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritDoc | ||||||
|  |      */ | ||||||
|  |     public function render(): View | ||||||
|  |     { | ||||||
|  |         return view('components.site.forms.textarea', [ | ||||||
|  |             'title' => $this->getTitle(), | ||||||
|  |             'name' => $this->getName(), | ||||||
|  |             'requestName' => $this->getRequestName(), | ||||||
|  |             'value' => $this->getValue(), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,6 +4,7 @@ use Illuminate\Foundation\Application; | |||||||
| use Illuminate\Foundation\Configuration\Exceptions; | use Illuminate\Foundation\Configuration\Exceptions; | ||||||
| use Illuminate\Foundation\Configuration\Middleware; | use Illuminate\Foundation\Configuration\Middleware; | ||||||
| use Illuminate\Support\Facades\Route; | use Illuminate\Support\Facades\Route; | ||||||
|  | use Illuminate\Http\Request; | ||||||
| 
 | 
 | ||||||
| return Application::configure(basePath: dirname(__DIR__)) | return Application::configure(basePath: dirname(__DIR__)) | ||||||
|     ->withRouting( |     ->withRouting( | ||||||
| @@ -16,11 +17,38 @@ return Application::configure(basePath: dirname(__DIR__)) | |||||||
|                 ->prefix('admin') |                 ->prefix('admin') | ||||||
|                 ->as('admin.') |                 ->as('admin.') | ||||||
|                 ->group(base_path('routes/admin.php')); |                 ->group(base_path('routes/admin.php')); | ||||||
|  | 
 | ||||||
|  |             Route::middleware(['api']) | ||||||
|  |                 ->prefix('registry') | ||||||
|  |                 ->name('registry.') | ||||||
|  |                 ->group(base_path('routes/registry.php')); | ||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
|     ->withMiddleware(function (Middleware $middleware) { |     ->withMiddleware(function (Middleware $middleware) { | ||||||
|         //
 |         $middleware->validateCsrfTokens(except: [ | ||||||
|  |             '/registry/v2/event', | ||||||
|  |             '/registry/v2/event/*', | ||||||
|  |         ]); | ||||||
|     }) |     }) | ||||||
|     ->withExceptions(function (Exceptions $exceptions) { |     ->withExceptions(function (Exceptions $exceptions) { | ||||||
|         //
 | 
 | ||||||
|  |         $exceptions->renderable(function (\Illuminate\Auth\AuthenticationException $e, Request $request) { | ||||||
|  |             if ($request->is('registry/*')) { | ||||||
|  |                 return \response()->json([ | ||||||
|  |                     'message' => $e->getMessage(), | ||||||
|  |                 ], \Illuminate\Http\Response::HTTP_UNAUTHORIZED); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return false; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $exceptions->renderable(function (\Illuminate\Http\Exceptions\HttpResponseException $e, Request $request) { | ||||||
|  |             if ($request->is('registry/*')) { | ||||||
|  |                 return \response()->json([ | ||||||
|  |                     'message' => $e->getResponse()->getContent(), | ||||||
|  |                 ], $e->getResponse()->getStatusCode()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return false; | ||||||
|  |         }); | ||||||
|     })->create(); |     })->create(); | ||||||
|   | |||||||
| @@ -2,4 +2,6 @@ | |||||||
| 
 | 
 | ||||||
| return [ | return [ | ||||||
|     App\Providers\AppServiceProvider::class, |     App\Providers\AppServiceProvider::class, | ||||||
|  |     App\Providers\RegistryServiceProvider::class, | ||||||
|  |     App\Providers\BladeProvider::class, | ||||||
| ]; | ]; | ||||||
|   | |||||||
| @@ -6,9 +6,12 @@ | |||||||
|     "license": "MIT", |     "license": "MIT", | ||||||
|     "require": { |     "require": { | ||||||
|         "php": "^8.3", |         "php": "^8.3", | ||||||
|  |         "firebase/php-jwt": "^6.10", | ||||||
|         "kor-elf/captcha-rule-for-laravel": "^1.0", |         "kor-elf/captcha-rule-for-laravel": "^1.0", | ||||||
|         "laravel/framework": "^11.9", |         "laravel/framework": "^11.9", | ||||||
|         "laravel/tinker": "^2.9" |         "laravel/sanctum": "^4.0", | ||||||
|  |         "laravel/tinker": "^2.9", | ||||||
|  |         "ext-openssl": "*" | ||||||
|     }, |     }, | ||||||
|     "require-dev": { |     "require-dev": { | ||||||
|         "barryvdh/laravel-debugbar": "^3.13", |         "barryvdh/laravel-debugbar": "^3.13", | ||||||
|   | |||||||
							
								
								
									
										129
									
								
								app/application/composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										129
									
								
								app/application/composer.lock
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | |||||||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", |         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||||
|         "This file is @generated automatically" |         "This file is @generated automatically" | ||||||
|     ], |     ], | ||||||
|     "content-hash": "5768ab30453b30f69da2a2d7ef5c21c5", |     "content-hash": "f10d849a6239c9c56993d1c9ab78c027", | ||||||
|     "packages": [ |     "packages": [ | ||||||
|         { |         { | ||||||
|             "name": "brick/math", |             "name": "brick/math", | ||||||
| @@ -506,6 +506,69 @@ | |||||||
|             ], |             ], | ||||||
|             "time": "2023-10-06T06:47:41+00:00" |             "time": "2023-10-06T06:47:41+00:00" | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             "name": "firebase/php-jwt", | ||||||
|  |             "version": "v6.10.1", | ||||||
|  |             "source": { | ||||||
|  |                 "type": "git", | ||||||
|  |                 "url": "https://github.com/firebase/php-jwt.git", | ||||||
|  |                 "reference": "500501c2ce893c824c801da135d02661199f60c5" | ||||||
|  |             }, | ||||||
|  |             "dist": { | ||||||
|  |                 "type": "zip", | ||||||
|  |                 "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", | ||||||
|  |                 "reference": "500501c2ce893c824c801da135d02661199f60c5", | ||||||
|  |                 "shasum": "" | ||||||
|  |             }, | ||||||
|  |             "require": { | ||||||
|  |                 "php": "^8.0" | ||||||
|  |             }, | ||||||
|  |             "require-dev": { | ||||||
|  |                 "guzzlehttp/guzzle": "^7.4", | ||||||
|  |                 "phpspec/prophecy-phpunit": "^2.0", | ||||||
|  |                 "phpunit/phpunit": "^9.5", | ||||||
|  |                 "psr/cache": "^2.0||^3.0", | ||||||
|  |                 "psr/http-client": "^1.0", | ||||||
|  |                 "psr/http-factory": "^1.0" | ||||||
|  |             }, | ||||||
|  |             "suggest": { | ||||||
|  |                 "ext-sodium": "Support EdDSA (Ed25519) signatures", | ||||||
|  |                 "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" | ||||||
|  |             }, | ||||||
|  |             "type": "library", | ||||||
|  |             "autoload": { | ||||||
|  |                 "psr-4": { | ||||||
|  |                     "Firebase\\JWT\\": "src" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "notification-url": "https://packagist.org/downloads/", | ||||||
|  |             "license": [ | ||||||
|  |                 "BSD-3-Clause" | ||||||
|  |             ], | ||||||
|  |             "authors": [ | ||||||
|  |                 { | ||||||
|  |                     "name": "Neuman Vong", | ||||||
|  |                     "email": "neuman+pear@twilio.com", | ||||||
|  |                     "role": "Developer" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "name": "Anant Narayanan", | ||||||
|  |                     "email": "anant@php.net", | ||||||
|  |                     "role": "Developer" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", | ||||||
|  |             "homepage": "https://github.com/firebase/php-jwt", | ||||||
|  |             "keywords": [ | ||||||
|  |                 "jwt", | ||||||
|  |                 "php" | ||||||
|  |             ], | ||||||
|  |             "support": { | ||||||
|  |                 "issues": "https://github.com/firebase/php-jwt/issues", | ||||||
|  |                 "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" | ||||||
|  |             }, | ||||||
|  |             "time": "2024-05-18T18:05:11+00:00" | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             "name": "fruitcake/php-cors", |             "name": "fruitcake/php-cors", | ||||||
|             "version": "v1.3.0", |             "version": "v1.3.0", | ||||||
| @@ -1363,6 +1426,70 @@ | |||||||
|             }, |             }, | ||||||
|             "time": "2024-05-27T13:53:20+00:00" |             "time": "2024-05-27T13:53:20+00:00" | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             "name": "laravel/sanctum", | ||||||
|  |             "version": "v4.0.2", | ||||||
|  |             "source": { | ||||||
|  |                 "type": "git", | ||||||
|  |                 "url": "https://github.com/laravel/sanctum.git", | ||||||
|  |                 "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1" | ||||||
|  |             }, | ||||||
|  |             "dist": { | ||||||
|  |                 "type": "zip", | ||||||
|  |                 "url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1", | ||||||
|  |                 "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1", | ||||||
|  |                 "shasum": "" | ||||||
|  |             }, | ||||||
|  |             "require": { | ||||||
|  |                 "ext-json": "*", | ||||||
|  |                 "illuminate/console": "^11.0", | ||||||
|  |                 "illuminate/contracts": "^11.0", | ||||||
|  |                 "illuminate/database": "^11.0", | ||||||
|  |                 "illuminate/support": "^11.0", | ||||||
|  |                 "php": "^8.2", | ||||||
|  |                 "symfony/console": "^7.0" | ||||||
|  |             }, | ||||||
|  |             "require-dev": { | ||||||
|  |                 "mockery/mockery": "^1.6", | ||||||
|  |                 "orchestra/testbench": "^9.0", | ||||||
|  |                 "phpstan/phpstan": "^1.10", | ||||||
|  |                 "phpunit/phpunit": "^10.5" | ||||||
|  |             }, | ||||||
|  |             "type": "library", | ||||||
|  |             "extra": { | ||||||
|  |                 "laravel": { | ||||||
|  |                     "providers": [ | ||||||
|  |                         "Laravel\\Sanctum\\SanctumServiceProvider" | ||||||
|  |                     ] | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "autoload": { | ||||||
|  |                 "psr-4": { | ||||||
|  |                     "Laravel\\Sanctum\\": "src/" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "notification-url": "https://packagist.org/downloads/", | ||||||
|  |             "license": [ | ||||||
|  |                 "MIT" | ||||||
|  |             ], | ||||||
|  |             "authors": [ | ||||||
|  |                 { | ||||||
|  |                     "name": "Taylor Otwell", | ||||||
|  |                     "email": "taylor@laravel.com" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", | ||||||
|  |             "keywords": [ | ||||||
|  |                 "auth", | ||||||
|  |                 "laravel", | ||||||
|  |                 "sanctum" | ||||||
|  |             ], | ||||||
|  |             "support": { | ||||||
|  |                 "issues": "https://github.com/laravel/sanctum/issues", | ||||||
|  |                 "source": "https://github.com/laravel/sanctum" | ||||||
|  |             }, | ||||||
|  |             "time": "2024-04-10T19:39:58+00:00" | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             "name": "laravel/serializable-closure", |             "name": "laravel/serializable-closure", | ||||||
|             "version": "v1.3.3", |             "version": "v1.3.3", | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user