Browse Source

Personal information and new route-based authorization

moell 1 year ago
parent
commit
592219223a

+ 43 - 0
app/Console/Commands/InitializeRoutePermission.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Permission;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Route;
+
+class InitializeRoutePermission extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'lpc:initialize-route-permission';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Initialize routing permissions';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $routes = Route::getRoutes();
+
+        foreach ($routes as $route) {
+            if (! $route->getName()) {
+                continue;
+            }
+
+            Permission::query()->firstOrCreate([
+                'name' => $route->getName(),
+                'guard_name' => 'api'
+            ]);
+        }
+    }
+}

+ 18 - 0
app/Http/Controllers/API/UserController.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Resources\API\UserInfoResource;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class UserController extends Controller
+{
+    public function info()
+    {
+        $user = Auth::user();
+
+        return new UserInfoResource($user);
+    }
+}

+ 2 - 0
app/Http/Kernel.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Http;
 namespace App\Http;
 
 
+use App\Http\Middleware\CheckPermission;
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
 
 class Kernel extends HttpKernel
 class Kernel extends HttpKernel
@@ -64,5 +65,6 @@ class Kernel extends HttpKernel
         'signed' => \App\Http\Middleware\ValidateSignature::class,
         'signed' => \App\Http\Middleware\ValidateSignature::class,
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+        'permission' => CheckPermission::class,
     ];
     ];
 }
 }

+ 29 - 0
app/Http/Middleware/CheckPermission.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Route;
+use Spatie\Permission\Exceptions\UnauthorizedException;
+use Symfony\Component\HttpFoundation\Response;
+
+class CheckPermission
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        $permission = Route::currentRouteName();
+
+        if (Auth::user()->hasPermissionTo($permission)) {
+            return $next($request);
+        }
+
+        throw UnauthorizedException::forPermissions([$permission]);
+    }
+}

+ 2 - 0
app/Http/Resources/API/RoleResource.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Http\Resources\API;
 namespace App\Http\Resources\API;
 
 
+use App\Http\Resources\PermissionResource;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\JsonResource;
 
 
@@ -18,6 +19,7 @@ class RoleResource extends JsonResource
             'id' => $this->id,
             'id' => $this->id,
             'name' => $this->name,
             'name' => $this->name,
             'description' => $this->description,
             'description' => $this->description,
+            'permissions' => PermissionResource::collection($this->permissions),
         ];
         ];
     }
     }
 }
 }

+ 22 - 0
app/Http/Resources/API/SimpleCompanyResource.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class SimpleCompanyResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+        ];
+    }
+}

+ 26 - 0
app/Http/Resources/API/UserInfoResource.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class UserInfoResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+            'email' => $this->email,
+            'username' => $this->username,
+            'company' => new SimpleCompanyResource($this->company),
+            'role' => new RoleResource($this->role),
+        ];
+    }
+}

+ 24 - 0
app/Http/Resources/PermissionResource.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class PermissionResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+            'description' => $this->description,
+            'default_check' => $this->default_check,
+        ];
+    }
+}

+ 13 - 0
app/Models/Company.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Company extends Model
+{
+    use HasFactory;
+
+    protected $table = 'company';
+}

+ 1 - 0
app/Models/Role.php

