Skip to content

Commit

Permalink
Merge pull request #16 from fleetbase/dev-v0.0.17
Browse files Browse the repository at this point in the history
v0.0.17
  • Loading branch information
roncodes authored Oct 10, 2024
2 parents 2f9a253 + 5b0f116 commit ad14662
Show file tree
Hide file tree
Showing 14 changed files with 444 additions and 57 deletions.
10 changes: 10 additions & 0 deletions addon/components/extension-pending-publish-viewer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
</div>
</button>
<div class="space-y-2 mt-3">
{{#unless (eq extension.status "published")}}
<Button
@type="magic"
@size="sm"
@icon="rocket"
@text={{t "registry-bridge.component.extension-pending-publish-viewer.publish"}}
@onClick={{perform this.publishExtension extension}}
class="w-full"
/>
{{/unless}}
<Button
@size="sm"
@icon="clipboard-list"
Expand Down
10 changes: 9 additions & 1 deletion addon/components/extension-pending-publish-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class ExtensionPendingPublishViewerComponent extends Component {
}

@task *getExtensionsPendingPublish() {
this.extensions = yield this.store.query('registry-extension', { status: 'approved' });
this.extensions = yield this.store.query('registry-extension', { status: 'approved', admin: 1 });
}

@task *downloadBundle(extension) {
Expand All @@ -27,6 +27,14 @@ export default class ExtensionPendingPublishViewerComponent extends Component {
}
}

@task *publishExtension(extension) {
try {
yield extension.publish();
} catch (error) {
this.notifications.error(error.message);
}
}

@action focusExtension(extension) {
this.focusedExtension = extension;
}
Expand Down
2 changes: 1 addition & 1 deletion addon/components/extension-reviewer-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default class ExtensionReviewerControlComponent extends Component {
}

@task *getExtensionsPendingReview() {
this.extensions = yield this.store.query('registry-extension', { status: 'awaiting_review' });
this.extensions = yield this.store.query('registry-extension', { status: 'awaiting_review', admin: 1 });
}

@task *downloadBundle(extension) {
Expand Down
13 changes: 13 additions & 0 deletions addon/models/registry-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ export default class RegistryExtensionModel extends Model {
return fetch.post('registry-extensions/approve', { id: this.id, ...params }, { namespace: '~registry/v1', normalizeToEmberData: true, modelType: 'registry-extension' });
}

/**
* Submits the registry extension to be manually published.
*
* @return {Promise<RegistryExtensionModel>}
* @memberof RegistryExtensionModel
*/
@action publish(params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');

return fetch.post('registry-extensions/publish', { id: this.id, ...params }, { namespace: '~registry/v1', normalizeToEmberData: true, modelType: 'registry-extension' });
}

/**
* Submits the registry extension for rejection.
*
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/registry-bridge",
"version": "0.0.16",
"version": "0.0.17",
"description": "Internal Bridge between Fleetbase API and Extensions Registry",
"keywords": [
"fleetbase-extension",
Expand All @@ -20,7 +20,7 @@
],
"require": {
"php": "^8.0",
"fleetbase/core-api": "^1.5.10",
"fleetbase/core-api": "*",
"laravel/cashier": "^15.2.1",
"php-http/guzzle7-adapter": "^1.0",
"psr/http-factory-implementation": "*",
Expand Down
2 changes: 1 addition & 1 deletion extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Registry Bridge",
"version": "0.0.16",
"version": "0.0.17",
"description": "Internal Bridge between Fleetbase API and Extensions Registry",
"repository": "https://github.com/fleetbase/registry-bridge",
"license": "AGPL-3.0-or-later",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/registry-bridge-engine",
"version": "0.0.16",
"version": "0.0.17",
"description": "Internal Bridge between Fleetbase API and Extensions Registry",
"fleetbase": {
"route": "extensions"
Expand Down
154 changes: 154 additions & 0 deletions server/src/Http/Controllers/Internal/v1/RegistryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
use Fleetbase\Http\Controllers\Controller;
use Fleetbase\Http\Resources\Category as CategoryResource;
use Fleetbase\Models\Category;
use Fleetbase\Models\File;
use Fleetbase\RegistryBridge\Models\RegistryExtension;
use Fleetbase\RegistryBridge\Models\RegistryExtensionBundle;
use Fleetbase\RegistryBridge\Models\RegistryUser;
use Fleetbase\RegistryBridge\Support\Utils;
use Illuminate\Http\Request;

class RegistryController extends Controller
Expand Down Expand Up @@ -137,4 +141,154 @@ public function lookupPackage(Request $request)
'composer' => $composerJsonName,
]);
}

/**
* Handles the upload of an extension bundle to the registry.
*
* This method performs the following operations:
* - Authenticates the user using a Bearer token from the Authorization header.
* - Validates the uploaded bundle file (ensuring it's a valid tar.gz file).
* - Extracts necessary files (`extension.json`, `package.json`, `composer.json`) from the bundle.
* - Associates the bundle with the correct extension based on package information.
* - Checks if the user is authorized to upload bundles for the extension.
* - Uploads the bundle to the storage system.
* - Creates a file record in the database.
* - Updates metadata and versioning information.
* - Creates a new extension bundle record.
*
* @param Request $request the HTTP request containing the bundle file and authentication token
*
* @return \Illuminate\Http\JsonResponse a JSON response indicating the success or failure of the upload process
*/
public function bundleUpload(Request $request)
{
// Check for Authorization header
$authHeader = $request->header('Authorization');
if (!$authHeader) {
return response()->json(['error' => 'Unauthorized.'], 401);
}

// Extract the token from the 'Bearer ' prefix
$token = null;
if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
$token = $matches[1];
}

// Validate the token (implement your own token validation logic)
$registryUser = RegistryUser::findFromToken($token);
if (!$registryUser) {
return response()->json(['error' => 'Unauthorized.', 'token' => $token], 401);
}

// Check if file was uploaded
if (!$request->hasFile('bundle')) {
return response()->json(['error' => 'No bundle uploaded.'], 400);
}

$bundle = $request->file('bundle');

// Validate the file
if (!$bundle->isValid()) {
return response()->json(['error' => 'Invalid bundle file uploaded.'], 400);
}

// Ensure the file is a tar.gz
$mimeType = $bundle->getMimeType();
if ($mimeType !== 'application/gzip' && $mimeType !== 'application/x-gzip') {
return response()->json(['error' => 'Invalid bundle file type.'], 400);
}

// Get the extension assosciated to bundle by extension name
try {
$bundleData = RegistryExtensionBundle::extractUploadedBundleFile($bundle, ['extension.json', 'package.json', 'composer.json']);
} catch (\Throwable $e) {
return response()->json(['error' => $e->getMessage()], 400);
}

$bundlePackageData = Utils::getObjectKeyValue($bundleData, 'package.json') ?? Utils::getObjectKeyValue($bundleData, 'composer.json');
if ($bundlePackageData && data_get($bundlePackageData, 'name')) {
$extension = RegistryExtension::findByPackageName(data_get($bundlePackageData, 'name'));
if (!$extension) {
return response()->json(['error' => 'Unable to find extension for the uploaded bundle.'], 400);
}

if ($extension->company_uuid !== $registryUser->company_uuid) {
return response()->json(['error' => 'User is not authorized to upload bundles for this extension.'], 401);
}
} else {
return response()->json(['error' => 'Unable to parse uploaded bundle.'], 400);
}

// Prepare to upload the bundle
$size = $bundle->getSize();
$fileName = File::randomFileNameFromRequest($request, 'bundle');
$disk = config('filesystems.default');
$bucket = config('filesystems.disks.' . $disk . '.bucket', config('filesystems.disks.s3.bucket'));
$path = 'uploads/extensions/' . $extension->uuid . '/bundles';
$type = 'extension_bundle';

// Upload the bundle
try {
$path = $bundle->storeAs($path, $fileName, ['disk' => $disk]);
} catch (\Throwable $e) {
return response()->error($e->getMessage());
}

// If file upload failed
if ($path === false) {
return response()->error('File upload failed.');
}

// Create a file record
try {
$file = File::createFromUpload($request->file('bundle'), $path, $type, $size, $disk, $bucket);
} catch (\Throwable $e) {
return response()->error($e->getMessage());
}

// Set company and uploader
$file->update([
'company_uuid' => $registryUser->company_uuid,
'uploader_uuid' => $registryUser->user_uuid,
]);

// Set file subject to extension
$file = $file->setSubject($extension);

// Get extension.json contents
$extensionJson = Utils::getObjectKeyValue($bundleData, 'extension.json');
if (!$extensionJson) {
return response()->error('Unable to find `extension.json` file required in bundle.');
}

// Set version in file meta
$file->updateMeta('version', data_get($extensionJson, 'version'));

// Check if version is set
if (!isset($extensionJson->version)) {
return response()->error('No `version` set in the `extension.json`');
}

// Check if either api or engine property is set
if (!isset($extensionJson->engine) && !isset($extensionJson->api)) {
return response()->error('No `api` or `engine` property set in the `extension.json`');
}

// Set bundle number to parsed JSON
$extensionJson->bundle_number = RegistryExtensionBundle::getNextBundleNumber($extension);

// Create the bundle
$extensionBundle = RegistryExtensionBundle::create([
'company_uuid' => $registryUser->company_uuid,
'created_by_uuid' => $registryUser->user_uuid,
'extension_uuid' => $extension->uuid,
'bundle_uuid' => $file->uuid,
'status' => 'pending',
]);

$extensionBundle->update(['bundle_number' => $extensionJson->bundle_number, 'version' => $extensionJson->version]);
$extensionBundle->updateMetaProperties((array) $bundleData);

return response()->json(['message' => 'Bundle uploaded successfully', 'filename' => $fileName, 'bundle' => $extensionBundle, 'extension' => $extension], 200);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,32 @@ public function approve(RegistryExtensionActionRequest $request)
return ['registryExtension' => new $this->resource($extension)];
}

/**
* Mannually publishes a specific extension by its ID.
*
* This function locates a `RegistryExtension` using the provided ID and sets its status to 'published'.
* If the extension is successfully found and updated, it returns the extension resource. If the extension
* cannot be found, it returns an error response indicating the inability to locate the extension.
*
* @param RegistryExtensionActionRequest $request the validated request object
*
* @return \Illuminate\Http\Response|array returns an array containing the extension resource if successful,
* or an error response if the extension cannot be found
*/
public function manualPublish(RegistryExtensionActionRequest $request)
{
$id = $request->input('id');
$extension = RegistryExtension::find($id);
if ($extension) {
$extension->update(['status' => 'published', 'current_bundle_uuid' => $extension->next_bundle_uuid]);
$extension->nextBundle()->update(['status' => 'published']);
} else {
return response()->error('Unable to find extension to publish.');
}

return ['registryExtension' => new $this->resource($extension)];
}

/**
* Rejects a specific extension by its ID.
*
Expand Down
10 changes: 9 additions & 1 deletion server/src/Http/Filter/RegistryExtensionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RegistryExtensionFilter extends Filter
{
public function queryForInternal()
{
if ($this->request->boolean('explore')) {
if ($this->request->boolean('explore') || $this->request->boolean('admin')) {
return;
}
$this->builder->where('company_uuid', $this->session->get('company'));
Expand All @@ -21,6 +21,14 @@ public function queryForPublic()
$this->builder->where('company_uuid', $this->session->get('company'));
}

public function admin()
{
$user = $this->request->user();
if ($user && $user->isNotAdmin()) {
$this->builder->where('company_uuid', $this->session->get('company'));
}
}

public function query(?string $searchQuery)
{
$this->builder->search($searchQuery);
Expand Down
Loading

0 comments on commit ad14662

Please sign in to comment.