Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/Domains/Activity/Contracts/Enums/ActivityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum ActivityType: string
case MemberRoleChanged = 'member.role_changed';
case InvitationSent = 'invitation.sent';
case TokenCreated = 'token.created';
case TokenUpdated = 'token.updated';
case TokenRevoked = 'token.revoked';
case SshKeyGenerated = 'ssh_key.generated';
case SshKeyDeleted = 'ssh_key.deleted';
Expand All @@ -41,6 +42,7 @@ public function label(): string
self::MemberRoleChanged => 'Role changed',
self::InvitationSent => 'Invitation sent',
self::TokenCreated => 'Token created',
self::TokenUpdated => 'Token updated',
self::TokenRevoked => 'Token revoked',
self::SshKeyGenerated => 'SSH key generated',
self::SshKeyDeleted => 'SSH key deleted',
Expand All @@ -66,6 +68,7 @@ public function icon(): string
self::MemberRoleChanged => 'shield',
self::InvitationSent => 'mail',
self::TokenCreated => 'key-round',
self::TokenUpdated => 'key-round',
self::TokenRevoked => 'key-round',
self::SshKeyGenerated => 'key-round',
self::SshKeyDeleted => 'key-round',
Expand All @@ -83,7 +86,7 @@ public function category(): string
self::RepositoryAdded, self::RepositoryRemoved, self::RepositorySynced, self::RepositorySyncFailed => 'repository',
self::PackageCreated, self::PackageRemoved => 'package',
self::MemberAdded, self::MemberRemoved, self::MemberRoleChanged, self::InvitationSent => 'member',
self::TokenCreated, self::TokenRevoked => 'token',
self::TokenCreated, self::TokenUpdated, self::TokenRevoked => 'token',
self::SshKeyGenerated, self::SshKeyDeleted => 'settings',
self::MirrorAdded, self::MirrorRemoved, self::MirrorSynced, self::MirrorSyncFailed => 'mirror',
self::VulnerabilitiesDetected => 'security',
Expand Down
74 changes: 74 additions & 0 deletions app/Domains/Repository/Actions/CreateRepositoryAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Domains\Repository\Actions;

use App\Domains\Activity\Actions\RecordActivityTask;
use App\Domains\Activity\Contracts\Enums\ActivityType;
use App\Domains\Repository\Contracts\Enums\GitProvider;
use App\Domains\Repository\Jobs\SyncRepositoryJob;
use App\Models\Organization;
use App\Models\Repository;
use App\Models\User;
use App\Models\UserGitCredential;

class CreateRepositoryAction
{
public function __construct(
protected ExtractRepositoryNameAction $extractRepositoryName,
protected RegisterWebhookAction $registerWebhook,
protected RecordActivityTask $recordActivity,
) {}

public function handle(
Organization $organization,
GitProvider $provider,
string $repoIdentifier,
User $user,
?string $name = null,
?string $defaultBranch = null,
?string $sshKeyUuid = null,
): Repository {
$name ??= $this->extractRepositoryName->handle($repoIdentifier, $provider);

$isGenericGit = $provider === GitProvider::Git;

$repository = Repository::create([
'organization_uuid' => $organization->uuid,
'credential_user_uuid' => $isGenericGit ? null : $user->uuid,
'ssh_key_uuid' => $isGenericGit ? $sshKeyUuid : null,
'name' => $name,
'provider' => $provider,
'repo_identifier' => $repoIdentifier,
'custom_base_url' => $this->resolveBaseUrl($provider, $user->uuid),
'default_branch' => $defaultBranch,
]);

$this->recordActivity->handle(
organization: $organization,
type: ActivityType::RepositoryAdded,
subject: $repository,
actor: $user,
properties: ['name' => $repository->name, 'provider' => $repository->provider->value],
);

SyncRepositoryJob::dispatch($repository);

$this->registerWebhook->handle($repository);

return $repository;
}

private function resolveBaseUrl(GitProvider $provider, string $userUuid): ?string
{
if (! $provider->supportsSelfHosted()) {
return null;
}

$credential = UserGitCredential::query()
->where('user_uuid', $userUuid)
->where('provider', $provider)
->first();

return $credential?->credentials['url'] ?? null;
}
}
63 changes: 10 additions & 53 deletions app/Domains/Repository/Http/Controllers/RepositoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,17 @@
use App\Domains\Organization\Contracts\Data\OrganizationSshKeyData;
use App\Domains\Package\Contracts\Data\PackageData;
use App\Domains\Repository\Actions\BulkCreateRepositoriesAction;
use App\Domains\Repository\Actions\CreateRepositoryAction;
use App\Domains\Repository\Actions\DeleteWebhookAction;
use App\Domains\Repository\Actions\ExtractRepositoryNameAction;
use App\Domains\Repository\Actions\RegisterWebhookAction;
use App\Domains\Repository\Contracts\Data\RepositoryData;
use App\Domains\Repository\Contracts\Data\SyncLogData;
use App\Domains\Repository\Contracts\Enums\GitProvider;
use App\Domains\Repository\Http\Requests\BulkStoreRepositoryRequest;
use App\Domains\Repository\Http\Requests\StoreRepositoryRequest;
use App\Domains\Repository\Jobs\SyncRepositoryJob;
use App\Http\Controllers\Controller;
use App\Models\Organization;
use App\Models\Repository;
use App\Models\User;
use App\Models\UserGitCredential;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
Expand All @@ -33,8 +30,6 @@ class RepositoryController extends Controller
use AuthorizesRequests;

