Implemented interaction with docker registry.
This commit is contained in:
		| @@ -1,6 +1,7 @@ | ||||
| DOCKER_APP_PORT=8080 | ||||
| DOCKER_CAPTCHA_PORT=8081 | ||||
| DOCKER_CAPTCHA_WEBSOCKET_PORT=8082 | ||||
| DOCKER_REGISTRY_WEB=8083 | ||||
| DOCKER_DB_PORT=3306 | ||||
| MYSQL_ROOT_PASSWORD=root_pass | ||||
| DB_DATABASE=registry | ||||
|   | ||||
| @@ -6,6 +6,14 @@ DEBUGBAR_OPEN_STORAGE=true | ||||
| APP_TIMEZONE=UTC | ||||
| 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 | ||||
| CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081 | ||||
| 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( | ||||
|         private string $name, | ||||
|         private string $email, | ||||
|         private string $username, | ||||
|         private bool   $isActive, | ||||
|         private ManyRoleDto $roles, | ||||
|         private ?string $username = null, | ||||
|         private ?string $password = null | ||||
|     ) { } | ||||
| 
 | ||||
| @@ -36,7 +36,7 @@ final readonly class StoreUpdate extends Dto | ||||
|         return $this->roles; | ||||
|     } | ||||
| 
 | ||||
|     public function getUsername(): string | ||||
|     public function getUsername(): ?string | ||||
|     { | ||||
|         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,10 @@ final readonly class Helpers | ||||
|             ->rtrim('.') | ||||
|             ->value(); | ||||
|     } | ||||
| 
 | ||||
