mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 00:23:27 +00:00
feat(api): add GitLab Apps CRUD endpoints
There was no API for creating GitLab sources. Add /api/v1/gitlab-apps list/create/update/delete with OpenAPI docs, sensitive-field redaction, and feature coverage mirroring the GitHub Apps API.
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Rules\SafeExternalUrl;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class GitlabController extends Controller
|
||||
{
|
||||
private function removeSensitiveData(GitlabApp $gitlabApp)
|
||||
{
|
||||
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
||||
$gitlabApp->makeVisible([
|
||||
'client_secret',
|
||||
'webhook_token',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
]);
|
||||
} else {
|
||||
$gitlabApp->makeHidden([
|
||||
'client_secret',
|
||||
'webhook_token',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
]);
|
||||
}
|
||||
|
||||
return serializeApiResponse($gitlabApp);
|
||||
}
|
||||
|
||||
private function findTeamGitlabApp(int|string $gitlabAppId, int $teamId): GitlabApp
|
||||
{
|
||||
return GitlabApp::where('id', $gitlabAppId)
|
||||
->where('team_id', $teamId)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
private function gitlabApiUrlFromHtmlUrl(string $htmlUrl): string
|
||||
{
|
||||
return rtrim($htmlUrl, '/').'/api/v4';
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'List all GitLab apps for the current team (and system-wide sources).',
|
||||
path: '/gitlab-apps',
|
||||
operationId: 'list-gitlab-apps',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['GitLab Apps'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List of GitLab 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'],
|
||||
'api_url' => ['type' => 'string'],
|
||||
'html_url' => ['type' => 'string'],
|
||||
'custom_user' => ['type' => 'string'],
|
||||
'custom_port' => ['type' => 'integer'],
|
||||
'client_id' => ['type' => 'string', 'nullable' => true],
|
||||
'group_name' => ['type' => 'string', 'nullable' => true],
|
||||
'redirect_uri' => ['type' => 'string', 'nullable' => true],
|
||||
'is_system_wide' => ['type' => 'boolean'],
|
||||
'is_public' => ['type' => 'boolean'],
|
||||
'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_gitlab_apps(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$gitlabApps = GitlabApp::where(function ($query) use ($teamId) {
|
||||
$query->where('team_id', $teamId)
|
||||
->orWhere('is_system_wide', true);
|
||||
})->get();
|
||||
|
||||
$gitlabApps = $gitlabApps->map(function ($app) {
|
||||
return $this->removeSensitiveData($app);
|
||||
});
|
||||
|
||||
return response()->json($gitlabApps);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create GitLab App',
|
||||
description: 'Create a new GitLab app (OAuth source). Credentials may be supplied later via the UI or update endpoint.',
|
||||
path: '/gitlab-apps',
|
||||
operationId: 'create-gitlab-app',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['GitLab Apps'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'GitLab app creation payload.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'Name of the GitLab app.'],
|
||||
'html_url' => ['type' => 'string', 'description' => 'GitLab instance URL (e.g., https://gitlab.com).'],
|
||||
'api_url' => ['type' => 'string', 'description' => 'GitLab API URL (defaults to {html_url}/api/v4).'],
|
||||
'custom_user' => ['type' => 'string', 'description' => 'Custom user for SSH access (default: git).'],
|
||||
'custom_port' => ['type' => 'integer', 'description' => 'Custom port for SSH access (default: 22).'],
|
||||
'group_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Optional comma-separated group names to filter repositories.'],
|
||||
'client_id' => ['type' => 'string', 'nullable' => true, 'description' => 'GitLab OAuth Application ID.'],
|
||||
'client_secret' => ['type' => 'string', 'nullable' => true, 'description' => 'GitLab OAuth Application Secret.'],
|
||||
'webhook_token' => ['type' => 'string', 'nullable' => true, 'description' => 'Webhook secret token (auto-generated when omitted).'],
|
||||
'redirect_uri' => ['type' => 'string', 'nullable' => true, 'description' => 'OAuth redirect URI registered in GitLab.'],
|
||||
'is_system_wide' => ['type' => 'boolean', 'description' => 'Is this app system-wide (non-cloud instances only).'],
|
||||
],
|
||||
required: ['name', 'html_url'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'GitLab app created successfully.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'api_url' => ['type' => 'string'],
|
||||
'html_url' => ['type' => 'string'],
|
||||
'custom_user' => ['type' => 'string'],
|
||||
'custom_port' => ['type' => 'integer'],
|
||||
'client_id' => ['type' => 'string', 'nullable' => true],
|
||||
'group_name' => ['type' => 'string', 'nullable' => true],
|
||||
'redirect_uri' => ['type' => 'string', 'nullable' => true],
|
||||
'is_system_wide' => ['type' => 'boolean'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_gitlab_app(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$this->authorize('create', [GitlabApp::class]);
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$allowedFields = [
|
||||
'name',
|
||||
'html_url',
|
||||
'api_url',
|
||||
'custom_user',
|
||||
'custom_port',
|
||||
'group_name',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'webhook_token',
|
||||
'redirect_uri',
|
||||
'is_system_wide',
|
||||
];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'html_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||
'api_url' => ['nullable', 'string', 'url', new SafeExternalUrl],
|
||||
'custom_user' => 'nullable|string|max:255',
|
||||
'custom_port' => 'nullable|integer|min:1|max:65535',
|
||||
'group_name' => 'nullable|string|max:255',
|
||||
'client_id' => 'nullable|string|max:255',
|
||||
'client_secret' => 'nullable|string',
|
||||
'webhook_token' => 'nullable|string',
|
||||
// Callback to this Coolify instance — may be a private/LAN URL; do not use SafeExternalUrl.
|
||||
'redirect_uri' => ['nullable', 'string', 'url'],
|
||||
'is_system_wide' => 'boolean',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$htmlUrl = rtrim((string) $request->input('html_url'), '/');
|
||||
$apiUrl = filled($request->input('api_url'))
|
||||
? rtrim((string) $request->input('api_url'), '/')
|
||||
: $this->gitlabApiUrlFromHtmlUrl($htmlUrl);
|
||||
|
||||
$payload = [
|
||||
'name' => $request->input('name'),
|
||||
'html_url' => $htmlUrl,
|
||||
'api_url' => $apiUrl,
|
||||
'custom_user' => $request->input('custom_user', 'git'),
|
||||
'custom_port' => $request->input('custom_port', 22),
|
||||
'group_name' => $request->input('group_name'),
|
||||
'client_id' => $request->input('client_id'),
|
||||
'client_secret' => $request->input('client_secret'),
|
||||
'webhook_token' => $request->input('webhook_token') ?: Str::random(32),
|
||||
'redirect_uri' => $request->input('redirect_uri'),
|
||||
'is_public' => false,
|
||||
'team_id' => $teamId,
|
||||
];
|
||||
|
||||
if (! isCloud()) {
|
||||
$payload['is_system_wide'] = $request->boolean('is_system_wide', false);
|
||||
}
|
||||
|
||||
$gitlabApp = GitlabApp::create($payload);
|
||||
|
||||
auditLog('api.gitlab_app.created', [
|
||||
'team_id' => $teamId,
|
||||
'gitlab_app_uuid' => $gitlabApp->uuid,
|
||||
'gitlab_app_name' => $gitlabApp->name,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($gitlabApp->fresh()), 201);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
path: '/gitlab-apps/{gitlab_app_id}',
|
||||
operationId: 'updateGitlabApp',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['GitLab Apps'],
|
||||
summary: 'Update GitLab App',
|
||||
description: 'Update an existing GitLab app.',
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'gitlab_app_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'GitLab App ID'
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'GitLab App name'],
|
||||
'html_url' => ['type' => 'string', 'description' => 'GitLab HTML URL'],
|
||||
'api_url' => ['type' => 'string', 'description' => 'GitLab API URL'],
|
||||
'custom_user' => ['type' => 'string', 'description' => 'Custom user for SSH'],
|
||||
'custom_port' => ['type' => 'integer', 'description' => 'Custom port for SSH'],
|
||||
'group_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Optional group filter'],
|
||||
'client_id' => ['type' => 'string', 'nullable' => true, 'description' => 'OAuth Application ID'],
|
||||
'client_secret' => ['type' => 'string', 'nullable' => true, 'description' => 'OAuth Application Secret'],
|
||||
'webhook_token' => ['type' => 'string', 'nullable' => true, 'description' => 'Webhook secret token'],
|
||||
'redirect_uri' => ['type' => 'string', 'nullable' => true, 'description' => 'OAuth redirect URI'],
|
||||
'is_system_wide' => ['type' => 'boolean', 'description' => 'Is system wide (non-cloud instances only)'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'GitLab app updated successfully',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'GitLab app updated successfully'],
|
||||
'data' => ['type' => 'object', 'description' => 'Updated GitLab app data'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||
new OA\Response(response: 404, description: 'GitLab app not found'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
]
|
||||
)]
|
||||
public function update_gitlab_app(Request $request, $gitlab_app_id)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$gitlabApp = $this->findTeamGitlabApp($gitlab_app_id, $teamId);
|
||||
$this->authorize('update', $gitlabApp);
|
||||
|
||||
$allowedFields = [
|
||||
'name',
|
||||
'html_url',
|
||||
'api_url',
|
||||
'custom_user',
|
||||
'custom_port',
|
||||
'group_name',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'webhook_token',
|
||||
'redirect_uri',
|
||||
];
|
||||
|
||||
if (! isCloud()) {
|
||||
$allowedFields[] = 'is_system_wide';
|
||||
}
|
||||
|
||||
$payload = $request->only($allowedFields);
|
||||
|
||||
$rules = [];
|
||||
if (isset($payload['name'])) {
|
||||
$rules['name'] = 'string|max:255';
|
||||
}
|
||||
if (isset($payload['html_url'])) {
|
||||
$rules['html_url'] = ['url', new SafeExternalUrl];
|
||||
}
|
||||
if (isset($payload['api_url'])) {
|
||||
$rules['api_url'] = ['url', new SafeExternalUrl];
|
||||
}
|
||||
if (isset($payload['custom_user'])) {
|
||||
$rules['custom_user'] = 'string|max:255';
|
||||
}
|
||||
if (isset($payload['custom_port'])) {
|
||||
$rules['custom_port'] = 'integer|min:1|max:65535';
|
||||
}
|
||||
if (array_key_exists('group_name', $payload)) {
|
||||
$rules['group_name'] = 'nullable|string|max:255';
|
||||
}
|
||||
if (array_key_exists('client_id', $payload)) {
|
||||
$rules['client_id'] = 'nullable|string|max:255';
|
||||
}
|
||||
if (array_key_exists('client_secret', $payload)) {
|
||||
$rules['client_secret'] = 'nullable|string';
|
||||
}
|
||||
if (array_key_exists('webhook_token', $payload)) {
|
||||
$rules['webhook_token'] = 'nullable|string';
|
||||
}
|
||||
if (array_key_exists('redirect_uri', $payload)) {
|
||||
// Callback to this Coolify instance — may be a private/LAN URL.
|
||||
$rules['redirect_uri'] = 'nullable|url';
|
||||
}
|
||||
if (! isCloud() && isset($payload['is_system_wide'])) {
|
||||
$rules['is_system_wide'] = 'boolean';
|
||||
}
|
||||
|
||||
$validator = customApiValidator($payload, $rules);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation error',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (isset($payload['html_url'])) {
|
||||
$payload['html_url'] = rtrim((string) $payload['html_url'], '/');
|
||||
if (! filled($payload['api_url'] ?? null)) {
|
||||
$payload['api_url'] = $this->gitlabApiUrlFromHtmlUrl($payload['html_url']);
|
||||
}
|
||||
}
|
||||
if (isset($payload['api_url'])) {
|
||||
$payload['api_url'] = rtrim((string) $payload['api_url'], '/');
|
||||
}
|
||||
|
||||
$gitlabApp->update($payload);
|
||||
|
||||
auditLog('api.gitlab_app.updated', [
|
||||
'team_id' => $teamId,
|
||||
'gitlab_app_uuid' => $gitlabApp->uuid,
|
||||
'gitlab_app_name' => $gitlabApp->name,
|
||||
'changed_fields' => array_values(array_diff(array_keys($payload), ['client_secret', 'webhook_token'])),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'GitLab app updated successfully',
|
||||
'data' => $this->removeSensitiveData($gitlabApp->fresh()),
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'message' => 'GitLab app not found',
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
path: '/gitlab-apps/{gitlab_app_id}',
|
||||
operationId: 'deleteGitlabApp',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['GitLab Apps'],
|
||||
summary: 'Delete GitLab App',
|
||||
description: 'Delete a GitLab app if it is not being used by any applications.',
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'gitlab_app_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'GitLab App ID'
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'GitLab app deleted successfully',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'GitLab app deleted successfully'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||
new OA\Response(response: 404, description: 'GitLab app not found'),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Conflict - GitLab app is in use',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'This GitLab app is being used by 5 application(s). Please delete all applications first.'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_gitlab_app($gitlab_app_id)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$gitlabApp = $this->findTeamGitlabApp($gitlab_app_id, $teamId);
|
||||
$this->authorize('delete', $gitlabApp);
|
||||
|
||||
if ($gitlabApp->applications->isNotEmpty()) {
|
||||
$count = $gitlabApp->applications->count();
|
||||
|
||||
return response()->json([
|
||||
'message' => "This GitLab app is being used by {$count} application(s). Please delete all applications first.",
|
||||
], 409);
|
||||
}
|
||||
|
||||
$deletedUuid = $gitlabApp->uuid;
|
||||
$deletedName = $gitlabApp->name;
|
||||
$gitlabApp->delete();
|
||||
|
||||
auditLog('api.gitlab_app.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'gitlab_app_uuid' => $deletedUuid,
|
||||
'gitlab_app_name' => $deletedName,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'GitLab app deleted successfully',
|
||||
]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'message' => 'GitLab app not found',
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -7,9 +7,9 @@ use App\Http\Controllers\Api\DeployController;
|
||||
use App\Http\Controllers\Api\DestinationsController;
|
||||
use App\Http\Controllers\Api\DigitalOceanController;
|
||||
use App\Http\Controllers\Api\GithubController;
|
||||
use App\Http\Controllers\Api\GitlabController;
|
||||
use App\Http\Controllers\Api\HetznerController;
|
||||
use App\Http\Controllers\Api\Internal\FluxResourceStatusController;
|
||||
use App\Support\V5\V5Feature;
|
||||
use App\Http\Controllers\Api\OtherController;
|
||||
use App\Http\Controllers\Api\ProjectController;
|
||||
use App\Http\Controllers\Api\ResourcesController;
|
||||
@@ -25,6 +25,7 @@ use App\Http\Controllers\Api\TeamController;
|
||||
use App\Http\Controllers\Api\VolumeBackupsController;
|
||||
use App\Http\Controllers\Api\VultrController;
|
||||
use App\Http\Middleware\ApiAllowed;
|
||||
use App\Support\V5\V5Feature;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/health', [OtherController::class, 'healthcheck']);
|
||||
@@ -185,6 +186,11 @@ Route::group([
|
||||
Route::get('/github-apps/{github_app_id}/repositories', [GithubController::class, 'load_repositories'])->middleware(['api.ability:read']);
|
||||
Route::get('/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches', [GithubController::class, 'load_branches'])->middleware(['api.ability:read']);
|
||||
|
||||
Route::get('/gitlab-apps', [GitlabController::class, 'list_gitlab_apps'])->middleware(['api.ability:read']);
|
||||
Route::post('/gitlab-apps', [GitlabController::class, 'create_gitlab_app'])->middleware(['api.ability:write']);
|
||||
Route::patch('/gitlab-apps/{gitlab_app_id}', [GitlabController::class, 'update_gitlab_app'])->middleware(['api.ability:write']);
|
||||
Route::delete('/gitlab-apps/{gitlab_app_id}', [GitlabController::class, 'delete_gitlab_app'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::get('/databases', [DatabasesController::class, 'databases'])->middleware(['api.ability:read']);
|
||||
Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware(['api.ability:write']);
|
||||
Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware(['api.ability:write']);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.driver', 'file');
|
||||
config()->set('cache.default', 'array');
|
||||
|
||||
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->token = $this->user->createToken('test-token', ['*']);
|
||||
$this->bearerToken = $this->token->plainTextToken;
|
||||
});
|
||||
|
||||
describe('GET /api/v1/gitlab-apps', function () {
|
||||
test('returns 401 when not authenticated', function () {
|
||||
$this->getJson('/api/v1/gitlab-apps')->assertStatus(401);
|
||||
});
|
||||
|
||||
test('returns empty array when no gitlab apps exist', function () {
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->getJson('/api/v1/gitlab-apps')
|
||||
->assertSuccessful()
|
||||
->assertJson([]);
|
||||
});
|
||||
|
||||
test('returns team gitlab apps without secrets for read tokens', function () {
|
||||
GitlabApp::create([
|
||||
'name' => 'Team GitLab',
|
||||
'api_url' => 'https://gitlab.com/api/v4',
|
||||
'html_url' => 'https://gitlab.com',
|
||||
'custom_user' => 'git',
|
||||
'custom_port' => 22,
|
||||
'client_id' => 'client-id',
|
||||
'client_secret' => 'secret-should-be-hidden',
|
||||
'webhook_token' => 'webhook-should-be-hidden',
|
||||
'team_id' => $this->team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
|
||||
$readToken = $this->user->createToken('read-token', ['read'])->plainTextToken;
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$readToken,
|
||||
])->getJson('/api/v1/gitlab-apps');
|
||||
|
||||
$response->assertSuccessful()
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment(['name' => 'Team GitLab']);
|
||||
|
||||
expect($response->json('0'))->not->toHaveKey('client_secret')
|
||||
->and($response->json('0'))->not->toHaveKey('webhook_token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/gitlab-apps', function () {
|
||||
test('creates a gitlab app with derived api url and generated webhook token', function () {
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->postJson('/api/v1/gitlab-apps', [
|
||||
'name' => 'Self-hosted GitLab',
|
||||
'html_url' => 'https://gitlab.com/',
|
||||
'group_name' => 'mygroup',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonFragment([
|
||||
'name' => 'Self-hosted GitLab',
|
||||
'html_url' => 'https://gitlab.com',
|
||||
'api_url' => 'https://gitlab.com/api/v4',
|
||||
'group_name' => 'mygroup',
|
||||
'custom_user' => 'git',
|
||||
'custom_port' => 22,
|
||||
]);
|
||||
|
||||
$app = GitlabApp::where('name', 'Self-hosted GitLab')->first();
|
||||
expect($app)->not->toBeNull()
|
||||
->and($app->team_id)->toBe($this->team->id)
|
||||
->and($app->webhook_token)->not->toBeEmpty()
|
||||
->and(strlen((string) $app->webhook_token))->toBe(32);
|
||||
});
|
||||
|
||||
test('creates a fully configured gitlab oauth source', function () {
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->postJson('/api/v1/gitlab-apps', [
|
||||
'name' => 'Configured GitLab',
|
||||
'html_url' => 'https://gitlab.com',
|
||||
'client_id' => 'oauth-app-id',
|
||||
'client_secret' => 'oauth-app-secret',
|
||||
'webhook_token' => 'custom-webhook-token',
|
||||
'redirect_uri' => 'https://example.com/webhooks/source/gitlab/redirect',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonFragment([
|
||||
'name' => 'Configured GitLab',
|
||||
'client_id' => 'oauth-app-id',
|
||||
'redirect_uri' => 'https://example.com/webhooks/source/gitlab/redirect',
|
||||
]);
|
||||
|
||||
$app = GitlabApp::where('name', 'Configured GitLab')->first();
|
||||
$app->makeVisible(['client_secret', 'webhook_token']);
|
||||
expect($app->client_secret)->toBe('oauth-app-secret')
|
||||
->and($app->webhook_token)->toBe('custom-webhook-token');
|
||||
});
|
||||
|
||||
test('rejects members without create permission', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$memberToken = $member->createToken('member-token', ['write'])->plainTextToken;
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$memberToken,
|
||||
])->postJson('/api/v1/gitlab-apps', [
|
||||
'name' => 'Forbidden GitLab',
|
||||
'html_url' => 'https://gitlab.com',
|
||||
])->assertForbidden();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/v1/gitlab-apps/{id}', function () {
|
||||
test('updates gitlab app credentials', function () {
|
||||
$app = GitlabApp::create([
|
||||
'name' => 'Existing',
|
||||
'api_url' => 'https://gitlab.com/api/v4',
|
||||
'html_url' => 'https://gitlab.com',
|
||||
'custom_user' => 'git',
|
||||
'custom_port' => 22,
|
||||
'team_id' => $this->team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->patchJson("/api/v1/gitlab-apps/{$app->id}", [
|
||||
'client_id' => 'new-client-id',
|
||||
'group_name' => 'ops',
|
||||
])->assertSuccessful()
|
||||
->assertJsonPath('message', 'GitLab app updated successfully')
|
||||
->assertJsonPath('data.client_id', 'new-client-id')
|
||||
->assertJsonPath('data.group_name', 'ops');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/v1/gitlab-apps/{id}', function () {
|
||||
test('deletes unused gitlab app', function () {
|
||||
$app = GitlabApp::create([
|
||||
'name' => 'Delete me',
|
||||
'api_url' => 'https://gitlab.com/api/v4',
|
||||
'html_url' => 'https://gitlab.com',
|
||||
'custom_user' => 'git',
|
||||
'custom_port' => 22,
|
||||
'team_id' => $this->team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->deleteJson("/api/v1/gitlab-apps/{$app->id}")
|
||||
->assertSuccessful()
|
||||
->assertJsonPath('message', 'GitLab app deleted successfully');
|
||||
|
||||
expect(GitlabApp::find($app->id))->toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user