@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.
@@ -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
- {{outlet}}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/registry-bridge",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Internal Bridge between Fleetbase API and Extensions Registry",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Registry Bridge",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Internal Bridge between Fleetbase API and Extensions Registry",
5
5
  "repository": "https://github.com/fleetbase/registry-bridge",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/registry-bridge-engine",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Internal Bridge between Fleetbase API and Extensions Registry",
5
5
  "fleetbase": {
6
6
  "route": "extensions"
@@ -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
- // Get identity
133
- $identity = $request->input('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
- // Find user by email or username
136
- $user = User::where('email', $identity)->orWhere('username', $identity)->first();
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
- // If user is not admin respond with error
139
- if (!$user->isAdmin()) {
140
- return response()->error('User is not allowed access to the registry.', 401);
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
- if (!$user->isAdmin()) {
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' => ['required', 'valid_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
  }
@@ -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->post('authenticate', 'RegistryAuthController@authenticate');
32
- $router->post('add-user', 'RegistryAuthController@addUser');
33
- $router->post('check-access', 'RegistryAuthController@checkAccess');
34
- $router->post('check-publish', 'RegistryAuthController@checkPublishAllowed');
35
- });
36
- $router->group(['prefix' => 'payments'], function ($router) {
37
- $router->post('account', 'RegistryPaymentsController@getStripeAccount');
38
- $router->post('account-session', 'RegistryPaymentsController@getStripeAccountSession');
39
- $router->get('has-stripe-connect-account', 'RegistryPaymentsController@hasStripeConnectAccount');
40
- $router->post('create-checkout-session', 'RegistryPaymentsController@createStripeCheckoutSession');
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
- $router->fleetbaseRoutes('registry-extension-bundles', function ($router, $controller) {
54
- $router->get('download', $controller('download'));
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
  }