diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..5dbe639 --- /dev/null +++ b/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,47 @@ +validated($request->all()); + + if (! Auth::attempt($request->only('email', 'password'))) { + return $this->error(__('Invalid credentials'), 401); + } + + $user = User::firstWhere('email', $request->email); + $tokenName = $request->token_name ?? __('Dynamic API Token'); + + return $this->success(__('Authenticated'), [ + // Only allowing the 'read' scope to be dynamically created. Can revisit later when writes are possible. + 'token' => $user->createToken($tokenName, ['read'])->plainTextToken, + ]); + } + + public function logout(Request $request): JsonResponse + { + $request->user()->currentAccessToken()->delete(); + + return $this->success(__('Revoked API token')); + } + + public function logoutAll(Request $request): JsonResponse + { + $request->user()->tokens()->delete(); + + return $this->success(__('Revoked all API tokens')); + } +} diff --git a/app/Http/Controllers/Api/V0/ModController.php b/app/Http/Controllers/Api/V0/ModController.php new file mode 100644 index 0000000..2711ce1 --- /dev/null +++ b/app/Http/Controllers/Api/V0/ModController.php @@ -0,0 +1,52 @@ + ['required', 'string', 'email'], + 'password' => ['required', 'string'], + 'token_name' => ['sometimes'], + ]; + } +} diff --git a/app/Http/Requests/Api/V0/StoreModRequest.php b/app/Http/Requests/Api/V0/StoreModRequest.php new file mode 100644 index 0000000..69bf403 --- /dev/null +++ b/app/Http/Requests/Api/V0/StoreModRequest.php @@ -0,0 +1,26 @@ + 'mod', + 'id' => $this->id, + 'attributes' => [ + 'name' => $this->name, + 'slug' => $this->slug, + 'description' => $this->when( + $request->routeIs('api.v0.mods.show'), + $this->description + ), + 'source_code_link' => $this->source_code_link, + 'user_id' => $this->user_id, + 'license_id' => $this->license_id, + 'created_at' => $this->created_at, + ], + 'relationships' => [ + 'user' => [ + 'data' => [ + 'type' => 'user', + 'id' => $this->user_id, + ], + // TODO: Provide 'links.self' to user profile: + //'links' => ['self' => '#'], + ], + 'license' => [ + 'data' => [ + 'type' => 'license', + 'id' => $this->license_id, + ], + ], + ], + 'included' => [ + new UserResource($this->user), + // TODO: Provide 'included' data for attached 'license': + //new LicenseResource($this->license), + ], + 'links' => [ + 'self' => route('mod.show', [ + 'mod' => $this->id, + 'slug' => $this->slug, + ]), + ], + ]; + } +} diff --git a/app/Http/Resources/Api/V0/UserResource.php b/app/Http/Resources/Api/V0/UserResource.php new file mode 100644 index 0000000..f4e2e68 --- /dev/null +++ b/app/Http/Resources/Api/V0/UserResource.php @@ -0,0 +1,40 @@ + 'user', + 'id' => $this->id, + 'attributes' => [ + 'name' => $this->name, + 'user_role_id' => $this->user_role_id, + 'created_at' => $this->created_at, + ], + 'relationships' => [ + 'user_role' => [ + 'data' => [ + 'type' => 'user_role', + 'id' => $this->user_role_id, + ], + ], + ], + // TODO: Provide 'included' data for attached 'user_role' + //'included' => [new UserRoleResource($this->role)], + + // TODO: Provide 'links.self' to user profile: + //'links' => ['self' => '#'], + ]; + } +} diff --git a/app/Traits/ApiResponses.php b/app/Traits/ApiResponses.php new file mode 100644 index 0000000..f26781d --- /dev/null +++ b/app/Traits/ApiResponses.php @@ -0,0 +1,26 @@ +baseResponse(message: $message, data: $data, code: 200); + } + + private function baseResponse(?string $message = '', ?array $data = [], ?int $code = 200): JsonResponse + { + return response()->json([ + 'message' => $message, + 'data' => $data, + ], $code); + } + + protected function error(string $message, int $code): JsonResponse + { + return $this->baseResponse(message: $message, code: $code); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c2e983d..9c47482 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Support\Facades\Route; use Mchev\Banhammer\Middleware\IPBanned; return Application::configure(basePath: dirname(__DIR__)) @@ -11,6 +12,12 @@ return Application::configure(basePath: dirname(__DIR__)) api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', + then: function () { + Route::middleware('api') + ->prefix('api/v0') + ->name('api.v0.') + ->group(base_path('routes/api_v0.php')); + }, ) ->withMiddleware(function (Middleware $middleware) { $middleware->append(IPBanned::class); diff --git a/routes/api.php b/routes/api.php index ccc387f..add5b2a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,14 @@ user(); -})->middleware('auth:sanctum'); +Route::get('/status', function () { + return response()->json(['message' => 'okay']); +}); + +Route::post('/login', [AuthController::class, 'login']); +Route::group(['middleware' => 'auth:sanctum'], function () { + Route::delete('/logout', [AuthController::class, 'logout']); + Route::delete('/logout/all', [AuthController::class, 'logoutAll']); +}); diff --git a/routes/api_v0.php b/routes/api_v0.php new file mode 100644 index 0000000..aa4bd6c --- /dev/null +++ b/routes/api_v0.php @@ -0,0 +1,10 @@ + 'auth:sanctum'], function () { + Route::apiResource('users', UsersController::class); + Route::apiResource('mods', ModController::class); +});