mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(api): add Gitea App endpoints for sources and applications
Add GET /gitea-apps, POST /applications/private-gitea-app, and
gitea_app_uuid in PATCH /applications/{uuid}, with OpenAPI docs and MCP tool support.
Closes #12459
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4bdf03db69
commit
3bc82eb71d
@@ -15,6 +15,7 @@ use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GiteaApp;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
@@ -36,6 +37,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
@@ -789,6 +791,218 @@ class ApplicationsController extends Controller
|
||||
return $this->create_application($request, 'private-gh-app');
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create (Private - Gitea App)',
|
||||
description: 'Create new application based on a private repository through a Gitea App.',
|
||||
path: '/applications/private-gitea-app',
|
||||
operationId: 'create-private-gitea-app-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Application object that needs to be created.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['project_uuid', 'server_uuid', 'gitea_app_uuid', 'git_repository', 'git_branch', 'build_pack'],
|
||||
properties: [
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'gitea_app_uuid' => ['type' => 'string', 'description' => 'The Gitea App UUID. The source must be connected.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'The repository as owner/name, or its URL on the Gitea instance.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
|
||||
'noindex_domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'],
|
||||
'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
|
||||
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
|
||||
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
|
||||
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
|
||||
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
||||
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
||||
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
||||
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
||||
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
|
||||
'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
|
||||
'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
|
||||
'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'],
|
||||
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
|
||||
'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
|
||||
'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
|
||||
'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
|
||||
'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
|
||||
'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
|
||||
'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
|
||||
'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
|
||||
'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
|
||||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
|
||||
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
|
||||
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
||||
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
||||
'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
|
||||
'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
|
||||
'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
|
||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
|
||||
'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
|
||||
'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
|
||||
'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
|
||||
'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
|
||||
'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
|
||||
'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
|
||||
'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github. Needs the deploy ability.'],
|
||||
'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab. Needs the deploy ability.'],
|
||||
'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket. Needs the deploy ability.'],
|
||||
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea. Needs the deploy ability.'],
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
|
||||
'docker_compose_domains' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Array of URLs to be applied to containers of a dockercompose application.',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
|
||||
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")'],
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'Per-service www/non-www redirect for this compose service.', 'enum' => ['www', 'non-www', 'both']],
|
||||
],
|
||||
),
|
||||
],
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'Watch path patterns, one per line (newline-separated). Commas do not separate patterns.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
|
||||
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
|
||||
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
|
||||
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
|
||||
'is_auto_cancel_queued_deployments_enabled' => ['type' => 'boolean', 'description' => 'Cancel older queued webhook deployments when a newer push arrives. Default: true.'],
|
||||
'is_auto_cancel_building_deployments_enabled' => ['type' => 'boolean', 'description' => 'Also stop an older webhook deployment that is still building when a newer push arrives. A deployment that already started to replace containers always finishes. This also cancels queued webhook deployments. Default: false.'],
|
||||
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
|
||||
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'rolling_update_drain_seconds' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 300, 'description' => 'Seconds to wait after the new container is healthy before old containers stop during a rolling update. 0 turns the wait off. Default: 5.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
'gpu_driver' => ['type' => 'string', 'nullable' => true, 'description' => 'GPU driver name.'],
|
||||
'gpu_count' => ['type' => 'string', 'nullable' => true, 'description' => 'Number of GPUs to allocate.'],
|
||||
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
|
||||
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
|
||||
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
|
||||
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
|
||||
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
|
||||
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
|
||||
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
|
||||
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
|
||||
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
|
||||
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
||||
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Application created successfully.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 403,
|
||||
description: 'The token needs the deploy ability to set a manual webhook secret.',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Domain conflicts detected.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'],
|
||||
'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'],
|
||||
'conflicts' => [
|
||||
'type' => 'array',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'domain' => ['type' => 'string', 'example' => 'example.com'],
|
||||
'resource_name' => ['type' => 'string', 'example' => 'My Application'],
|
||||
'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'],
|
||||
'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'],
|
||||
'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_private_gitea_app_application(Request $request)
|
||||
{
|
||||
return $this->create_application($request, 'private-gitea-app');
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create (Private - Deploy Key)',
|
||||
description: 'Create new application based on a private repository through a Deploy Key.',
|
||||
@@ -1396,7 +1610,7 @@ class ApplicationsController extends Controller
|
||||
'autogenerate_domain' => 'boolean',
|
||||
'tags' => 'array|nullable',
|
||||
'tags.*' => 'string|min:2',
|
||||
]);
|
||||
], ValidationPatterns::combinedMessages());
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -1458,7 +1672,6 @@ class ApplicationsController extends Controller
|
||||
$autogenerateDomain = $request->boolean('autogenerate_domain', true) && $request->build_pack !== 'dockercompose';
|
||||
$forceDomainOverride = $request->boolean('force_domain_override');
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
$githubAppUuid = $request->github_app_uuid;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
$useBuildSecrets = $request->use_build_secrets;
|
||||
$isStatic = $request->is_static;
|
||||
@@ -1472,7 +1685,7 @@ class ApplicationsController extends Controller
|
||||
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled', false);
|
||||
$applicationSettings = $this->applicationSettingsFromRequest($request);
|
||||
|
||||
$requestedBuildPack = in_array($type, ['public', 'private-gh-app', 'private-deploy-key'], true)
|
||||
$requestedBuildPack = in_array($type, ['public', 'private-gh-app', 'private-gitea-app', 'private-deploy-key'], true)
|
||||
? $request->input('build_pack')
|
||||
: $type;
|
||||
if (($applicationSettings['is_raw_compose_deployment_enabled'] ?? false) && $requestedBuildPack !== 'dockercompose') {
|
||||
@@ -1804,13 +2017,14 @@ class ApplicationsController extends Controller
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
]))->setStatusCode(201);
|
||||
} elseif ($type === 'private-gh-app') {
|
||||
} elseif (in_array($type, ['private-gh-app', 'private-gitea-app'], true)) {
|
||||
$sourceUuidField = $type === 'private-gh-app' ? 'github_app_uuid' : 'gitea_app_uuid';
|
||||
$validationRules = [
|
||||
'git_repository' => 'string|required',
|
||||
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
|
||||
'github_app_uuid' => 'string|required',
|
||||
$sourceUuidField => 'string|required',
|
||||
'watch_paths' => 'string|nullable',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_domains.*' => 'array:name,domain,redirect',
|
||||
@@ -1851,39 +2065,13 @@ class ApplicationsController extends Controller
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $githubAppUuid)
|
||||
->where(function ($query) use ($teamId) {
|
||||
$query->where('team_id', $teamId)
|
||||
->orWhere('is_system_wide', true);
|
||||
})
|
||||
->first();
|
||||
if (! $githubApp) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
$sourceRepository = $type === 'private-gh-app'
|
||||
? $this->githubAppRepository($request->github_app_uuid, $request->git_repository, $teamId)
|
||||
: $this->giteaAppRepository($this->teamSourceByUuid(GiteaApp::class, $request->gitea_app_uuid, $teamId), $request->git_repository);
|
||||
if ($sourceRepository instanceof JsonResponse) {
|
||||
return $sourceRepository;
|
||||
}
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
|
||||
}
|
||||
|
||||
$gitRepository = gitRepositorySlug($request->git_repository);
|
||||
|
||||
// Use direct API call to verify repository access instead of loading all repositories
|
||||
// This is much faster and avoids timeouts for GitHub Apps with many repositories
|
||||
$response = Http::GitHub($githubApp->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get("/repos/{$gitRepository}");
|
||||
|
||||
if ($response->status() === 404 || $response->status() === 403) {
|
||||
return response()->json(['message' => 'Repository not found or not accessible by the GitHub App.'], 404);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['message' => 'Failed to verify repository access: '.($response->json()['message'] ?? 'Unknown error')], 400);
|
||||
}
|
||||
|
||||
$gitRepositoryFound = $response->json();
|
||||
$repository_project_id = data_get($gitRepositoryFound, 'id');
|
||||
['source' => $source, 'repository' => $gitRepository, 'repository_project_id' => $repository_project_id] = $sourceRepository;
|
||||
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
@@ -1970,8 +2158,8 @@ class ApplicationsController extends Controller
|
||||
$application->destination_id = $destination->id;
|
||||
$application->destination_type = $destination->getMorphClass();
|
||||
$application->environment_id = $environment->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->source_type = $source->getMorphClass();
|
||||
$application->source_id = $source->id;
|
||||
$application->repository_project_id = $repository_project_id;
|
||||
|
||||
if ($imageNameValidation = $this->sharedImageNameValidationResponse($application)) {
|
||||
@@ -3230,6 +3418,7 @@ class ApplicationsController extends Controller
|
||||
type: 'object',
|
||||
properties: [
|
||||
'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
|
||||
'gitea_app_uuid' => ['type' => 'string', 'description' => 'The Gitea App UUID. The source must be connected. Do not send it together with github_app_uuid.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'git_https_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTPS access to a private repository on a custom Git host. Send null to remove it.'],
|
||||
@@ -3471,6 +3660,7 @@ class ApplicationsController extends Controller
|
||||
];
|
||||
$validationRules = array_merge(sharedDataApplications($application), $validationRules);
|
||||
$validationMessages = [
|
||||
...ValidationPatterns::combinedMessages(),
|
||||
'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only name, domain, and redirect fields are supported.',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
|
||||
@@ -3551,39 +3741,36 @@ class ApplicationsController extends Controller
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
$githubApp = null;
|
||||
if ($request->has('github_app_uuid')) {
|
||||
$newSource = null;
|
||||
if ($request->has('github_app_uuid') && $request->has('gitea_app_uuid')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['gitea_app_uuid' => 'Send only one of github_app_uuid or gitea_app_uuid.'],
|
||||
], 422);
|
||||
}
|
||||
foreach (['github_app_uuid' => GithubApp::class, 'gitea_app_uuid' => GiteaApp::class] as $sourceUuidField => $sourceClass) {
|
||||
if (! $request->has($sourceUuidField)) {
|
||||
continue;
|
||||
}
|
||||
if ($application->build_pack === 'dockerimage' || filled($application->dockerfile) || $application->deploymentType() === 'deploy_key') {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'github_app_uuid' => 'The github_app_uuid can only be changed for applications deployed from a GitHub App or public repository.',
|
||||
$sourceUuidField => "The {$sourceUuidField} can only be changed for applications deployed from a Git source or public repository.",
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $request->github_app_uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
if (! $githubApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
$newSource = $this->teamSourceByUuid($sourceClass, $request->input($sourceUuidField), $teamId);
|
||||
if (! $newSource?->isConnected()) {
|
||||
return response()->json(['message' => $sourceClass === GithubApp::class ? 'Github App not found.' : 'Gitea App not found.'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$githubApp = null;
|
||||
if ($request->has('github_app_uuid')) {
|
||||
if ($application->build_pack === 'dockerimage' || filled($application->dockerfile) || $application->deploymentType() === 'deploy_key') {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'github_app_uuid' => 'The github_app_uuid can only be changed for applications deployed from a GitHub App or public repository.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $request->github_app_uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
if (! $githubApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
$giteaRepository = null;
|
||||
if ($newSource instanceof GiteaApp) {
|
||||
// Gitea clones and webhooks need owner/name and the repository ID of the new source.
|
||||
$giteaRepository = $this->giteaAppRepository($newSource, (string) $request->input('git_repository', $application->git_repository));
|
||||
if ($giteaRepository instanceof JsonResponse) {
|
||||
return $giteaRepository;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3871,12 +4058,16 @@ class ApplicationsController extends Controller
|
||||
$application->git_https_username = null;
|
||||
}
|
||||
}
|
||||
if ($githubApp && ($application->source_id !== $githubApp->id || $application->source_type !== $githubApp->getMorphClass())) {
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
// The next deployment fetches the repository ID from the new source.
|
||||
if ($newSource && ($application->source_id !== $newSource->id || $application->source_type !== $newSource->getMorphClass())) {
|
||||
$application->source_id = $newSource->id;
|
||||
$application->source_type = $newSource->getMorphClass();
|
||||
// The next deployment fetches the repository ID from a GitHub App.
|
||||
$application->repository_project_id = null;
|
||||
}
|
||||
if ($giteaRepository) {
|
||||
$application->git_repository = $giteaRepository['repository'];
|
||||
$application->repository_project_id = $giteaRepository['repository_project_id'];
|
||||
}
|
||||
if ($requestHasNoindexDomains) {
|
||||
// Must run after fqdn is filled: flags are kept only for domains the app still has.
|
||||
$application->setNoindexDomains($request->input('noindex_domains') ?? []);
|
||||
@@ -5092,6 +5283,120 @@ class ApplicationsController extends Controller
|
||||
return migrateResourceToDestination($request, $application, 'Application', $teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Git source of the team, or a system-wide one, by UUID.
|
||||
*
|
||||
* @template TSource of GithubApp|GiteaApp
|
||||
*
|
||||
* @param class-string<TSource> $sourceClass
|
||||
* @return TSource|null
|
||||
*/
|
||||
private function teamSourceByUuid(string $sourceClass, string $uuid, int|string $teamId): GithubApp|GiteaApp|null
|
||||
{
|
||||
return $sourceClass::where('uuid', $uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a GitHub App of the team and check that it can read the repository.
|
||||
*
|
||||
* @return array{source: GithubApp, repository: string, repository_project_id: mixed}|JsonResponse
|
||||
*/
|
||||
private function githubAppRepository(string $githubAppUuid, string $gitRepository, int|string $teamId): array|JsonResponse
|
||||
{
|
||||
$githubApp = $this->teamSourceByUuid(GithubApp::class, $githubAppUuid, $teamId);
|
||||
if (! $githubApp) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
}
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
|
||||
}
|
||||
|
||||
$gitRepository = gitRepositorySlug($gitRepository);
|
||||
|
||||
// Use direct API call to verify repository access instead of loading all repositories
|
||||
// This is much faster and avoids timeouts for GitHub Apps with many repositories
|
||||
$response = Http::GitHub($githubApp->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get("/repos/{$gitRepository}");
|
||||
|
||||
if ($response->status() === 404 || $response->status() === 403) {
|
||||
return response()->json(['message' => 'Repository not found or not accessible by the GitHub App.'], 404);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['message' => 'Failed to verify repository access: '.($response->json()['message'] ?? 'Unknown error')], 400);
|
||||
}
|
||||
|
||||
return ['source' => $githubApp, 'repository' => $gitRepository, 'repository_project_id' => data_get($response->json(), 'id')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a connected Gitea App can read the repository.
|
||||
*
|
||||
* @return array{source: GiteaApp, repository: string, repository_project_id: mixed}|JsonResponse
|
||||
*/
|
||||
private function giteaAppRepository(?GiteaApp $giteaApp, string $gitRepository): array|JsonResponse
|
||||
{
|
||||
if (! $giteaApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Gitea App not found.'], 404);
|
||||
}
|
||||
|
||||
$repositoryPath = $this->giteaRepositoryPath($giteaApp, $gitRepository);
|
||||
if ($repositoryPath === null) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['git_repository' => 'The git_repository must be owner/name or a repository URL on the Gitea instance of the source.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
refreshGiteaToken($giteaApp);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
}
|
||||
|
||||
$response = Http::Gitea($giteaApp->apiUrlBase(), $giteaApp->access_token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get('/repos/'.collect(explode('/', $repositoryPath))->map(fn (string $part) => rawurlencode($part))->implode('/'));
|
||||
|
||||
if ($response->status() === 404 || $response->status() === 403
|
||||
|| ($response->successful() && ! $giteaApp->allowsRepositoryOwner(data_get($response->json(), 'owner.login')))) {
|
||||
return response()->json(['message' => 'Repository not found or not accessible by the Gitea App.'], 404);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['message' => 'Failed to verify repository access: '.data_get($response->json(), 'message', 'Unknown error')], 400);
|
||||
}
|
||||
|
||||
return ['source' => $giteaApp, 'repository' => $repositoryPath, 'repository_project_id' => data_get($response->json(), 'id')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get owner/name from owner/name, or from an HTTPS or SSH URL on the Gitea instance of the source.
|
||||
*/
|
||||
private function giteaRepositoryPath(GiteaApp $giteaApp, string $gitRepository): ?string
|
||||
{
|
||||
$repository = sshGitUrlToHttps($gitRepository) ?? $gitRepository;
|
||||
if (str_contains($repository, '://')) {
|
||||
if (strtolower((string) parse_url($repository, PHP_URL_HOST)) !== strtolower((string) parse_url($giteaApp->html_url, PHP_URL_HOST))) {
|
||||
return null;
|
||||
}
|
||||
$repository = Str::chopStart((string) parse_url($repository, PHP_URL_PATH), rtrim((string) parse_url($giteaApp->html_url, PHP_URL_PATH), '/'));
|
||||
}
|
||||
$repository = str($repository)->trim('/')->replaceEnd('.git', '')->toString();
|
||||
|
||||
if (! preg_match('#^[\w.-]+/[\w.-]+$#', $repository) || array_intersect(explode('/', $repository), ['.', '..']) !== []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
@@ -6105,9 +6410,9 @@ class ApplicationsController extends Controller
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'destination_uuid' => 'required|string',
|
||||
'name' => 'string|max:255|nullable',
|
||||
'name' => ValidationPatterns::nameRules(required: false),
|
||||
'clone_volumes' => 'boolean',
|
||||
]);
|
||||
], ValidationPatterns::combinedMessages());
|
||||
$allowedFields = ['destination_uuid', 'name', 'clone_volumes'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiteaApp;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class GiteaController extends Controller
|
||||
{
|
||||
private function removeSensitiveData(GiteaApp $giteaApp, int $teamId): Collection
|
||||
{
|
||||
$sensitiveFields = ['client_secret', 'access_token', 'refresh_token', 'webhook_secret'];
|
||||
|
||||
if (request()->attributes->get('can_read_sensitive', false) === true && $giteaApp->team_id === $teamId) {
|
||||
$giteaApp->makeVisible($sensitiveFields);
|
||||
} else {
|
||||
$giteaApp->makeHidden($sensitiveFields);
|
||||
}
|
||||
|
||||
return serializeApiResponse($giteaApp);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'List all Gitea apps for the current team (and system-wide sources).',
|
||||
path: '/gitea-apps',
|
||||
operationId: 'list-gitea-apps',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Gitea Apps'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List of Gitea apps.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'organization' => ['type' => 'string', 'nullable' => true],
|
||||
'api_url' => ['type' => 'string'],
|
||||
'html_url' => ['type' => 'string'],
|
||||
'custom_user' => ['type' => 'string'],
|
||||
'custom_port' => ['type' => 'integer'],
|
||||
'client_id' => ['type' => 'string', 'nullable' => true],
|
||||
'redirect_uri' => ['type' => 'string', 'nullable' => true],
|
||||
'is_system_wide' => ['type' => 'boolean'],
|
||||
'is_public' => ['type' => 'boolean'],
|
||||
'is_connected' => ['type' => 'boolean', 'description' => 'True when the OAuth connection is complete and the source can deploy private repositories.'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function list_gitea_apps(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$giteaApps = GiteaApp::where(function ($query) use ($teamId) {
|
||||
$query->where('team_id', $teamId)
|
||||
->orWhere('is_system_wide', true);
|
||||
})->get();
|
||||
|
||||
return response()->json($giteaApps->map(
|
||||
fn (GiteaApp $giteaApp) => $this->removeSensitiveData($giteaApp, $teamId)->put('is_connected', $giteaApp->isConnected())
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,14 @@ class CreateApplication extends RestActionTool
|
||||
{
|
||||
protected string $name = 'create_application';
|
||||
|
||||
protected string $description = 'Create an application in a project of the authenticated team, from a public Git repository, a private repository (GitHub App or deploy key), a Dockerfile, or a Docker image. Requires write ability (instant_deploy and manual webhook secrets also need deploy). Same validation and permissions as the REST API.';
|
||||
protected string $description = 'Create an application in a project of the authenticated team, from a public Git repository, a private repository (GitHub App, Gitea App, or deploy key), a Dockerfile, or a Docker image. Requires write ability (instant_deploy and manual webhook secrets also need deploy). Same validation and permissions as the REST API.';
|
||||
|
||||
protected function actions(): array
|
||||
{
|
||||
return [
|
||||
'public' => ['method' => 'POST', 'path' => 'applications/public', 'ability' => 'write', 'description' => 'create from a public Git repository (data: git_repository, git_branch, build_pack, ports_exposes)'],
|
||||
'github_app' => ['method' => 'POST', 'path' => 'applications/private-github-app', 'ability' => 'write', 'description' => 'create from a private repository through a GitHub App (data: github_app_uuid, git_repository, git_branch, build_pack, ports_exposes)'],
|
||||
'gitea_app' => ['method' => 'POST', 'path' => 'applications/private-gitea-app', 'ability' => 'write', 'description' => 'create from a private repository through a connected Gitea App (data: gitea_app_uuid, git_repository as owner/name, git_branch, build_pack, ports_exposes)'],
|
||||
'deploy_key' => ['method' => 'POST', 'path' => 'applications/private-deploy-key', 'ability' => 'write', 'description' => 'create from a private repository with a deploy key (data: private_key_uuid, git_repository, git_branch, build_pack, ports_exposes)'],
|
||||
'dockerfile' => ['method' => 'POST', 'path' => 'applications/dockerfile', 'ability' => 'write', 'description' => 'create from a base64 encoded Dockerfile (data: dockerfile, ports_exposes)'],
|
||||
'docker_image' => ['method' => 'POST', 'path' => 'applications/dockerimage', 'ability' => 'write', 'description' => 'create from a Docker image (data: docker_registry_image_name, docker_registry_image_tag, ports_exposes)'],
|
||||
@@ -32,7 +33,7 @@ class CreateApplication extends RestActionTool
|
||||
protected function dataDescription(): string
|
||||
{
|
||||
return 'Request body, same fields as the REST API. Always: project_uuid, environment_name or environment_uuid, server_uuid, destination_uuid (required when the server has several destinations). '
|
||||
.'public / github_app / deploy_key: git_repository, git_branch, build_pack (nixpacks | railpack | static | dockerfile | dockercompose), ports_exposes (not for dockercompose); github_app also needs github_app_uuid, deploy_key needs private_key_uuid. '
|
||||
.'public / github_app / gitea_app / deploy_key: git_repository, git_branch, build_pack (nixpacks | railpack | static | dockerfile | dockercompose), ports_exposes (not for dockercompose); github_app also needs github_app_uuid, gitea_app needs gitea_app_uuid, deploy_key needs private_key_uuid. '
|
||||
.'dockerfile: dockerfile (base64). docker_image: docker_registry_image_name, docker_registry_image_tag, ports_exposes. '
|
||||
.'Optional: name, description, domains, autogenerate_domain, git_commit_sha, base_directory, publish_directory, install_command, build_command, start_command, docker_compose_location, docker_compose_domains, is_static, is_spa, health_check_*, limits_*, tags, instant_deploy (needs deploy ability).';
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ class ManageGitSource extends RestActionTool
|
||||
{
|
||||
protected string $name = 'manage_git_source';
|
||||
|
||||
protected string $description = 'List, add, update, or delete GitHub apps (resource=github) and GitLab apps (resource=gitlab) of the authenticated team. Secrets and keys are never returned. Requires read ability for list, write ability for changes. Same validation and permissions as the REST API.';
|
||||
protected string $description = 'List, add, update, or delete GitHub apps (resource=github) and GitLab apps (resource=gitlab) of the authenticated team. Gitea apps (resource=gitea) can only be listed; add and connect them in the UI. Secrets and keys are never returned. Requires read ability for list, write ability for changes. Same validation and permissions as the REST API.';
|
||||
|
||||
protected function actions(): array
|
||||
{
|
||||
return [
|
||||
'list' => ['method' => 'GET', 'paths' => ['github' => 'github-apps', 'gitlab' => 'gitlab-apps'], 'ability' => 'read', 'description' => 'git apps of the team and system-wide apps'],
|
||||
'list' => ['method' => 'GET', 'paths' => ['github' => 'github-apps', 'gitlab' => 'gitlab-apps', 'gitea' => 'gitea-apps'], 'ability' => 'read', 'description' => 'git apps of the team and system-wide apps'],
|
||||
'create' => ['method' => 'POST', 'paths' => ['github' => 'github-apps', 'gitlab' => 'gitlab-apps'], 'ability' => 'write', 'description' => 'add a git app (github data: name, html_url, api_url, app_id, installation_id, client_id, client_secret, webhook_secret, private_key_uuid, organization, custom_user, custom_port; gitlab data: name, html_url, api_url, group_name, client_id, client_secret, webhook_token, redirect_uri, custom_user, custom_port)'],
|
||||
'update' => ['method' => 'PATCH', 'paths' => ['github' => 'github-apps/{source_id}', 'gitlab' => 'gitlab-apps/{source_id}'], 'ability' => 'write', 'description' => 'update a git app of the team (data: same fields as create, all optional)'],
|
||||
'delete' => ['method' => 'DELETE', 'paths' => ['github' => 'github-apps/{source_id}', 'gitlab' => 'gitlab-apps/{source_id}'], 'ability' => 'delete', 'confirm' => true, 'description' => 'delete a git app of the team that no application uses'],
|
||||
|
||||
@@ -107,6 +107,18 @@ class GiteaApp extends BaseModel
|
||||
return ! empty($this->access_token) && ! empty($this->refresh_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* The organization field limits the source to repositories of these owners. Empty allows all owners.
|
||||
*/
|
||||
public function allowsRepositoryOwner(?string $owner): bool
|
||||
{
|
||||
$organizations = collect(explode(',', (string) $this->organization))
|
||||
->map(fn ($organization) => strtolower(trim($organization)))
|
||||
->filter();
|
||||
|
||||
return $organizations->isEmpty() || $organizations->contains(strtolower((string) $owner));
|
||||
}
|
||||
|
||||
public function apiUrlBase(): string
|
||||
{
|
||||
$apiUrl = rtrim($this->api_url, '/');
|
||||
|
||||
Reference in New Issue
Block a user