Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117792892
D4938.1775259480.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
D4938.1775259480.diff
View Options
diff --git a/src/app/Console/Commands/Data/Import/LicensesCommand.php b/src/app/Console/Commands/Data/Import/LicensesCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/LicensesCommand.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\License;
+use App\Console\Command;
+
+class LicensesCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:licenses {type} {file} {--tenant=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Imports licenses from a file.';
+
+ /** @var bool Adds --tenant option handler */
+ protected $withTenant = true;
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ $file = $this->argument('file');
+ $type = $this->argument('type');
+
+ if (!file_exists($file)) {
+ $this->error("File '$file' does not exist");
+ return 1;
+ }
+
+ $list = file($file);
+
+ if (empty($list)) {
+ $this->error("File '$file' is empty");
+ return 1;
+ }
+
+ $list = array_map('trim', $list);
+
+ $bar = $this->createProgressBar(count($list), "Importing license keys");
+
+ // Import licenses
+ foreach ($list as $key) {
+ License::create([
+ 'key' => $key,
+ 'type' => $type,
+ 'tenant_id' => $this->tenantId,
+ ]);
+
+ $bar->advance();
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+}
diff --git a/src/app/Console/Commands/LicensesCommand.php b/src/app/Console/Commands/LicensesCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/LicensesCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Console\ObjectListCommand;
+
+/**
+ * List licenses.
+ */
+class LicensesCommand extends ObjectListCommand
+{
+ protected $objectClass = \App\License::class;
+ protected $objectName = 'license';
+}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\RelationController;
use App\Domain;
+use App\License;
use App\Plan;
use App\Rules\Password;
use App\Rules\UserEmailDomain;
@@ -110,6 +111,64 @@
return response()->json($result);
}
+ /**
+ * Get a license information.
+ *
+ * @param string $id The account to get licenses for
+ * @param string $type License type
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function licenses(string $id, string $type)
+ {
+ $user = User::find($id);
+
+ if (!$this->checkTenant($user)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canRead($user)) {
+ return $this->errorResponse(403);
+ }
+
+ $licenses = $user->licenses()->where('type', $type)->orderBy('created_at')->get();
+
+ // No licenses for the user, take one if available
+ if (!count($licenses)) {
+ DB::beginTransaction();
+
+ $license = License::withObjectTenantContext($user)
+ ->where('type', $type)
+ ->whereNull('user_id')
+ ->limit(1)
+ ->lockForUpdate()
+ ->first();
+
+ if ($license) {
+ $license->user_id = $user->id;
+ $license->save();
+
+ $licenses = \collect([$license]);
+ }
+
+ DB::commit();
+ }
+
+ // Slim down the result set
+ $licenses = $licenses->map(function ($license) {
+ return [
+ 'key' => $license->key,
+ 'type' => $license->type,
+ ];
+ });
+
+ return response()->json([
+ 'list' => $licenses,
+ 'count' => count($licenses),
+ 'hasMore' => false, // TODO
+ ]);
+ }
+
/**
* Display information on the user account specified by $id.
*
diff --git a/src/app/License.php b/src/app/License.php
new file mode 100644
--- /dev/null
+++ b/src/app/License.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace App;
+
+use App\Traits\BelongsToTenantTrait;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a License
+ *
+ * @property int $id The license identifier
+ * @property string $key The license key
+ * @property string $type An email address
+ * @property int $tenant_id Tenant identifier
+ * @property ?int $user_id User identifier
+ */
+class License extends Model
+{
+ use BelongsToTenantTrait;
+
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = ['key', 'type', 'tenant_id'];
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -422,6 +422,16 @@
return ($this->status & self::STATUS_RESTRICTED) > 0;
}
+ /**
+ * Licenses whis user has.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function licenses()
+ {
+ return $this->hasMany(License::class);
+ }
+
/**
* A shortcut to get the user name.
*
diff --git a/src/database/migrations/2024_09_13_100000_create_licenses_table.php b/src/database/migrations/2024_09_13_100000_create_licenses_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2024_09_13_100000_create_licenses_table.php
@@ -0,0 +1,45 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'licenses',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('user_id')->nullable()->index();
+ $table->bigInteger('tenant_id')->unsigned()->nullable()->index();
+ $table->string('type', 16);
+ $table->string('key', 255);
+ $table->timestamps();
+
+ $table->unique(['type', 'key']);
+
+ $table->foreign('user_id')->references('id')->on('users')
+ ->onDelete('cascade')->onUpdate('cascade');
+ $table->foreign('tenant_id')->references('id')->on('tenants')
+ ->onDelete('set null')->onUpdate('cascade');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('licenses');
+ }
+};
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -164,6 +164,8 @@
Route::post('users/{id}/config', [API\V4\UsersController::class, 'setConfig']);
Route::get('users/{id}/skus', [API\V4\UsersController::class, 'skus']);
Route::get('users/{id}/status', [API\V4\UsersController::class, 'status']);
+ Route::get('users/{id}/licenses/{type}', [API\V4\UsersController::class, 'licenses']);
+
Route::apiResource('wallets', API\V4\WalletsController::class);
Route::get('wallets/{id}/transactions', [API\V4\WalletsController::class, 'transactions']);
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -5,6 +5,7 @@
use App\Discount;
use App\Domain;
use App\Http\Controllers\API\V4\UsersController;
+use App\License;
use App\Package;
use App\Plan;
use App\Sku;
@@ -290,6 +291,68 @@
// TODO: Test paging
}
+ /**
+ * Test fetching licenses for a user (GET /users/<id>/licenses/<type>)
+ */
+ public function testLicenses(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $user->licenses()->delete();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/users/{$user->id}/licenses/test");
+ $response->assertStatus(401);
+
+ // Access forbidden
+ $response = $this->actingAs($jack)->get("api/v4/users/{$user->id}/licenses/test");
+ $response->assertStatus(403);
+
+ $license = License::create([
+ 'key' => (string) microtime(true),
+ 'type' => 'test',
+ 'tenant_id' => $user->tenant_id,
+ ]);
+
+ // Unknow type
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/licenses/unknown");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(0, $json['list']);
+ $this->assertSame(0, $json['count']);
+ $this->assertFalse($json['hasMore']);
+
+ // Valid type, existing license - expect license assignment
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/licenses/test");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json['list']);
+ $this->assertSame(1, $json['count']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($license->key, $json['list'][0]['key']);
+ $this->assertSame($license->type, $json['list'][0]['type']);
+
+ $license->refresh();
+ $this->assertEquals($user->id, $license->user_id);
+
+ // Try again with assigned license
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/licenses/test");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json['list']);
+ $this->assertSame(1, $json['count']);
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame($license->key, $json['list'][0]['key']);
+ $this->assertSame($license->type, $json['list'][0]['type']);
+ $this->assertEquals($user->id, $license->user_id);
+ }
+
/**
* Test fetching user data/profile (GET /api/v4/users/<user-id>)
*/
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 11:38 PM (12 m, 10 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823203
Default Alt Text
D4938.1775259480.diff (10 KB)
Attached To
Mode
D4938: User licenses
Attached
Detach File
Event Timeline