@@ -4,6 +4,7 @@ namespace App\Models;
 
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
 
 class Role extends \Spatie\Permission\Models\Role
 class Role extends \Spatie\Permission\Models\Role
 {
 {

+ 17 - 0
app/Models/User.php

@@ -13,6 +13,8 @@ class User extends Authenticatable
 {
 {
     use HasApiTokens, HasFactory, Notifiable, HasRoles;
     use HasApiTokens, HasFactory, Notifiable, HasRoles;
 
 
+    protected string $guard_name = 'api';
+
     /**
     /**
      * The attributes that are mass assignable.
      * The attributes that are mass assignable.
      *
      *
@@ -45,4 +47,19 @@ class User extends Authenticatable
         'email_verified_at' => 'datetime',
         'email_verified_at' => 'datetime',
         'password' => 'hashed',
         'password' => 'hashed',
     ];
     ];
+
+    public function guardName(): string
+    {
+        return $this->guard_name;
+    }
+
+    public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(Company::class);
+    }
+
+    public function role(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(Role::class);
+    }
 }
 }

+ 28 - 0
database/migrations/2024_02_04_065924_add_role_id_to_users_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->integer("role_id")->nullable();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn("role_id");
+        });
+    }
+};

+ 35 - 37
routes/api.php

@@ -15,45 +15,43 @@ use App\Http\Controllers\API;
 |
 |
 */
 */
 
 
-
-Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
-    return $request->user();
-});
-
 Route::post("/login", [API\AuthController::class, "login"]);
 Route::post("/login", [API\AuthController::class, "login"]);
 
 
 Route::middleware(['auth:sanctum'])->group(function () {
 Route::middleware(['auth:sanctum'])->group(function () {
     Route::post("/logout", [API\AuthController::class, "logout"]);
     Route::post("/logout", [API\AuthController::class, "logout"]);
-
-    Route::apiResources([
-        'asset-group' => API\AssetGroupController::class,
-        'asset' => API\AssetController::class,
-        'requirement-group' => API\RequirementGroupController::class,
-        'requirement' => API\RequirementController::class,
-        'project' => API\ProjectController::class,
-        'plan' => API\PlanController::class,
-        'role' => API\RoleController::class,
-    ]);
-
-    Route::get("requirement/{asset_id}/asset", [API\RequirementController::class, "byAsset"])->name("requirement.byAsset");
-    Route::patch("requirement/{plan_id}/plan", [API\RequirementController::class, "linkPlan"])->name("requirement.linkPlan");
-
-    Route::patch("project/{project}/closed", [API\ProjectController::class, "closed"])->name("project.closed");
-    Route::patch("project/{project}/start", [API\ProjectController::class, "start"])->name("project.start");
-    Route::patch("project/{project}/pause", [API\ProjectController::class, "pause"])->name("project.pause");
-    Route::patch("project/{project}/postpone", [API\ProjectController::class, "postpone"])
-        ->name("project.postpone");
-    Route::patch("project/{project}/link-requirement", [API\ProjectController::class, "linkRequirement"])
-        ->name("project.link-requirement");
-    Route::patch("project/{project}/unlink-requirement", [API\ProjectController::class, "unlinkRequirement"])
-        ->name("project.unlink-requirement");
-    Route::patch("project/{project}/link-requirement-by-plan", [API\ProjectController::class, "linkRequirementByPlan"])
-        ->name("project.link-requirement-by-plan");
-
-    Route::get("project/{project}/plan", [API\ProjectController::class, "plan"])
-        ->name("project.plan"); //项目关联计划
-    Route::get("project/{project}/requirement", [API\ProjectController::class, "requirement"])
-        ->name("project.requirement"); //项目需求
-    Route::get("project/{project}/not-link-asset-requirement", [API\ProjectController::class, "notLinkAssetRequirement"])
-        ->name("project.not-link-asset-requirement"); //项目未关联的资产需求
+    Route::get("user/info", [API\UserController::class, 'info'])->name("user.info");
+
+    Route::middleware(['permission'])->group(function() {
+        Route::apiResources([
+            'asset-group' => API\AssetGroupController::class,
+            'asset' => API\AssetController::class,
+            'requirement-group' => API\RequirementGroupController::class,
+            'requirement' => API\RequirementController::class,
+            'project' => API\ProjectController::class,
+            'plan' => API\PlanController::class,
+            'role' => API\RoleController::class,
+        ]);
+
+        Route::get("requirement/{asset_id}/asset", [API\RequirementController::class, "byAsset"])->name("requirement.byAsset");
+        Route::patch("requirement/{plan_id}/plan", [API\RequirementController::class, "linkPlan"])->name("requirement.linkPlan");
+
+        Route::patch("project/{project}/closed", [API\ProjectController::class, "closed"])->name("project.closed");
+        Route::patch("project/{project}/start", [API\ProjectController::class, "start"])->name("project.start");
+        Route::patch("project/{project}/pause", [API\ProjectController::class, "pause"])->name("project.pause");
+        Route::patch("project/{project}/postpone", [API\ProjectController::class, "postpone"])
+            ->name("project.postpone");
+        Route::patch("project/{project}/link-requirement", [API\ProjectController::class, "linkRequirement"])
+            ->name("project.link-requirement");
+        Route::patch("project/{project}/unlink-requirement", [API\ProjectController::class, "unlinkRequirement"])
+            ->name("project.unlink-requirement");
+        Route::patch("project/{project}/link-requirement-by-plan", [API\ProjectController::class, "linkRequirementByPlan"])
+            ->name("project.link-requirement-by-plan");
+
+        Route::get("project/{project}/plan", [API\ProjectController::class, "plan"])
+            ->name("project.plan"); //项目关联计划
+        Route::get("project/{project}/requirement", [API\ProjectController::class, "requirement"])
+            ->name("project.requirement"); //项目需求
+        Route::get("project/{project}/not-link-asset-requirement", [API\ProjectController::class, "notLinkAssetRequirement"])
+            ->name("project.not-link-asset-requirement"); //项目未关联的资产需求
+    });
 });
 });

+ 24 - 0
tests/Feature/API/UserTest.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace API;
+
+
+use Tests\Feature\TestCase;
+
+class UserTest extends TestCase
+{
+   public function test_user_info(): void
+   {
+       $response = $this->get(route("user.info"));
+
+       $response->assertStatus(200)->assertJsonStructure([
+           'data' => [
+               'id',
+               'name',
+               'email',
+               'company',
+               'role',
+           ]
+       ]);
+   }
+}

+ 14 - 0
tests/Feature/TestCase.php

@@ -3,9 +3,12 @@
 namespace Tests\Feature;
 namespace Tests\Feature;
 
 
 
 
+use App\Models\Permission;
+use App\Models\Role;
 use App\Models\User;
 use App\Models\User;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Foundation\Testing\WithFaker;
 use Illuminate\Foundation\Testing\WithFaker;
+use Illuminate\Support\Facades\Artisan;
 use Laravel\Sanctum\Sanctum;
 use Laravel\Sanctum\Sanctum;
 
 
 class TestCase extends \Tests\TestCase
 class TestCase extends \Tests\TestCase
@@ -23,6 +26,17 @@ class TestCase extends \Tests\TestCase
         ]);
         ]);
 
 
         Sanctum::actingAs($user);
         Sanctum::actingAs($user);
+
+        Artisan::call("lpc:initialize-route-permission");
+
+        $role = Role::factory()->create();
+
+        $user->role_id = $role->id;
+        $user->save();
+
+        $role->syncPermissions(Permission::query()->pluck('name')->toArray());
+
+        $user->assignRole([$role->id]);
     }
     }
 
 
     protected function simplePaginateResponseStructure(array $data): array
     protected function simplePaginateResponseStructure(array $data): array