Compare commits
6 Commits
a8d656148a
...
15388b4894
Author | SHA1 | Date | |
---|---|---|---|
15388b4894 | |||
e5c4abdf28 | |||
f3b6c8cd49 | |||
7501968d04 | |||
a949eae777 | |||
10ccf2f800 |
@ -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
|
||||
|
@ -1,11 +1,19 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:LAXuVFnfcrPlPcYftpV/hJ7mEO1TlbriMaDN7hT5WOo=
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
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 @@
|
||||
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 @@ public function getRoles(): ManyRoleDto
|
||||
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,15 @@ public static function formatAttributeNameToRequestName(string $name): string
|
||||
->rtrim('.')
|
||||
->value();
|
||||
}
|
||||
|
||||
public static function shortDigest(string $digest): string
|
||||
{
|
||||
$digest = Str::of($digest)->explode(':', 2)->last();
|
||||
return Str::of($digest)->take(12)->toString();
|
||||
}
|
||||
|
||||
public static function dockerServiceAddress(): string
|
||||
{
|
||||
return Str::of(config('registry.service_http'))->remove(['http://', 'https://'])->toString();
|
||||
}
|
||||
}
|
||||
|
74
app/application/app/Helpers/Number.php
Normal file
74
app/application/app/Helpers/Number.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Illuminate\Support\Number as LaravelNumber;
|
||||
|
||||
final readonly class Number
|
||||
{
|
||||
private const array ABBREVIATE_UNITS = [
|
||||
3 => 'K',
|
||||
6 => 'M',
|
||||
9 => 'B',
|
||||
12 => 'T',
|
||||
15 => 'Q',
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert the number to its human-readable equivalent.
|
||||
*
|
||||
* @param int|float $number
|
||||
* @return string
|
||||
*/
|
||||
public static function abbreviate(int|float $number): string
|
||||
{
|
||||
if ($number < 0) {
|
||||
throw new \InvalidArgumentException('Number cannot be negative');
|
||||
}
|
||||
|
||||
return self::summarize($number);
|
||||
}
|
||||
|
||||
/**
|
||||
* $size - Size in bytes
|
||||
* @param int $size
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sizeForHumans(int $size): string
|
||||
{
|
||||
return LaravelNumber::fileSize($size);
|
||||
}
|
||||
|
||||
private static function summarize(int|float $number, bool $hidePlusSymbol = false): string
|
||||
{
|
||||
$symbol = '+';
|
||||
if ($hidePlusSymbol) {
|
||||
$symbol = '';
|
||||
}
|
||||
|
||||
if ($number < 1000) {
|
||||
return (string) $number;
|
||||
}
|
||||
|
||||
if ($number >= 1e15) {
|
||||
$units = self::ABBREVIATE_UNITS;
|
||||
return sprintf('%s' . end($units), self::summarize($number / 1e15, hidePlusSymbol: true)) . $symbol;
|
||||
}
|
||||
|
||||
$numberExponent = \floor(\log10($number));
|
||||
$displayExponent = $numberExponent - ($numberExponent % 3);
|
||||
$number /= \pow(10, $displayExponent);
|
||||
$number = \explode('.', (string) $number, 2);
|
||||
|
||||
$remainder = $number[1] ?? 0;
|
||||
$number = $number[0] ?? 0;
|
||||
|
||||
if ($remainder === 0) {
|
||||
$symbol = '';
|
||||
}
|
||||
$abbreviated = self::ABBREVIATE_UNITS[$displayExponent] ?? '';
|
||||
|
||||
return (string) $number . $abbreviated . $symbol;
|
||||
}
|
||||
}
|
@ -2,7 +2,17 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
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 @@ public function rules(): array
|
||||
$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 @@ public function getDto(): StoreUpdate
|
||||
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 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 @@ protected function casts(): array
|
||||
];
|
||||
}
|
||||
|
||||
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 @@
|
||||
{
|
||||
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 @@ public function boot(): void
|
||||
|
||||
$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 @@ public function getUserByEmail(string $email): ?User
|
||||
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 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
}
|
||||
|
||||
$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 @@ public function edit(int $id, User $user): ServiceResultError | ServiceResultArr
|
||||
{
|
||||
$modelUser = $this->userRepository->getUserById($id);
|
||||
|
||||
if (is_null($modelUser)) {
|
||||
if (\is_null($modelUser)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
@ -101,14 +101,14 @@ public function store(StoreUpdate $data, User $user): ServiceResultError | Store
|
||||
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 @@ public function update(int $id, StoreUpdate $data, User $user): ServiceResultErr
|
||||
{
|
||||
$modelUser = $this->userRepository->getUserById($id);
|
||||
|
||||
if (is_null($modelUser)) {
|
||||
if (\is_null($modelUser)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
@ -134,13 +134,6 @@ public function update(int $id, StoreUpdate $data, User $user): ServiceResultErr
|
||||
);
|
||||
}
|
||||
|
||||
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 @@ public function update(int $id, StoreUpdate $data, User $user): ServiceResultErr
|
||||
return $modelUser;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
\report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
@ -162,7 +155,7 @@ public function updatePassword(int $id, UpdatePassword $data, User $user): Servi
|
||||
{
|
||||
$modelUser = $this->userRepository->getUserById($id);
|
||||
|
||||
if (is_null($modelUser)) {
|
||||
if (\is_null($modelUser)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
@ -173,7 +166,7 @@ public function updatePassword(int $id, UpdatePassword $data, User $user): Servi
|
||||
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 @@ public function destroy(int $id, User $user): ServiceResultError | ServiceResult
|
||||
{
|
||||
$modelUser = $this->userRepository->getUserById($id);
|
||||
|
||||
if (is_null($modelUser)) {
|
||||
if (\is_null($modelUser)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
@ -197,7 +190,7 @@ public function destroy(int $id, User $user): ServiceResultError | ServiceResult
|
||||
$this->userCommandHandler->handleDestroy($modelUser);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
\report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
@ -209,7 +202,6 @@ private function getDataUser(StoreUpdate $data): array
|
||||
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();
|
||||
}
|
||||
}
|
114
app/application/app/Services/Site/AccessTokenService.php
Normal file
114
app/application/app/Services/Site/AccessTokenService.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Site;
|
||||
|
||||
use App\Dto\QuerySettingsDto;
|
||||
use App\Dto\Service\Site\AccessTokens\StoreUpdate;
|
||||
use App\Enums\AccessTokenPermission;
|
||||
use App\Helpers\Helpers;
|
||||
use App\Models\User;
|
||||
use App\Repositories\AccessTokenRepository;
|
||||
use App\ServiceResults\ServiceResultArray;
|
||||
use App\ServiceResults\ServiceResultError;
|
||||
use App\ServiceResults\ServiceResultSuccess;
|
||||
use App\ServiceResults\Site\AccessTokenService\StoreTokenResult;
|
||||
use App\ServiceResults\StoreUpdateResult;
|
||||
use App\Services\Service;
|
||||
|
||||
final class AccessTokenService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccessTokenRepository $accessTokenRepository,
|
||||
) { }
|
||||
|
||||
public function index(QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
|
||||
{
|
||||
$tokens = $this->accessTokenRepository->getUserTokens(
|
||||
$user,
|
||||
$querySettingsDto->getQueryWith()
|
||||
)->pagination(
|
||||
$querySettingsDto->getLimit(),
|
||||
$querySettingsDto->getPage()
|
||||
);
|
||||
|
||||
return $this->result([
|
||||
'tokens' => $tokens
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(User $user): ServiceResultError | ServiceResultArray
|
||||
{
|
||||
return $this->result([
|
||||
'user' => $user,
|
||||
'serviceAddress' => Helpers::dockerServiceAddress(),
|
||||
'permissions' => AccessTokenPermission::toCollection()->pluck('title', 'value')->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
|
||||
{
|
||||
$token = $this->accessTokenRepository->getTokenById($user, $id);
|
||||
if (\is_null($token)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
'permissions' => AccessTokenPermission::toCollection()->pluck('title', 'value')->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreTokenResult
|
||||
{
|
||||
try {
|
||||
$token = $user->createToken($data->getName(), [$data->getPermission()->value])->plainTextToken;
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return new StoreTokenResult(
|
||||
name: $data->getName(),
|
||||
permission: $data->getPermission(),
|
||||
token: $token,
|
||||
);
|
||||
}
|
||||
|
||||
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
|
||||
{
|
||||
$token = $this->accessTokenRepository->getTokenById($user, $id);
|
||||
if (\is_null($token)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
try {
|
||||
$token->update([
|
||||
'name' => $data->getName(),
|
||||
'abilities' => [$data->getPermission()->value],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return $this->resultStoreUpdateModel($token, __('site.Token updated successfully'));
|
||||
}
|
||||
|
||||
public function destroy(int $id, User $user): ServiceResultError|ServiceResultSuccess
|
||||
{
|
||||
$token = $this->accessTokenRepository->getTokenById($user, $id);
|
||||
if (\is_null($token)) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
try {
|
||||
$token->delete();
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return $this->ok(__('site.The token has been deleted'));
|
||||
}
|
||||
}
|
210
app/application/app/Services/Site/RepositoryService.php
Normal file
210
app/application/app/Services/Site/RepositoryService.php
Normal file
@ -0,0 +1,210 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Site;
|
||||
|
||||
use App\Dto\Builder\Repository as RepositoryBuilder;
|
||||
use App\Dto\QuerySettingsDto;
|
||||
use App\Dto\Service\Site\Repository\StoreUpdate;
|
||||
use App\Helpers\Helpers;
|
||||
use App\Models\Repository;
|
||||
use App\Models\User;
|
||||
use App\Repositories\RepositoryRepository;
|
||||
use App\Repositories\TagRepositoryRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\ServiceResults\ServiceResultArray;
|
||||
use App\ServiceResults\ServiceResultError;
|
||||
use App\ServiceResults\ServiceResultSuccess;
|
||||
use App\ServiceResults\StoreUpdateResult;
|
||||
use App\Services\Repository\RepositoryCommandHandler;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class RepositoryService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RepositoryRepository $repositoryRepository,
|
||||
private readonly RepositoryCommandHandler $repositoryCommandHandler,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly TagRepositoryRepository $tagRepositoryRepository,
|
||||
) { }
|
||||
|
||||
public function repositories(QuerySettingsDto $querySettingsDto): ServiceResultArray | ServiceResultError
|
||||
{
|
||||
$repositories = $this->repositoryRepository->getRepositories(
|
||||
new RepositoryBuilder(isPublic: true),
|
||||
$querySettingsDto->getQueryWith()
|
||||
)->pagination(
|
||||
$querySettingsDto->getLimit(),
|
||||
$querySettingsDto->getPage()
|
||||
);
|
||||
|
||||
if ($querySettingsDto->getPage() > 1 && $repositories->count() === 0) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'repositories' => $repositories,
|
||||
]);
|
||||
}
|
||||
|
||||
public function repository(string $username, string $repositoryName, ?User $user = null): ServiceResultArray | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$repository = $this->repositoryRepository->getRepositoryByName($pageUser, $repositoryName);
|
||||
if (!$repository || $user === null && $repository->is_public === false) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user?->cannot('view', $repository)) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'repository' => $repository,
|
||||
'pageUser' => $pageUser,
|
||||
'serviceAddress' => Helpers::dockerServiceAddress(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(string $username, User $user): ServiceResultArray | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user->cannot('create', [Repository::class, $pageUser])) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'user' => $user,
|
||||
'repository' => new Repository(),
|
||||
'pageUser' => $pageUser,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(string $username, string $name, User $user): ServiceResultArray | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$repository = $this->repositoryRepository->getRepositoryByName($pageUser, $name);
|
||||
if (!$repository) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user->cannot('update', $repository)) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'user' => $user,
|
||||
'repository' => $repository,
|
||||
'pageUser' => $pageUser,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(string $username, StoreUpdate $data, User $user): StoreUpdateResult | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user->cannot('create', [Repository::class, $pageUser])) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
if ($this->repositoryRepository->isExistsName($pageUser, $data->getName())) {
|
||||
return $this->errValidate(
|
||||
__('validation.unique', ['attribute' => __('validation.attributes.name')]),
|
||||
['name' => __('validation.unique', ['attribute' => __('validation.attributes.name')])]
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$modelRepository = DB::transaction(function () use ($data, $pageUser) {
|
||||
$dataRepository = $this->getDataRepository($data);
|
||||
return $this->repositoryCommandHandler->handleStore($pageUser, $dataRepository, $data->getName());
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
\report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return $this->resultStoreUpdateModel($modelRepository, __('site.The repository has been successfully created'));
|
||||
}
|
||||
|
||||
public function update(string $username, string $name, StoreUpdate $data, User $user): StoreUpdateResult | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$repository = $this->repositoryRepository->getRepositoryByName($pageUser, $name);
|
||||
if (!$repository) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user->cannot('update', $repository)) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
try {
|
||||
$modelRepository = DB::transaction(function () use ($data, $repository) {
|
||||
$dataRepository = $this->getDataRepository($data);
|
||||
return $this->repositoryCommandHandler->handleUpdate($repository, $dataRepository);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
\report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return $this->resultStoreUpdateModel($modelRepository, __('site.The repository has been successfully updated'));
|
||||
}
|
||||
|
||||
public function destroy(string $username, string $name, User $user): ServiceResultError | ServiceResultSuccess
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$repository = $this->repositoryRepository->getRepositoryByName($pageUser, $name);
|
||||
if (!$repository) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user->cannot('delete', $repository)) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($repository) {
|
||||
$this->repositoryCommandHandler->handleDestroy($repository);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
\report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return $this->ok(__('site.Repository deleted successfully'));
|
||||
}
|
||||
|
||||
private function getDataRepository(StoreUpdate $data): array
|
||||
{
|
||||
return [
|
||||
'is_public' => $data->isPublic(),
|
||||
'description' => $data->getDescription(),
|
||||
'overview' => $data->getOverview(),
|
||||
];
|
||||
}
|
||||
}
|
95
app/application/app/Services/Site/TagRepositoryService.php
Normal file
95
app/application/app/Services/Site/TagRepositoryService.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Site;
|
||||
|
||||
use App\Dto\QuerySettingsDto;
|
||||
use App\Helpers\Helpers;
|
||||
use App\Models\User;
|
||||
use App\Repositories\RepositoryRepository;
|
||||
use App\Repositories\TagRepositoryRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\ServiceResults\ServiceResultArray;
|
||||
use App\ServiceResults\ServiceResultError;
|
||||
use App\ServiceResults\ServiceResultSuccess;
|
||||
use App\Services\Service;
|
||||
use App\Services\TagRepository\TagRepositoryCommandHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class TagRepositoryService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RepositoryRepository $repositoryRepository,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly TagRepositoryRepository $tagRepositoryRepository,
|
||||
private readonly TagRepositoryCommandHandler $tagRepositoryCommandHandler,
|
||||
) { }
|
||||
|
||||
public function tags(string $username, string $repositoryName, QuerySettingsDto $querySettingsDto, ?User $user = null): ServiceResultArray | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$repository = $this->repositoryRepository->getRepositoryByName($pageUser, $repositoryName);
|
||||
if (!$repository || $user === null && $repository->is_public === false) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user?->cannot('view', $repository)) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
$tags = $this->tagRepositoryRepository->getTagsRepository(
|
||||
$repository,
|
||||
$querySettingsDto->getQueryWith()
|
||||
)->pagination(
|
||||
$querySettingsDto->getLimit(),
|
||||
$querySettingsDto->getPage()
|
||||
);
|
||||
|
||||
if ($querySettingsDto->getPage() > 1 && $tags->count() === 0) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'tags' => $tags,
|
||||
'repository' => $repository,
|
||||
'pageUser' => $pageUser,
|
||||
'serviceAddress' => Helpers::dockerServiceAddress(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(string $username, string $repositoryName, int $tagId, User $user): ServiceResultError | ServiceResultSuccess
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$repository = $this->repositoryRepository->getRepositoryByName($pageUser, $repositoryName);
|
||||
if (!$repository) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$tag = $this->tagRepositoryRepository->getTagById($tagId);
|
||||
if (!$tag || $tag->repository_id !== $repository->id) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
if ($user->cannot('delete', $tag)) {
|
||||
return $this->errFobidden(__('Access is denied'));
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($tag) {
|
||||
$this->tagRepositoryCommandHandler->handleDestroy($tag);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
\report($e);
|
||||
return $this->errService(__('Server Error'));
|
||||
}
|
||||
|
||||
return $this->ok(__('site.Tag removed successfully'));
|
||||
}
|
||||
}
|
51
app/application/app/Services/Site/UserService.php
Normal file
51
app/application/app/Services/Site/UserService.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Site;
|
||||
|
||||
use App\Dto\Builder\Repository as RepositoryBuilder;
|
||||
use App\Dto\QuerySettingsDto;
|
||||
use App\Models\User;
|
||||
use App\Repositories\RepositoryRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\ServiceResults\ServiceResultArray;
|
||||
use App\ServiceResults\ServiceResultError;
|
||||
use App\Services\Service;
|
||||
|
||||
final class UserService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly RepositoryRepository $repositoryRepository,
|
||||
) { }
|
||||
|
||||
public function repositories(string $username, QuerySettingsDto $querySettingsDto, ?User $user = null): ServiceResultArray | ServiceResultError
|
||||
{
|
||||
$pageUser = $this->userRepository->getUserByUsername($username);
|
||||
if ($pageUser === null) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
$isPublic = true;
|
||||
if ($user?->id === $pageUser->id) {
|
||||
$isPublic = null;
|
||||
}
|
||||
|
||||
$repositories = $this->repositoryRepository->getUserRepositories(
|
||||
$pageUser,
|
||||
new RepositoryBuilder(isPublic: $isPublic),
|
||||
$querySettingsDto->getQueryWith()
|
||||
)->pagination(
|
||||
$querySettingsDto->getLimit(),
|
||||
$querySettingsDto->getPage()
|
||||
);
|
||||
|
||||
if ($querySettingsDto->getPage() > 1 && $repositories->count() === 0) {
|
||||
return $this->errNotFound(__('Not Found'));
|
||||
}
|
||||
|
||||
return $this->result([
|
||||
'pageUser' => $pageUser,
|
||||
'repositories' => $repositories,
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\TagRepository;
|
||||
|
||||
use App\Models\Repository;
|
||||
use App\Models\TagRepository;
|
||||
use App\ServiceResults\ServiceResultError;
|
||||
use App\ServiceResults\TagRepository\IncrementPullResult;
|
||||
use App\Services\Service;
|
||||
|
||||
final class IncrementPullCommand extends Service
|
||||
{
|
||||
public function execute(Repository $repository, string $tagName, string $digest): ServiceResultError | IncrementPullResult
|
||||
{
|
||||
$tag = $repository->tags()
|
||||
->withTrashed()
|
||||
->where('name', $tagName)
|
||||
->where('digest', $digest)
|
||||
->first();
|
||||
|
||||
if (null === $tag) {
|
||||
return $this->errNotFound(__('Not Found') . ' Tag: ' . $tagName . ':' . $digest);
|
||||
}
|
||||
|
||||
Repository::withTrashed()->where('id', $repository->id)
|
||||
// So that the updated_at field is not updated
|
||||
->where(fn($q) => $q->getModel()->timestamps = false)
|
||||
->increment('quantity_pulls');
|
||||
|
||||
TagRepository::withTrashed()->where('id', $tag->id)
|
||||
// So that the updated_at field is not updated
|
||||
->where(fn($q) => $q->getModel()->timestamps = false)
|
||||
->increment('quantity_pulls');
|
||||
|
||||
return new IncrementPullResult($repository, $tag);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\TagRepository;
|
||||
|
||||
use App\Models\Architecture;
|
||||
use App\Models\Repository;
|
||||
use App\Models\TagRepository;
|
||||
|
||||
final readonly class TagRepositoryCommandHandler
|
||||
{
|
||||
public function handleStore(Repository $repository, Architecture $architecture, string $name, array $data): TagRepository
|
||||
{
|
||||
$tag = new TagRepository();
|
||||
$tag->repository_id = $repository->id;
|
||||
$tag->architecture_id = $architecture->id;
|
||||
$tag->name = $name;
|
||||
|
||||
$tag->fill($data);
|
||||
$tag->save();
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public function handleUpdate(TagRepository $tag, array $data): TagRepository
|
||||
{
|
||||
$tag->update($data);
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public function handleDestroy(TagRepository $tag): void
|
||||
{
|
||||
$tag->delete();
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Services\TagRepository;
|
||||
|
||||
use App\Models\Architecture;
|
||||
use App\Models\Repository;
|
||||
use App\Models\TagRepository;
|
||||
|
||||
final readonly class UpdateOrCreateCommand
|
||||
{
|
||||
public function __construct(
|
||||
private TagRepositoryCommandHandler $tagRepositoryCommandHandler,
|
||||
) { }
|
||||
|
||||
public function execute(Repository $repository, Architecture $architecture, string $name, array $data): TagRepository
|
||||
{
|
||||
$tag = TagRepository::withTrashed()
|
||||
->where('repository_id', $repository->id)
|
||||
->where('architecture_id', $architecture->id)
|
||||
->where('name', $name)
|
||||
->first();
|
||||
|
||||
if (null === $tag) {
|
||||
return $this->tagRepositoryCommandHandler->handleStore($repository, $architecture, $name, $data);
|
||||
}
|
||||
|
||||
return $this->tagRepositoryCommandHandler->handleUpdate($tag, $data);
|
||||
}
|
||||
}
|
@ -28,7 +28,8 @@ public function handleUpdate(User $user, array $data): User
|
||||
}
|
||||
|
||||
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 @@ public function __construct(
|
||||
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 @@ private function getValue(): string
|
||||
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 @@ public function render(): View
|
||||
'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\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 @@
|
||||
->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
Loading…
Reference in New Issue
Block a user