|     public static function shortDigest(string $digest): string | ||||
|     { | ||||
|         $digest = Str::of($digest)->explode(':', 2)->last(); | ||||
|         return Str::of($digest)->take(12)->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; | ||||
| 
 | ||||
| use App\Contracts\ServiceResultError as ServiceResultErrorContract; | ||||
| use Illuminate\Http\Response; | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
| 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 = [ | ||||
|             'name' => ['required', 'max:255'], | ||||
|             'email' => ['required', 'email', 'max:255'], | ||||
|             'username' => ['required', 'string', new Username()], | ||||
|             'is_active' => ['required', 'boolean'], | ||||
|             'roles' => ['array', Rule::exists('roles', 'id')], | ||||
|         ]; | ||||
| 
 | ||||
|         if ($this->getMethod() === 'POST') { | ||||
|             $rules['password'] = ['required', Password::default()]; | ||||
|             $rules['username'] = ['required', 'string', new Username()]; | ||||
|         } | ||||
| 
 | ||||
|         return $rules; | ||||
| @@ -38,9 +38,9 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto | ||||
|         return new StoreUpdate( | ||||
|             name: $this->input('name'), | ||||
|             email: $this->input('email'), | ||||
|             username: $this->input('username'), | ||||
|             isActive: (bool) $this->input('is_active', false), | ||||
|             roles: new ManyRoleDto($this->input('roles', [])), | ||||
|             username: $this->input('username', 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\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsToMany; | ||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
| use Illuminate\Database\Eloquent\SoftDeletes; | ||||
| use Illuminate\Foundation\Auth\User as Authenticatable; | ||||
| use Illuminate\Notifications\Notifiable; | ||||
| use Laravel\Sanctum\HasApiTokens; | ||||
| 
 | ||||
| final class User extends Authenticatable | ||||
| { | ||||
|     use HasFactory, Notifiable, SoftDeletes; | ||||
|     use HasApiTokens, HasFactory, Notifiable, SoftDeletes; | ||||
| 
 | ||||
|     /** | ||||
|      * 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 | ||||
|      */ | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
| 
 | ||||
| namespace App\Policies; | ||||
| 
 | ||||
| use App\Models\Repository; | ||||
| use App\Models\TagRepository; | ||||
| use App\Models\User; | ||||
| use Illuminate\Auth\Access\HandlesAuthorization; | ||||
| 
 | ||||
| @@ -9,13 +11,23 @@ abstract readonly class Policy | ||||
| { | ||||
|     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) { | ||||
|             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; | ||||
|         } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										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; | ||||
| 
 | ||||
| use App\Enums\Morph; | ||||
| use App\Services\Search\CreateSearchInstanceCommand; | ||||
| use App\Services\Search\Search; | ||||
| use Illuminate\Cache\RateLimiting\Limit; | ||||
| use Illuminate\Database\Eloquent\Relations\Relation; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Facades\Gate; | ||||
| use Illuminate\Support\Facades\RateLimiter; | ||||
| use Illuminate\Support\Facades\Route; | ||||
| use Illuminate\Support\Facades\URL; | ||||
| use Illuminate\Support\ServiceProvider; | ||||
| 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. | ||||
| @@ -35,8 +40,25 @@ class AppServiceProvider extends ServiceProvider | ||||
| 
 | ||||
|         $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(); | ||||
|         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(); | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|     { | ||||
|         $query = $this->builderCommand->execute( | ||||
|   | ||||
| @@ -21,9 +21,9 @@ final readonly class Repository implements ValidationRule | ||||
|         } | ||||
| 
 | ||||
|         $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()) { | ||||
|             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); | ||||
| 
 | ||||
|         if (is_null($modelUser)) { | ||||
|         if (\is_null($modelUser)) { | ||||
|             return $this->errNotFound(__('Not Found')); | ||||
|         } | ||||
| 
 | ||||
| @@ -101,14 +101,14 @@ final class UserService extends Service | ||||
|         try { | ||||
|             $modelUser = DB::transaction(function () use ($data) { | ||||
|                 $dataUser = $this->getDataUser($data); | ||||
| 
 | ||||
|                 $dataUser['username'] = $data->getUsername(); | ||||
|                 $modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword()); | ||||
|                 $this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles()); | ||||
| 
 | ||||
|                 return $modelUser; | ||||
|             }); | ||||
|         } catch (\Throwable $e) { | ||||
|             report($e); | ||||
|             \report($e); | ||||
|             return $this->errService(__('Server Error')); | ||||
|         } | ||||
| 
 | ||||
| @@ -119,7 +119,7 @@ final class UserService extends Service | ||||
|     { | ||||
|         $modelUser = $this->userRepository->getUserById($id); | ||||
| 
 | ||||
|         if (is_null($modelUser)) { | ||||
|         if (\is_null($modelUser)) { | ||||
|             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 { | ||||
|             $modelUser = DB::transaction(function () use ($data, $modelUser) { | ||||
|                 $dataUser = $this->getDataUser($data); | ||||
| @@ -151,7 +144,7 @@ final class UserService extends Service | ||||
|                 return $modelUser; | ||||
|             }); | ||||
|         } catch (\Throwable $e) { | ||||
|             report($e); | ||||
|             \report($e); | ||||
|             return $this->errService(__('Server Error')); | ||||
|         } | ||||
| 
 | ||||
| @@ -162,7 +155,7 @@ final class UserService extends Service | ||||
|     { | ||||
|         $modelUser = $this->userRepository->getUserById($id); | ||||
| 
 | ||||
|         if (is_null($modelUser)) { | ||||
|         if (\is_null($modelUser)) { | ||||
|             return $this->errNotFound(__('Not Found')); | ||||
|         } | ||||
| 
 | ||||
| @@ -173,7 +166,7 @@ final class UserService extends Service | ||||
|         try { | ||||
|             $this->userCommandHandler->handleUpdatePassword($modelUser, $data->getPassword()); | ||||
|         } catch (\Throwable $e) { | ||||
|             report($e->getMessage()); | ||||
|             \report($e->getMessage()); | ||||
|             return $this->errService($e->getMessage()); | ||||
|         } | ||||
| 
 | ||||
| @@ -184,7 +177,7 @@ final class UserService extends Service | ||||
|     { | ||||
|         $modelUser = $this->userRepository->getUserById($id); | ||||
| 
 | ||||
|         if (is_null($modelUser)) { | ||||
|         if (\is_null($modelUser)) { | ||||
|             return $this->errNotFound(__('Not Found')); | ||||
|         } | ||||
| 
 | ||||
| @@ -197,7 +190,7 @@ final class UserService extends Service | ||||
|                 $this->userCommandHandler->handleDestroy($modelUser); | ||||
|             }); | ||||
|         } catch (\Throwable $e) { | ||||
|             report($e); | ||||
|             \report($e); | ||||
|             return $this->errService(__('Server Error')); | ||||
|         } | ||||
| 
 | ||||
| @@ -209,7 +202,6 @@ final class UserService extends Service | ||||
|         return [ | ||||
|             'name'      => $data->getName(), | ||||
|             'email'     => $data->getEmail(), | ||||
|             'username'  => $data->getUsername(), | ||||
|             '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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										113
									
								
								app/application/app/Services/Site/AccessTokenService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								app/application/app/Services/Site/AccessTokenService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <?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\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' => config('registry.service_http'), | ||||
|             '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')); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										209
									
								
								app/application/app/Services/Site/RepositoryService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								app/application/app/Services/Site/RepositoryService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| <?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\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' => config('registry.service_http'), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     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(), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										94
									
								
								app/application/app/Services/Site/TagRepositoryService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								app/application/app/Services/Site/TagRepositoryService.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| <?php declare(strict_types=1); | ||||
| 
 | ||||
| namespace App\Services\Site; | ||||
| 
 | ||||
| use App\Dto\QuerySettingsDto; | ||||
| 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' => config('registry.service_http'), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     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'])) { | ||||
|             $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'])) { | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| 
 | ||||
| namespace App\View\Components\Layout; | ||||
| 
 | ||||
| use Illuminate\Pagination\Paginator; | ||||
| use Illuminate\View\Component; | ||||
| use Illuminate\View\View; | ||||
| 
 | ||||
| @@ -13,6 +14,8 @@ final class Site extends Component | ||||
|      */ | ||||
|     public function render(): View | ||||
|     { | ||||
|         Paginator::defaultView('pagination.site'); | ||||
| 
 | ||||
|         return view('layout.site'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,8 @@ final class Input extends Form | ||||
|         private readonly string $name, | ||||
|         private readonly string $type = 'text', | ||||
|         private readonly ?string $value = '', | ||||
|         private readonly ?string $example = null, | ||||
|         private readonly ?string $allowedCharacters = null, | ||||
|     ) { } | ||||
| 
 | ||||
|     protected function getName(): string | ||||
| @@ -34,6 +36,16 @@ final class Input extends Form | ||||
|         return (string) old($this->getRequestName(), $this->value); | ||||
|     } | ||||
| 
 | ||||
|     private function getExample(): ?string | ||||
|     { | ||||
|         return $this->example; | ||||
|     } | ||||
| 
 | ||||
|     public function getAllowedCharacters(): ?string | ||||
|     { | ||||
|         return $this->allowedCharacters; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritDoc | ||||
|      */ | ||||
| @@ -45,6 +57,8 @@ final class Input extends Form | ||||
|             'requestName' => $this->getRequestName(), | ||||
|             'type' => $this->getType(), | ||||
|             '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\Middleware; | ||||
| use Illuminate\Support\Facades\Route; | ||||
| use Illuminate\Http\Request; | ||||
| 
 | ||||
| return Application::configure(basePath: dirname(__DIR__)) | ||||
|     ->withRouting( | ||||
| @@ -16,11 +17,38 @@ return Application::configure(basePath: dirname(__DIR__)) | ||||
|                 ->prefix('admin') | ||||
|                 ->as('admin.') | ||||
|                 ->group(base_path('routes/admin.php')); | ||||
| 
 | ||||
|             Route::middleware(['api']) | ||||
|                 ->prefix('registry') | ||||
|                 ->name('registry.') | ||||
|                 ->group(base_path('routes/registry.php')); | ||||
|         }, | ||||
|     ) | ||||
|     ->withMiddleware(function (Middleware $middleware) { | ||||
|         //
 | ||||
|         $middleware->validateCsrfTokens(except: [ | ||||
|             '/registry/v2/event', | ||||
|             '/registry/v2/event/*', | ||||
|         ]); | ||||
|     }) | ||||
|     ->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(); | ||||
|   | ||||
| @@ -2,4 +2,6 @@ | ||||
| 
 | ||||
| return [ | ||||
|     App\Providers\AppServiceProvider::class, | ||||
|     App\Providers\RegistryServiceProvider::class, | ||||
|     App\Providers\BladeProvider::class, | ||||
| ]; | ||||
|   | ||||
| @@ -6,9 +6,12 @@ | ||||
|     "license": "MIT", | ||||
|     "require": { | ||||
|         "php": "^8.3", | ||||
|         "firebase/php-jwt": "^6.10", | ||||
|         "kor-elf/captcha-rule-for-laravel": "^1.0", | ||||
|         "laravel/framework": "^11.9", | ||||
|         "laravel/tinker": "^2.9" | ||||
|         "laravel/sanctum": "^4.0", | ||||
|         "laravel/tinker": "^2.9", | ||||
|         "ext-openssl": "*" | ||||
|     }, | ||||
|     "require-dev": { | ||||
|         "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", | ||||
|         "This file is @generated automatically" | ||||
|     ], | ||||
|     "content-hash": "5768ab30453b30f69da2a2d7ef5c21c5", | ||||
|     "content-hash": "f10d849a6239c9c56993d1c9ab78c027", | ||||
|     "packages": [ | ||||
|         { | ||||
|             "name": "brick/math", | ||||
| @@ -506,6 +506,69 @@ | ||||
|             ], | ||||
|             "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", | ||||
|             "version": "v1.3.0", | ||||
| @@ -1363,6 +1426,70 @@ | ||||
|             }, | ||||
|             "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", | ||||
|             "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