public function __construct(
protected ExtractRepositoryNameAction $extractRepositoryNameAction,
protected RegisterWebhookAction $registerWebhookAction,
protected DeleteWebhookAction $deleteWebhookAction,
protected RecordActivityTask $recordActivity,
) {}
Expand Down Expand Up @@ -70,49 +65,25 @@ public function index(Organization $organization): Response
]);
}

public function store(StoreRepositoryRequest $request, Organization $organization): RedirectResponse
public function store(StoreRepositoryRequest $request, Organization $organization, CreateRepositoryAction $createRepository): RedirectResponse
{
$this->authorize('deleteRepository', $organization);

$name = $request->name ?? $this->extractRepositoryNameAction->handle(
$request->repo_identifier,
GitProvider::from($request->provider)
);

$provider = GitProvider::from($request->provider);

/** @var User $user */
$user = auth()->user();

$baseUrl = $this->resolveBaseUrl($provider, $user->uuid);

$isGenericGit = $provider === GitProvider::Git;

$repository = Repository::create([
'organization_uuid' => $organization->uuid,
'credential_user_uuid' => $isGenericGit ? null : $user->uuid,
'ssh_key_uuid' => $isGenericGit ? $request->ssh_key_uuid : null,
'name' => $name,
'provider' => $provider,
'repo_identifier' => $request->repo_identifier,
'custom_base_url' => $baseUrl,
'default_branch' => $request->default_branch,
]);

$this->recordActivity->handle(
$repository = $createRepository->handle(
organization: $organization,
type: ActivityType::RepositoryAdded,
subject: $repository,
actor: $request->user(),
properties: ['name' => $repository->name, 'provider' => $repository->provider->value],
provider: GitProvider::from($request->provider),
repoIdentifier: $request->repo_identifier,
user: $user,
name: $request->name,
defaultBranch: $request->default_branch,
sshKeyUuid: $request->ssh_key_uuid,
);

SyncRepositoryJob::dispatch($repository);

$webhookRegistered = $this->registerWebhookAction->handle($repository);

$message = 'Repository added successfully.';
if (! $webhookRegistered && $repository->provider->supportsAutomaticWebhooks()) {
if ($repository->webhook_id === null && $repository->provider->supportsAutomaticWebhooks()) {
$message .= ' Webhook registration failed — you can retry from the repository page.';
}

Expand Down Expand Up @@ -223,18 +194,4 @@ public function destroy(Request $request, Organization $organization, Repository
->route('organizations.repositories.index', $organization)
->with('status', 'Repository deleted successfully.');
}

private function resolveBaseUrl(GitProvider $provider, string $userUuid): ?string
{
if (! $provider->supportsSelfHosted()) {
return null;
}

$credential = UserGitCredential::query()
->where('user_uuid', $userUuid)
->where('provider', $provider)
->first();

return $credential?->credentials['url'] ?? null;
}
}
9 changes: 8 additions & 1 deletion app/Domains/Token/Actions/CreateAccessTokenAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Domains\Activity\Actions\RecordActivityTask;
use App\Domains\Activity\Contracts\Enums\ActivityType;
use App\Domains\Token\Contracts\Data\TokenCreatedData;
use App\Domains\Token\Contracts\Enums\TokenScope;
use App\Models\AccessToken;
use App\Models\Organization;
use App\Models\User;
Expand All @@ -17,11 +18,15 @@ public function __construct(
protected RecordActivityTask $recordActivity,
) {}

/**
* @param array<int, TokenScope|string>|null $scopes Null grants full (legacy) access.
*/
public function handle(
?Organization $organization,
?User $user,
string $name,
?Carbon $expiresAt = null
?Carbon $expiresAt = null,
?array $scopes = null,
): TokenCreatedData {
$plainToken = Str::random(64);
$tokenHash = hash('sha256', $plainToken);
Expand All @@ -32,6 +37,7 @@ public function handle(
'name' => $name,
'token_hash' => $tokenHash,
'expires_at' => $expiresAt,
'scopes' => $scopes !== null ? TokenScope::normalize($scopes) : null,
]);

if ($organization) {
Expand All @@ -49,6 +55,7 @@ public function handle(
name: $name,
expiresAt: $accessToken->expires_at,
organizationUuid: $accessToken->organization_uuid,
scopes: $accessToken->scopes ?? [],
);
}
}
45 changes: 45 additions & 0 deletions app/Domains/Token/Actions/UpdateAccessTokenAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Domains\Token\Actions;

use App\Domains\Activity\Actions\RecordActivityTask;
use App\Domains\Activity\Contracts\Enums\ActivityType;
use App\Domains\Token\Contracts\Enums\TokenScope;
use App\Models\AccessToken;
use App\Models\User;

class UpdateAccessTokenAction
{
public function __construct(
protected RecordActivityTask $recordActivity,
) {}

/**
* Update a token's name and (optionally) its scopes. A null $scopes leaves
* the existing scopes untouched; an array replaces them.
*
* @param array<int, TokenScope|string>|null $scopes
*/
public function handle(AccessToken $accessToken, string $name, ?array $scopes = null, ?User $actor = null): AccessToken
{
$attributes = ['name' => $name];

if ($scopes !== null) {
$attributes['scopes'] = TokenScope::normalize($scopes);
}

$accessToken->update($attributes);

if ($accessToken->organization) {
$this->recordActivity->handle(
organization: $accessToken->organization,
type: ActivityType::TokenUpdated,
subject: $accessToken,
actor: $actor ?? auth()->user(),
properties: ['name' => $accessToken->name],
);
}

return $accessToken;
}
}
5 changes: 5 additions & 0 deletions app/Domains/Token/Contracts/Data/AccessTokenData.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
#[TypeScript]
class AccessTokenData extends Data
{
/**
* @param array<int, string> $scopes
*/
public function __construct(
public string $uuid,
public string $name,
public ?CarbonInterface $lastUsedAt,
public ?CarbonInterface $expiresAt,
public CarbonInterface $createdAt,
public array $scopes = [],
) {}

public static function fromModel(AccessToken $token): self
Expand All @@ -32,6 +36,7 @@ public static function fromModel(AccessToken $token): self
lastUsedAt: $token->last_used_at,
expiresAt: $token->expires_at,
createdAt: $createdAt,
scopes: $token->scopes ?? [],
);
}
}
4 changes: 4 additions & 0 deletions app/Domains/Token/Contracts/Data/TokenCreatedData.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
#[TypeScript]
class TokenCreatedData extends Data
{
/**
* @param array<int, string> $scopes
*/
public function __construct(
public string $plainToken,
public string $name,
public ?CarbonInterface $expiresAt,
public ?string $organizationUuid,
public array $scopes = [],
) {}
}
Loading