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 @@ +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 @@ +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 @@ + 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 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 @@ +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//licenses/) + */ + 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/) */