@fleetbase/registry-bridge-engine 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/modals/create-registry-credentials.hbs +5 -0
- package/addon/controllers/developers/credentials.js +106 -0
- package/addon/routes/developers/credentials.js +8 -1
- package/addon/templates/application.hbs +1 -0
- package/addon/templates/developers/credentials.hbs +13 -1
- package/app/components/modals/create-registry-credentials.js +1 -0
- package/app/controllers/developers/credentials.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/config/registry-bridge.php +2 -1
- package/server/migrations/2024_07_18_151000_add_auth_token_column_to_registry_users_table.php +28 -0
- package/server/src/Http/Controllers/Internal/v1/RegistryAuthController.php +120 -27
- package/server/src/Http/Requests/RegistryAuthRequest.php +3 -1
- package/server/src/Http/Resources/RegistryUser.php +0 -12
- package/server/src/Models/RegistryExtension.php +40 -0
- package/server/src/Models/RegistryUser.php +111 -1
- package/server/src/Providers/RegistryBridgeServiceProvider.php +0 -1
- package/server/src/Support/Bridge.php +123 -0
- package/server/src/routes.php +42 -31
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
|
2
|
+
<div class="modal-body-container">
|
|
3
|
+
<InputGroup @name="Password" @helpText="You must authenticate using your password." @placeholder="Enter your password" @value={{@options.password}} @type="password" />
|
|
4
|
+
</div>
|
|
5
|
+
</Modal::Default>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import Controller from '@ember/controller';
|
|
2
|
+
import { action } from '@ember/object';
|
|
3
|
+
import { inject as service } from '@ember/service';
|
|
4
|
+
|
|
5
|
+
export default class DevelopersCredentialsController extends Controller {
|
|
6
|
+
@service modalsManager;
|
|
7
|
+
@service notifications;
|
|
8
|
+
@service hostRouter;
|
|
9
|
+
@service fetch;
|
|
10
|
+
|
|
11
|
+
columns = [
|
|
12
|
+
{
|
|
13
|
+
label: 'Owner',
|
|
14
|
+
valuePath: 'user.name',
|
|
15
|
+
width: '15%',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'Fleetbase Token',
|
|
19
|
+
valuePath: 'token',
|
|
20
|
+
cellComponent: 'click-to-copy',
|
|
21
|
+
width: '20%',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Registry Token',
|
|
25
|
+
valuePath: 'registry_token',
|
|
26
|
+
cellComponent: 'click-to-reveal',
|
|
27
|
+
cellComponentArgs: {
|
|
28
|
+
clickToCopy: true,
|
|
29
|
+
},
|
|
30
|
+
width: '25%',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: 'Expiry',
|
|
34
|
+
valuePath: 'expires_at',
|
|
35
|
+
width: '15%',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'Created',
|
|
39
|
+
valuePath: 'created_at',
|
|
40
|
+
width: '15%',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: '',
|
|
44
|
+
cellComponent: 'table/cell/dropdown',
|
|
45
|
+
ddButtonText: false,
|
|
46
|
+
ddButtonIcon: 'ellipsis-h',
|
|
47
|
+
ddButtonIconPrefix: 'fas',
|
|
48
|
+
ddMenuLabel: 'Credential Actions',
|
|
49
|
+
cellClassNames: 'overflow-visible',
|
|
50
|
+
wrapperClass: 'flex items-center justify-end mx-2',
|
|
51
|
+
width: '10%',
|
|
52
|
+
align: 'right',
|
|
53
|
+
actions: [
|
|
54
|
+
{
|
|
55
|
+
label: 'Delete Credentials',
|
|
56
|
+
fn: this.deleteCredentials,
|
|
57
|
+
className: 'text-red-700 hover:text-red-800',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
@action deleteCredentials(credentials) {
|
|
64
|
+
this.modalsManager.confirm({
|
|
65
|
+
title: 'Delete extension registry credentials?',
|
|
66
|
+
body: 'Are you sure you wish to delete these credentials? Once deleted any service or user using these credentials will loose access to the registry.',
|
|
67
|
+
confirm: async (modal) => {
|
|
68
|
+
modal.startLoading();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await this.fetch.delete(`auth/registry-tokens/${credentials.uuid}`, {}, { namespace: '~registry/v1' });
|
|
72
|
+
this.notifications.success('Registry credentials deleted.');
|
|
73
|
+
return this.hostRouter.refresh();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.notifications.serverError(error);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@action createCredentials() {
|
|
82
|
+
this.modalsManager.show('modals/create-registry-credentials', {
|
|
83
|
+
title: 'Create new registry credentials',
|
|
84
|
+
acceptButtonText: 'Create',
|
|
85
|
+
acceptButtonIcon: 'check',
|
|
86
|
+
password: null,
|
|
87
|
+
confirm: async (modal) => {
|
|
88
|
+
modal.startLoading();
|
|
89
|
+
|
|
90
|
+
const password = modal.getOption('password');
|
|
91
|
+
if (!password) {
|
|
92
|
+
this.notifications.warning('Password cannot be empty');
|
|
93
|
+
return modal.stopLoading();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await this.fetch.post('auth/registry-tokens', { password }, { namespace: '~registry/v1' });
|
|
98
|
+
this.notifications.success('Registry credentials created.');
|
|
99
|
+
return this.hostRouter.refresh();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.notifications.serverError(error);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import Route from '@ember/routing/route';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
2
3
|
|
|
3
|
-
export default class DevelopersCredentialsRoute extends Route {
|
|
4
|
+
export default class DevelopersCredentialsRoute extends Route {
|
|
5
|
+
@service fetch;
|
|
6
|
+
|
|
7
|
+
model() {
|
|
8
|
+
return this.fetch.get('auth/registry-tokens', {}, { namespace: '~registry/v1' });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<Layout::Sidebar::Item @route="console.extensions.developers.extensions" @icon="box-archive">Extensions</Layout::Sidebar::Item>
|
|
18
18
|
<Layout::Sidebar::Item @route="console.extensions.developers.analytics" @icon="chart-simple">Analytics</Layout::Sidebar::Item>
|
|
19
19
|
<Layout::Sidebar::Item @route="console.extensions.developers.payments" @icon="cash-register">Payments</Layout::Sidebar::Item>
|
|
20
|
+
<Layout::Sidebar::Item @route="console.extensions.developers.credentials" @icon="key">Credentials</Layout::Sidebar::Item>
|
|
20
21
|
</Layout::Sidebar::Panel>
|
|
21
22
|
<Spacer @height="200px" />
|
|
22
23
|
</EmberWormhole>
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
<Layout::Section::Header @title="Credentials">
|
|
2
|
+
<Button @type="primary" @icon="plus" @iconPrefix="fas" @text="Create new credentials" class="mr-2" @onClick={{this.createCredentials}} />
|
|
3
|
+
</Layout::Section::Header>
|
|
4
|
+
|
|
5
|
+
<Layout::Section::Body class="overflow-y-scroll h-full">
|
|
6
|
+
<Table
|
|
7
|
+
@rows={{@model}}
|
|
8
|
+
@columns={{this.columns}}
|
|
9
|
+
@selectable={{false}}
|
|
10
|
+
@canSelectAll={{false}}
|
|
11
|
+
@pagination={{false}}
|
|
12
|
+
/>
|
|
13
|
+
</Layout::Section::Body>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/registry-bridge-engine/components/modals/create-registry-credentials';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/registry-bridge-engine/controllers/developers/credentials';
|
package/composer.json
CHANGED
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -26,7 +26,8 @@ return [
|
|
|
26
26
|
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
|
|
27
27
|
],
|
|
28
28
|
'extensions' => [
|
|
29
|
-
'preinstalled' => Utils::castBoolean(env('REGISTRY_PREINSTALLED_EXTENSIONS', false))
|
|
29
|
+
'preinstalled' => Utils::castBoolean(env('REGISTRY_PREINSTALLED_EXTENSIONS', false)),
|
|
30
|
+
'protected_prefixes' => explode(',', env('REGISTRY_PROTECTED_PREFIXES', '@fleetbase,fleetbase,@flb,@fleetbase-extension,@flb-extension'))
|
|
30
31
|
],
|
|
31
32
|
'facilitator_fee' => env('REGISTRY_FACILITATOR_FEE', 10)
|
|
32
33
|
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Illuminate\Database\Migrations\Migration;
|
|
4
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
5
|
+
use Illuminate\Support\Facades\Schema;
|
|
6
|
+
|
|
7
|
+
return new class extends Migration
|
|
8
|
+
{
|
|
9
|
+
/**
|
|
10
|
+
* Run the migrations.
|
|
11
|
+
*/
|
|
12
|
+
public function up(): void
|
|
13
|
+
{
|
|
14
|
+
Schema::table('registry_users', function (Blueprint $table) {
|
|
15
|
+
$table->string('registry_token')->nullable()->after('token');
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reverse the migrations.
|
|
21
|
+
*/
|
|
22
|
+
public function down(): void
|
|
23
|
+
{
|
|
24
|
+
Schema::table('registry_users', function (Blueprint $table) {
|
|
25
|
+
$table->dropColumn('registry_token');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -12,14 +12,11 @@ use Fleetbase\RegistryBridge\Models\RegistryExtension;
|
|
|
12
12
|
use Fleetbase\RegistryBridge\Models\RegistryUser;
|
|
13
13
|
use Fleetbase\RegistryBridge\Support\Bridge;
|
|
14
14
|
use Fleetbase\Support\Auth;
|
|
15
|
+
use Illuminate\Http\Request;
|
|
16
|
+
use Illuminate\Support\Str;
|
|
15
17
|
|
|
16
18
|
class RegistryAuthController extends Controller
|
|
17
19
|
{
|
|
18
|
-
public function test()
|
|
19
|
-
{
|
|
20
|
-
dd(Bridge::get('~/flb/extensions'));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
/**
|
|
24
21
|
* Authenticates a registry user based on provided credentials.
|
|
25
22
|
*
|
|
@@ -129,15 +126,29 @@ class RegistryAuthController extends Controller
|
|
|
129
126
|
*/
|
|
130
127
|
public function checkAccess(RegistryAuthRequest $request)
|
|
131
128
|
{
|
|
132
|
-
|
|
133
|
-
$identity
|
|
129
|
+
$packageName = $request->input('package');
|
|
130
|
+
$identity = $request->input('identity');
|
|
131
|
+
$protectedPackage = Str::startsWith($packageName, config('registry-bridge.extensions.protected_prefixes'));
|
|
134
132
|
|
|
135
|
-
//
|
|
136
|
-
|
|
133
|
+
// If no identity and not a protected package allow access
|
|
134
|
+
if (!$identity && !$protectedPackage) {
|
|
135
|
+
return response()->json(['allowed' => true]);
|
|
136
|
+
}
|
|
137
137
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// Get registry user via identity
|
|
139
|
+
$registryUser = RegistryUser::findFromUsername($identity);
|
|
140
|
+
|
|
141
|
+
// If registry user is admin allow access
|
|
142
|
+
if ($registryUser->is_admin) {
|
|
143
|
+
return response()->json(['allowed' => true]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if package is protected, if so verify user has access to package
|
|
147
|
+
if ($protectedPackage) {
|
|
148
|
+
$extension = RegistryExtension::findByPackageName($packageName);
|
|
149
|
+
if ($extension && $extension->doesntHaveAccess($registryUser)) {
|
|
150
|
+
return response()->error('This package requires payment to access.', 401);
|
|
151
|
+
}
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
// For now only admin users can access registry
|
|
@@ -171,15 +182,16 @@ class RegistryAuthController extends Controller
|
|
|
171
182
|
$force = $request->boolean('force');
|
|
172
183
|
$password = $request->input('password');
|
|
173
184
|
|
|
185
|
+
// Find user by email or username
|
|
186
|
+
$registryUser = RegistryUser::findFromUsername($identity);
|
|
187
|
+
if (!$registryUser) {
|
|
188
|
+
return response()->error('Attempting to publish extension with invalid user.', 401);
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
// If force publish bypass checks, authenticate by user login
|
|
175
192
|
if ($force === true) {
|
|
176
|
-
// Find user by email or username
|
|
177
|
-
$user = User::where(function ($query) use ($identity) {
|
|
178
|
-
$query->where('email', $identity)->orWhere('phone', $identity)->orWhere('username', $identity);
|
|
179
|
-
})->first();
|
|
180
|
-
|
|
181
193
|
// Authenticate user with password
|
|
182
|
-
if (Auth::isInvalidPassword($password, $user->password)) {
|
|
194
|
+
if (Auth::isInvalidPassword($password, $registryUser->user->password)) {
|
|
183
195
|
return response()->error('Invalid credentials, unable to force publish.', 401);
|
|
184
196
|
}
|
|
185
197
|
|
|
@@ -197,16 +209,10 @@ class RegistryAuthController extends Controller
|
|
|
197
209
|
return response()->error('Attempting to publish extension which has no record.', 401);
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
// Find user by email or username
|
|
201
|
-
$user = User::where(function ($query) use ($identity) {
|
|
202
|
-
$query->where('email', $identity)->orWhere('phone', $identity)->orWhere('username', $identity);
|
|
203
|
-
})->first();
|
|
204
|
-
if (!$user) {
|
|
205
|
-
return response()->error('Attempting to publish extension with invalid user.', 401);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
212
|
// If user is not admin respond with error
|
|
209
|
-
|
|
213
|
+
// For now only admin is allowed to publish to registry
|
|
214
|
+
// This may change in the future with approval/reject flow
|
|
215
|
+
if ($registryUser->isNotAdmin()) {
|
|
210
216
|
return response()->error('User is not allowed publish to the registry.', 401);
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -227,4 +233,91 @@ class RegistryAuthController extends Controller
|
|
|
227
233
|
// Passed all checks
|
|
228
234
|
return response()->json(['allowed' => true]);
|
|
229
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Creates a registry user by authenticating with the provided password.
|
|
239
|
+
*
|
|
240
|
+
* This method retrieves the current authenticated user and checks the provided password.
|
|
241
|
+
* If the password is valid, it logs in to the npm registry using the user's credentials,
|
|
242
|
+
* retrieves the authentication token, and associates it with the user. The registry token
|
|
243
|
+
* is stored in the database for the user's current session.
|
|
244
|
+
*
|
|
245
|
+
* @param Request $request the incoming HTTP request containing the user's password
|
|
246
|
+
*
|
|
247
|
+
* @return \Illuminate\Http\JsonResponse the JSON response containing the created RegistryUser or an error message
|
|
248
|
+
*/
|
|
249
|
+
public function createRegistryUser(Request $request)
|
|
250
|
+
{
|
|
251
|
+
$password = $request->input('password');
|
|
252
|
+
if (!$password) {
|
|
253
|
+
return response()->error('Password is required.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get current user
|
|
257
|
+
$user = Auth::getUserFromSession();
|
|
258
|
+
if (!$user) {
|
|
259
|
+
return response()->error('No user authenticated.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Authenticate user with password
|
|
263
|
+
if (Auth::isInvalidPassword($password, $user->password)) {
|
|
264
|
+
return response()->error('Invalid credentials.', 401);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Create registry user
|
|
268
|
+
try {
|
|
269
|
+
$registryUser = Bridge::loginWithUser($user, $password);
|
|
270
|
+
} catch (\Throwable $e) {
|
|
271
|
+
return response()->json($e->getMessage());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return response()->json($registryUser);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Retrieves all registry tokens for the current company.
|
|
279
|
+
*
|
|
280
|
+
* This method queries the `RegistryUser` model to get all registry tokens
|
|
281
|
+
* associated with the current company's UUID from the session. It also includes
|
|
282
|
+
* user details for each registry token and returns the data as a JSON response.
|
|
283
|
+
*
|
|
284
|
+
* @return \Illuminate\Http\JsonResponse the JSON response containing a list of registry tokens with user details
|
|
285
|
+
*/
|
|
286
|
+
public function getRegistryTokens()
|
|
287
|
+
{
|
|
288
|
+
$registryUsers = RegistryUser::select(
|
|
289
|
+
['uuid', 'user_uuid', 'company_uuid', 'token', 'registry_token', 'expires_at', 'created_at']
|
|
290
|
+
)->where('company_uuid', session('company'))->with(
|
|
291
|
+
[
|
|
292
|
+
'user' => function ($query) {
|
|
293
|
+
$query->select(['uuid', 'company_uuid', 'name', 'email']);
|
|
294
|
+
},
|
|
295
|
+
]
|
|
296
|
+
)->get();
|
|
297
|
+
|
|
298
|
+
return response()->json($registryUsers);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Deletes a specific registry token by its UUID.
|
|
303
|
+
*
|
|
304
|
+
* This method deletes a registry token identified by its UUID. If the registry token
|
|
305
|
+
* does not exist, it returns an error response. If successful, it returns a JSON response
|
|
306
|
+
* with a status indicating the deletion was successful.
|
|
307
|
+
*
|
|
308
|
+
* @param string $id the UUID of the registry token to be deleted
|
|
309
|
+
*
|
|
310
|
+
* @return \Illuminate\Http\JsonResponse the JSON response indicating the status of the deletion
|
|
311
|
+
*/
|
|
312
|
+
public function deleteRegistryToken(string $id)
|
|
313
|
+
{
|
|
314
|
+
$registryUser = RegistryUser::where('uuid', $id)->first();
|
|
315
|
+
if (!$registryUser) {
|
|
316
|
+
return response()->error('Registry token does not exist.');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
$registryUser->delete();
|
|
320
|
+
|
|
321
|
+
return response()->json(['status' => 'ok']);
|
|
322
|
+
}
|
|
230
323
|
}
|
|
@@ -5,6 +5,7 @@ namespace Fleetbase\RegistryBridge\Http\Requests;
|
|
|
5
5
|
use Fleetbase\Http\Requests\FleetbaseRequest;
|
|
6
6
|
use Fleetbase\Models\User;
|
|
7
7
|
use Illuminate\Support\Facades\Validator;
|
|
8
|
+
use Illuminate\Validation\Rule;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Request class to handle the addition of new registry users.
|
|
@@ -40,7 +41,8 @@ class RegistryAuthRequest extends FleetbaseRequest
|
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
return [
|
|
43
|
-
'identity' => ['
|
|
44
|
+
'identity' => [Rule::requiredIf($this->isNotFilled('package')), 'valid_identity'],
|
|
45
|
+
'package' => ['nullable'],
|
|
44
46
|
];
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
namespace Fleetbase\RegistryBridge\Http\Resources;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
6
|
-
use Fleetbase\Models\Group;
|
|
7
6
|
|
|
8
7
|
class RegistryUser extends FleetbaseResource
|
|
9
8
|
{
|
|
@@ -26,15 +25,4 @@ class RegistryUser extends FleetbaseResource
|
|
|
26
25
|
'created_at' => $this->created_at,
|
|
27
26
|
];
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
public function groups(): array
|
|
31
|
-
{
|
|
32
|
-
return collect(['$all', '$authenticated', ...data_get($this->user, 'groups', [])])->map(function ($group) {
|
|
33
|
-
if ($group instanceof Group) {
|
|
34
|
-
return $group->public_id;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return $group;
|
|
38
|
-
})->toArray();
|
|
39
|
-
}
|
|
40
28
|
}
|
|
@@ -653,4 +653,44 @@ class RegistryExtension extends Model
|
|
|
653
653
|
|
|
654
654
|
return $checkoutSession;
|
|
655
655
|
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Determine if the registry user has access to the registry extension.
|
|
659
|
+
*
|
|
660
|
+
* This method checks if the extension requires payment. If it does not,
|
|
661
|
+
* access is granted. If payment is required, it checks if the user's company
|
|
662
|
+
* has made a purchase of the extension.
|
|
663
|
+
*
|
|
664
|
+
* @param RegistryUser $registryUser the registry user to check access for
|
|
665
|
+
*
|
|
666
|
+
* @return bool true if the user has access, false otherwise
|
|
667
|
+
*/
|
|
668
|
+
public function hasAccess(RegistryUser $registryUser): bool
|
|
669
|
+
{
|
|
670
|
+
if (!$this->payment_required) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return $this->purchases()->where('company_uuid', $registryUser->company_uuid)->exists();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Determine if the registry user does not have access to the registry extension.
|
|
679
|
+
*
|
|
680
|
+
* This method checks if the extension requires payment. If it does not,
|
|
681
|
+
* access is always denied. If payment is required, it checks if the user's
|
|
682
|
+
* company has not made a purchase of the extension.
|
|
683
|
+
*
|
|
684
|
+
* @param RegistryUser $registryUser the registry user to check access for
|
|
685
|
+
*
|
|
686
|
+
* @return bool true if the user does not have access, false otherwise
|
|
687
|
+
*/
|
|
688
|
+
public function doesntHaveAccess(RegistryUser $registryUser): bool
|
|
689
|
+
{
|
|
690
|
+
if (!$this->payment_required) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return $this->purchases()->where('company_uuid', $registryUser->company_uuid)->doesntExist();
|
|
695
|
+
}
|
|
656
696
|
}
|
|
@@ -41,6 +41,7 @@ class RegistryUser extends Model
|
|
|
41
41
|
'company_uuid',
|
|
42
42
|
'user_uuid',
|
|
43
43
|
'token',
|
|
44
|
+
'registry_token',
|
|
44
45
|
'scope',
|
|
45
46
|
'expires_at',
|
|
46
47
|
'last_used_at',
|
|
@@ -65,7 +66,7 @@ class RegistryUser extends Model
|
|
|
65
66
|
*
|
|
66
67
|
* @var array
|
|
67
68
|
*/
|
|
68
|
-
protected $appends = [];
|
|
69
|
+
protected $appends = ['is_admin'];
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* The attributes excluded from the model's JSON form.
|
|
@@ -114,6 +115,54 @@ class RegistryUser extends Model
|
|
|
114
115
|
return $this->belongsTo(User::class);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
|
120
|
+
*/
|
|
121
|
+
public function extensions()
|
|
122
|
+
{
|
|
123
|
+
return $this->hasMany(RegistryExtension::class, 'company_uuid', 'company_uuid');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
|
128
|
+
*/
|
|
129
|
+
public function purchases()
|
|
130
|
+
{
|
|
131
|
+
return $this->hasMany(RegistryExtensionPurchase::class, 'company_uuid', 'company_uuid');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Undocumented function.
|
|
136
|
+
*/
|
|
137
|
+
public function getIsAdminAttribute(): bool
|
|
138
|
+
{
|
|
139
|
+
return $this->user->is_admin === true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Determine if the user is an admin.
|
|
144
|
+
*
|
|
145
|
+
* This method checks if the `is_admin` attribute of the user is set to true.
|
|
146
|
+
*
|
|
147
|
+
* @return bool true if the user is an admin, false otherwise
|
|
148
|
+
*/
|
|
149
|
+
public function isAdmin(): bool
|
|
150
|
+
{
|
|
151
|
+
return $this->is_admin === true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Determine if the user is not an admin.
|
|
156
|
+
*
|
|
157
|
+
* This method checks if the `is_admin` attribute of the user is set to false.
|
|
158
|
+
*
|
|
159
|
+
* @return bool true if the user is not an admin, false otherwise
|
|
160
|
+
*/
|
|
161
|
+
public function isNotAdmin(): bool
|
|
162
|
+
{
|
|
163
|
+
return $this->is_admin === false;
|
|
164
|
+
}
|
|
165
|
+
|
|
117
166
|
/**
|
|
118
167
|
* Generates a unique token for authenticating with the registry.
|
|
119
168
|
*
|
|
@@ -137,4 +186,65 @@ class RegistryUser extends Model
|
|
|
137
186
|
|
|
138
187
|
return $token;
|
|
139
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Find a registry user by their username.
|
|
192
|
+
*
|
|
193
|
+
* This method joins the users table with the registry users table to find a
|
|
194
|
+
* registry user by their email or username. If a matching user is found, the
|
|
195
|
+
* corresponding registry user is returned.
|
|
196
|
+
*
|
|
197
|
+
* @param string $username the username to search for
|
|
198
|
+
*
|
|
199
|
+
* @return RegistryUser|null the found registry user, or null if no user is found
|
|
200
|
+
*/
|
|
201
|
+
public static function findFromUsername(string $username): ?RegistryUser
|
|
202
|
+
{
|
|
203
|
+
return static::select('registry_users.*')
|
|
204
|
+
->join('users', function ($join) use ($username) {
|
|
205
|
+
$join->on('users.uuid', '=', 'registry_users.user_uuid')
|
|
206
|
+
->on('users.company_uuid', '=', 'registry_users.company_uuid')
|
|
207
|
+
->where(function ($query) use ($username) {
|
|
208
|
+
$query->where('users.email', $username)
|
|
209
|
+
->orWhere('users.username', $username);
|
|
210
|
+
});
|
|
211
|
+
})
|
|
212
|
+
->first();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Determine if the registry user can access a specific package.
|
|
217
|
+
*
|
|
218
|
+
* This method checks if the package name exists in the list of purchased
|
|
219
|
+
* extensions for the user. It verifies if the package name matches either
|
|
220
|
+
* the `package.json` or `composer.json` name in the metadata of the current bundle
|
|
221
|
+
* of any purchased extension.
|
|
222
|
+
*
|
|
223
|
+
* @param string $packageName the name of the package to check access for
|
|
224
|
+
*
|
|
225
|
+
* @return bool true if the user can access the package, false otherwise
|
|
226
|
+
*/
|
|
227
|
+
public function canAccessPackage(string $packageName): bool
|
|
228
|
+
{
|
|
229
|
+
return $this->purchases()->whereHas('extension', function ($query) use ($packageName) {
|
|
230
|
+
$query->whereHas('currentBundle', function ($query) use ($packageName) {
|
|
231
|
+
$query->where('meta->package.json->name', $packageName)->orWhere('meta->composer.json->name', $packageName);
|
|
232
|
+
});
|
|
233
|
+
})->exists();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Retrieves the user's access groups.
|
|
238
|
+
*
|
|
239
|
+
* This method returns an array of groups that the user belongs to, including
|
|
240
|
+
* the default groups (`$all` and `$authenticated`) and the names of the purchased
|
|
241
|
+
* extension groups. The purchased extension groups are obtained by calling the
|
|
242
|
+
* `getPurchasedExtensionGroups` method.
|
|
243
|
+
*
|
|
244
|
+
* @return array an array of the user's access groups, including default and purchased extension groups
|
|
245
|
+
*/
|
|
246
|
+
public function groups(): array
|
|
247
|
+
{
|
|
248
|
+
return [$this->public_id];
|
|
249
|
+
}
|
|
140
250
|
}
|
|
@@ -37,7 +37,6 @@ class RegistryBridgeServiceProvider extends CoreServiceProvider
|
|
|
37
37
|
*/
|
|
38
38
|
public $middleware = [
|
|
39
39
|
'fleetbase.registry' => [
|
|
40
|
-
'throttle:60,1',
|
|
41
40
|
\Illuminate\Session\Middleware\StartSession::class,
|
|
42
41
|
\Fleetbase\Http\Middleware\AuthenticateOnceWithBasicAuth::class,
|
|
43
42
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\RegistryBridge\Support;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\Models\User;
|
|
6
|
+
use Fleetbase\RegistryBridge\Models\RegistryUser;
|
|
7
|
+
use Fleetbase\Support\Auth;
|
|
5
8
|
use Illuminate\Support\Facades\Http;
|
|
9
|
+
use Illuminate\Support\Facades\Storage;
|
|
10
|
+
use Illuminate\Support\Str;
|
|
11
|
+
use Symfony\Component\Process\Exception\ProcessFailedException;
|
|
12
|
+
use Symfony\Component\Process\Process;
|
|
6
13
|
|
|
7
14
|
class Bridge
|
|
8
15
|
{
|
|
@@ -50,4 +57,120 @@ class Bridge
|
|
|
50
57
|
{
|
|
51
58
|
return Http::withOptions($options)->post(static::createUrl($uri), $parameters);
|
|
52
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Logs in to the npm registry using the provided user and retrieves the authentication token.
|
|
63
|
+
*
|
|
64
|
+
* This method uses the npm-cli-login tool to authenticate with the npm registry using the
|
|
65
|
+
* provided user's credentials, retrieves the authentication token from the .npmrc file,
|
|
66
|
+
* and associates it with the currently authenticated user in the application. The registry
|
|
67
|
+
* token is stored in the database for the user's current session.
|
|
68
|
+
*
|
|
69
|
+
* @param User $user the fleetbase user model containing the username and email
|
|
70
|
+
* @param string $password the npm password for the user
|
|
71
|
+
*
|
|
72
|
+
* @return RegistryUser the RegistryUser model containing the registry token and associated data
|
|
73
|
+
*
|
|
74
|
+
* @throws \Exception If there is no active session, the .npmrc file is not found, the auth token is not found, or the npm login fails.
|
|
75
|
+
*/
|
|
76
|
+
public static function loginWithUser(User $user, string $password): RegistryUser
|
|
77
|
+
{
|
|
78
|
+
return static::login($user->username, $password, $user->email);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Logs in to the fleetbase registry and retrieves the authentication token.
|
|
83
|
+
*
|
|
84
|
+
* This method uses the npm-cli-login tool to authenticate with the fleetbase registry,
|
|
85
|
+
* retrieves the authentication token from the .npmrc file, and associates it with
|
|
86
|
+
* the currently authenticated user in the application. The registry token is stored
|
|
87
|
+
* in the database for the user's current session.
|
|
88
|
+
*
|
|
89
|
+
* @param string $username the npm username
|
|
90
|
+
* @param string $password the npm password
|
|
91
|
+
* @param string $email the npm email
|
|
92
|
+
*
|
|
93
|
+
* @return RegistryUser the RegistryUser model containing the registry token and associated data
|
|
94
|
+
*
|
|
95
|
+
* @throws \Exception If there is no active session, the .npmrc file is not found, the auth token is not found, or the npm login fails.
|
|
96
|
+
*/
|
|
97
|
+
public static function login(string $username, string $password, string $email): RegistryUser
|
|
98
|
+
{
|
|
99
|
+
// Session is required
|
|
100
|
+
if (session()->missing(['company', 'user'])) {
|
|
101
|
+
throw new \Exception('No active session to create registry token for.');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get registry
|
|
105
|
+
$registry = static::createUrl();
|
|
106
|
+
|
|
107
|
+
// Set .npmrc path
|
|
108
|
+
$npmrcPath = 'tmp/.npmrc-' . session('user');
|
|
109
|
+
|
|
110
|
+
// Create .npmrc file
|
|
111
|
+
Storage::disk('local')->put($npmrcPath, '');
|
|
112
|
+
|
|
113
|
+
// Prepare command
|
|
114
|
+
$process = new Process([
|
|
115
|
+
'npm-cli-login',
|
|
116
|
+
'-u', $username,
|
|
117
|
+
'-p', $password,
|
|
118
|
+
'-e', $email,
|
|
119
|
+
'-r', $registry,
|
|
120
|
+
'-s', 'false',
|
|
121
|
+
'--config-path', storage_path('app/' . $npmrcPath),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
// Set timeout
|
|
125
|
+
$process->setTimeout(60);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Run the process
|
|
129
|
+
$process->mustRun();
|
|
130
|
+
|
|
131
|
+
// Check if .npmrc file exists
|
|
132
|
+
if (!Storage::drive('local')->exists($npmrcPath)) {
|
|
133
|
+
throw new \Exception('.npmrc file not found');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove protocol from registry URL for matching
|
|
137
|
+
$registryHost = preg_replace('/^https?:\/\//', '', $registry);
|
|
138
|
+
|
|
139
|
+
// Read the .npmrc file to get the auth token
|
|
140
|
+
$npmrcContent = Storage::drive('local')->get($npmrcPath);
|
|
141
|
+
$lines = explode("\n", $npmrcContent);
|
|
142
|
+
$authToken = null;
|
|
143
|
+
|
|
144
|
+
foreach ($lines as $line) {
|
|
145
|
+
$line = trim($line);
|
|
146
|
+
if (Str::contains($line, $registryHost) && Str::contains($line, '_authToken=')) {
|
|
147
|
+
$parts = explode('_authToken=', $line);
|
|
148
|
+
if (count($parts) === 2) {
|
|
149
|
+
$authToken = trim($parts[1], ' "');
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Delete .npmrc file
|
|
156
|
+
Storage::drive('local')->delete($npmrcPath);
|
|
157
|
+
|
|
158
|
+
if ($authToken) {
|
|
159
|
+
// Get current authenticated user
|
|
160
|
+
$user = Auth::getUserFromSession();
|
|
161
|
+
|
|
162
|
+
// Create or update registry user for current session
|
|
163
|
+
$registryUser = RegistryUser::updateOrCreate(
|
|
164
|
+
['company_uuid' => session('company'), 'user_uuid' => $user->uuid],
|
|
165
|
+
['registry_token' => $authToken, 'scope' => '*', 'expires_at' => now()->addYear(), 'name' => $user->public_id . ' developer token']
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return $registryUser;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new \Exception('Auth token not found in .npmrc');
|
|
172
|
+
} catch (ProcessFailedException $exception) {
|
|
173
|
+
throw new \Exception('npm login failed: ' . $exception->getMessage());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
53
176
|
}
|
package/server/src/routes.php
CHANGED
|
@@ -13,45 +13,56 @@ use Illuminate\Support\Facades\Route;
|
|
|
13
13
|
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
Route::get('~registry/test', 'Fleetbase\RegistryBridge\Http\Controllers\Internal\v1\RegistryAuthController@test');
|
|
17
|
-
|
|
18
16
|
Route::prefix(config('internals.api.routing.prefix', '~registry'))->middleware(['fleetbase.registry'])->namespace('Fleetbase\RegistryBridge\Http\Controllers')->group(
|
|
19
17
|
function ($router) {
|
|
20
18
|
/*
|
|
21
19
|
* Internal Routes v1
|
|
22
20
|
*/
|
|
23
21
|
$router->group(['prefix' => config('internals.api.routing.internal_prefix', 'v1'), 'namespace' => 'Internal\v1'], function ($router) {
|
|
24
|
-
$router->get('categories', 'RegistryController@categories');
|
|
25
|
-
$router->get('engines', 'RegistryController@getInstalledEngines');
|
|
26
|
-
$router->group(['prefix' => 'installer'], function ($router) {
|
|
27
|
-
$router->post('install', 'ExtensionInstallerController@install');
|
|
28
|
-
$router->post('uninstall', 'ExtensionInstallerController@uninstall');
|
|
29
|
-
});
|
|
30
22
|
$router->group(['prefix' => 'auth'], function ($router) {
|
|
31
|
-
$router->
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
$router->post('
|
|
38
|
-
$router->post('
|
|
39
|
-
$router->
|
|
40
|
-
$router->post('
|
|
41
|
-
$router->post('get-checkout-session', 'RegistryPaymentsController@getStripeCheckoutSessionStatus');
|
|
42
|
-
$router->get('author-received', 'RegistryPaymentsController@getAuthorReceivedPayments');
|
|
43
|
-
});
|
|
44
|
-
$router->fleetbaseRoutes('registry-extensions', function ($router, $controller) {
|
|
45
|
-
$router->post('{id}/submit', $controller('submit'));
|
|
46
|
-
$router->post('approve', $controller('approve'));
|
|
47
|
-
$router->post('reject', $controller('reject'));
|
|
48
|
-
$router->get('download-bundle', $controller('downloadBundle'));
|
|
49
|
-
$router->get('analytics', $controller('analytics'));
|
|
50
|
-
$router->get('installed', $controller('installed'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
51
|
-
$router->get('purchased', $controller('purchased'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
23
|
+
$router->group(['middleware' => ['fleetbase.protected', 'throttle:60,1']], function ($router) {
|
|
24
|
+
$router->get('registry-tokens', 'RegistryAuthController@getRegistryTokens');
|
|
25
|
+
$router->delete('registry-tokens/{id}', 'RegistryAuthController@deleteRegistryToken');
|
|
26
|
+
$router->post('registry-tokens', 'RegistryAuthController@createRegistryUser');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
$router->post('authenticate', 'RegistryAuthController@authenticate')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
30
|
+
$router->post('add-user', 'RegistryAuthController@addUser')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
31
|
+
$router->post('check-access', 'RegistryAuthController@checkAccess')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
32
|
+
$router->post('check-publish', 'RegistryAuthController@checkPublishAllowed')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
52
33
|
});
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
|
|
35
|
+
$router->group(['middleware' => ['fleetbase.protected', 'throttle:60,1']], function ($router) {
|
|
36
|
+
$router->get('categories', 'RegistryController@categories');
|
|
37
|
+
$router->get('engines', 'RegistryController@getInstalledEngines');
|
|
38
|
+
|
|
39
|
+
$router->group(['prefix' => 'installer'], function ($router) {
|
|
40
|
+
$router->post('install', 'ExtensionInstallerController@install');
|
|
41
|
+
$router->post('uninstall', 'ExtensionInstallerController@uninstall');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
$router->group(['prefix' => 'payments'], function ($router) {
|
|
45
|
+
$router->post('account', 'RegistryPaymentsController@getStripeAccount');
|
|
46
|
+
$router->post('account-session', 'RegistryPaymentsController@getStripeAccountSession');
|
|
47
|
+
$router->get('has-stripe-connect-account', 'RegistryPaymentsController@hasStripeConnectAccount');
|
|
48
|
+
$router->post('create-checkout-session', 'RegistryPaymentsController@createStripeCheckoutSession');
|
|
49
|
+
$router->post('get-checkout-session', 'RegistryPaymentsController@getStripeCheckoutSessionStatus');
|
|
50
|
+
$router->get('author-received', 'RegistryPaymentsController@getAuthorReceivedPayments');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
$router->fleetbaseRoutes('registry-extensions', function ($router, $controller) {
|
|
54
|
+
$router->post('{id}/submit', $controller('submit'));
|
|
55
|
+
$router->post('approve', $controller('approve'));
|
|
56
|
+
$router->post('reject', $controller('reject'));
|
|
57
|
+
$router->get('download-bundle', $controller('downloadBundle'));
|
|
58
|
+
$router->get('analytics', $controller('analytics'));
|
|
59
|
+
$router->get('installed', $controller('installed'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
60
|
+
$router->get('purchased', $controller('purchased'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
$router->fleetbaseRoutes('registry-extension-bundles', function ($router, $controller) {
|
|
64
|
+
$router->get('download', $controller('download'));
|
|
65
|
+
});
|
|
55
66
|
});
|
|
56
67
|
});
|
|
57
68
|
}
|