refactor: improve security, split routes, add API resources and FormRequests

- Fix timing attack vulnerability in AuthController
- Split web.php (316 lines) into 7 focused route files
- Add 8 API Resources for consistent response formatting
- Add 8 FormRequest classes for centralized validation
- Use Resources instead of manual array mapping in controllers
This commit is contained in:
root
2026-05-20 23:03:16 +02:00
parent 2f30a058a4
commit 75b78c17fa
26 changed files with 745 additions and 404 deletions
+45 -100
View File
@@ -1,26 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Actions\Fortify\CreateNewUser;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\ArticleCommentRequest;
use App\Http\Requests\Api\LoginRequest;
use App\Http\Requests\Api\RegisterRequest;
use App\Http\Requests\Api\UpdatePasswordRequest;
use App\Http\Requests\Api\UpdateUserRequest;
use App\Http\Resources\Api\ArticleResource;
use App\Http\Resources\Api\PhotoResource;
use App\Http\Resources\Api\UserApiResource;
use App\Models\Articles\WebsiteArticle;
use App\Models\Miscellaneous\CameraWeb;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function login(Request $request): JsonResponse
public function login(LoginRequest $request): JsonResponse
{
$request->validate([
'username' => ['required', 'string'],
'password' => ['required'],
]);
$username = $request->input('username');
$user = User::where('username', $username)
->orWhere('mail', $username)
@@ -29,16 +33,16 @@ class AuthController extends Controller
$credentialsValid = $user && Hash::check($request->input('password'), $user->password);
if (! $credentialsValid) {
Hash::make($request->input('password'));
Hash::check($request->input('password'), Hash::make('timing-attack-prevention'));
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
'username' => ['The provided credentials are incorrect.'],
]);
}
if ($user->is_banned) {
throw ValidationException::withMessages([
'email' => ['Your account has been banned.'],
'username' => ['Your account has been banned.'],
]);
}
@@ -47,66 +51,31 @@ class AuthController extends Controller
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => [
'id' => (string) $user->id,
'email' => $user->mail,
'username' => $user->username,
'look' => $user->look,
],
'user' => new UserApiResource($user),
'token' => $token,
]);
}
public function register(Request $request): JsonResponse
public function register(RegisterRequest $request): JsonResponse
{
$createNewUser = new CreateNewUser;
try {
$validated = $request->validate([
'username' => ['required', 'string', 'max:50'],
'password' => ['required', 'string', 'min:6'],
'mail' => ['required', 'email', 'max:255'],
'look' => ['nullable', 'string'],
'motto' => ['nullable', 'string', 'max:100'],
]);
$user = $createNewUser->create($request->validated());
$user = $createNewUser->create($validated);
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'user' => [
'id' => (string) $user->id,
'email' => $user->mail,
'username' => $user->username,
'look' => $user->look,
],
'token' => $token,
], 201);
} catch (ValidationException $e) {
return response()->json([
'errors' => $e->errors(),
], 422);
}
}
public function user(Request $request): JsonResponse
{
$user = $request->user();
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'id' => (string) $user->id,
'email' => $user->mail,
'username' => $user->username,
'look' => $user->look,
'motto' => $user->motto ?? '',
'credits' => $user->credits ?? 0,
'pixels' => $user->pixels ?? 0,
'diamonds' => $user->diamonds ?? 0,
]);
'user' => new UserApiResource($user),
'token' => $token,
], 201);
}
public function logout(Request $request): JsonResponse
public function user(\Illuminate\Http\Request $request): JsonResponse
{
return response()->json(new UserApiResource($request->user()));
}
public function logout(\Illuminate\Http\Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
@@ -118,69 +87,45 @@ class AuthController extends Controller
$articles = WebsiteArticle::with(['user:id,username,look'])
->latest('id')
->take(4)
->get()
->map(fn ($article) => [
'id' => $article->id,
'title' => $article->title,
'slug' => $article->slug,
'image' => $article->image,
'excerpt' => $article->excerpt,
'user' => $article->user,
'created_at' => $article->created_at,
]);
->get();
$photos = CameraWeb::query()
->latest('id')
->take(4)
->where('visible', true)
->with('user:id,username,look')
->get()
->map(fn ($photo) => [
'id' => $photo->id,
'image' => $photo->image,
'user' => $photo->user,
]);
->get();
return response()->json([
'articles' => $articles,
'photos' => $photos,
'articles' => ArticleResource::collection($articles),
'photos' => PhotoResource::collection($photos),
]);
}
public function updateUser(Request $request): JsonResponse
public function updateUser(UpdateUserRequest $request): JsonResponse
{
$user = $request->user();
$user->update($request->validated());
$validated = $request->validate([
'motto' => ['nullable', 'string', 'max:100'],
'look' => ['nullable', 'string'],
]);
$user->update($validated);
return response()->json([
'id' => (string) $user->id,
'email' => $user->mail,
'username' => $user->username,
'look' => $user->look,
'motto' => $user->motto,
'credits' => $user->credits,
'pixels' => $user->pixels,
'diamonds' => $user->diamonds,
]);
return response()->json(new UserApiResource($user));
}
public function articleComment(Request $request, string $slug): JsonResponse
public function updatePassword(UpdatePasswordRequest $request): JsonResponse
{
$request->user()->update([
'password' => Hash::make($request->input('password')),
]);
return response()->json(['message' => 'Password updated successfully']);
}
public function articleComment(ArticleCommentRequest $request, string $slug): JsonResponse
{
$article = WebsiteArticle::where('slug', $slug)->firstOrFail();
$validated = $request->validate([
'comment' => ['required', 'string', 'max:1000'],
]);
$comment = $article->comments()->create([
'user_id' => $request->user()->id,
'comment' => strip_tags((string) $validated['comment']),
'comment' => strip_tags((string) $request->input('comment')),
]);
return response()->json([
+27 -37
View File
@@ -1,8 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\HelpTicketReplyRequest;
use App\Http\Requests\Api\HelpTicketRequest;
use App\Http\Requests\Api\PhotoUploadRequest;
use App\Http\Resources\Api\ArticleResource;
use App\Http\Resources\Api\HelpTicketResource;
use App\Http\Resources\Api\LeaderboardUserResource;
use App\Http\Resources\Api\PhotoResource;
use App\Http\Resources\Api\ShopPackageResource;
use App\Http\Resources\Api\UserApiResource;
use App\Http\Resources\Api\UserBriefResource;
use App\Models\Articles\WebsiteArticle;
use App\Models\Game\Furniture\CatalogItem;
use App\Models\Game\Furniture\CatalogPage;
@@ -61,7 +73,7 @@ class HotelApiController extends Controller
->paginate(12);
return response()->json([
'data' => $articles->items(),
'data' => ArticleResource::collection($articles),
'meta' => [
'current_page' => $articles->currentPage(),
'last_page' => $articles->lastPage(),
@@ -78,7 +90,7 @@ class HotelApiController extends Controller
->firstOrFail();
return response()->json([
'data' => $article,
'data' => new ArticleResource($article),
]);
}
@@ -90,7 +102,7 @@ class HotelApiController extends Controller
->paginate(12);
return response()->json([
'data' => $photos->items(),
'data' => PhotoResource::collection($photos),
'meta' => [
'current_page' => $photos->currentPage(),
'last_page' => $photos->lastPage(),
@@ -113,20 +125,8 @@ class HotelApiController extends Controller
{
$packages = WebsiteShopArticle::latest('id')->paginate(12);
$mapped = $packages->items()->map(fn ($pkg) => [
'id' => $pkg->id,
'title' => $pkg->name,
'description' => $pkg->description,
'price' => $pkg->price(),
'credits' => null,
'pixels' => null,
'diamonds' => null,
'image' => null,
'currency' => 'credits',
]);
return response()->json([
'data' => $mapped,
'data' => ShopPackageResource::collection($packages),
'meta' => [
'current_page' => $packages->currentPage(),
'last_page' => $packages->lastPage(),
@@ -175,7 +175,7 @@ class HotelApiController extends Controller
->get(['id', 'username', 'look', 'motto', 'credits', 'pixels']);
return response()->json([
'data' => $users,
'data' => LeaderboardUserResource::collection($users),
'type' => $type,
]);
}
@@ -249,7 +249,7 @@ class HotelApiController extends Controller
->paginate(10);
return response()->json([
'data' => $tickets->items(),
'data' => HelpTicketResource::collection($tickets),
'meta' => [
'current_page' => $tickets->currentPage(),
'last_page' => $tickets->lastPage(),
@@ -264,16 +264,12 @@ class HotelApiController extends Controller
->where('id', $id)
->firstOrFail();
return response()->json(['data' => $ticket]);
return response()->json(['data' => new HelpTicketResource($ticket)]);
}
public function helpTicketCreate(Request $request): JsonResponse
public function helpTicketCreate(HelpTicketRequest $request): JsonResponse
{
$validated = $request->validate([
'subject' => ['required', 'string', 'max:200'],
'category' => ['required', 'string', 'max:100'],
'message' => ['required', 'string', 'max:5000'],
]);
$validated = $request->validated();
$ticket = WebsiteHelpCenterTicket::create([
'user_id' => $request->user()->id,
@@ -287,32 +283,26 @@ class HotelApiController extends Controller
'message' => $validated['message'],
]);
return response()->json(['data' => $ticket], 201);
return response()->json(['data' => new HelpTicketResource($ticket)], 201);
}
public function helpTicketReply(Request $request, string $id): JsonResponse
public function helpTicketReply(HelpTicketReplyRequest $request, string $id): JsonResponse
{
$validated = $request->validate(['message' => 'required', 'string', 'max:5000']);
$ticket = WebsiteHelpCenterTicket::where('id', $id)
->where('user_id', $request->user()->id)
->firstOrFail();
$reply = $ticket->replies()->create([
'user_id' => $request->user()->id,
'message' => $validated['message'],
'message' => $request->input('message'),
]);
return response()->json(['data' => $reply->load('user:id,username,look')], 201);
}
public function uploadPhoto(Request $request): JsonResponse
public function uploadPhoto(PhotoUploadRequest $request): JsonResponse
{
$validated = $request->validate([
'image' => ['required', 'image', 'max:5120'],
]);
$path = $validated['image']->store('photos', 'public');
$path = $request->file('image')->store('photos', 'public');
$photo = CameraWeb::create([
'user_id' => $request->user()->id,
@@ -320,7 +310,7 @@ class HotelApiController extends Controller
'visible' => true,
]);
return response()->json(['data' => $photo], 201);
return response()->json(['data' => new PhotoResource($photo)], 201);
}
public function purchasePackage(Request $request, int $packageId): JsonResponse
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use App\Rules\WebsiteWordfilterRule;
use Illuminate\Foundation\Http\FormRequest;
class ArticleCommentRequest extends FormRequest
{
public function rules(): array
{
return [
'comment' => ['required', 'string', 'max:1000', new WebsiteWordfilterRule],
];
}
public function authorize(): bool
{
return true;
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class HelpTicketReplyRequest extends FormRequest
{
public function rules(): array
{
return [
'message' => ['required', 'string', 'max:5000'],
];
}
public function authorize(): bool
{
return true;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class HelpTicketRequest extends FormRequest
{
public function rules(): array
{
return [
'subject' => ['required', 'string', 'max:200'],
'category' => ['required', 'string', 'max:100'],
'message' => ['required', 'string', 'max:5000'],
];
}
public function authorize(): bool
{
return true;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => ['required', 'string'],
'password' => ['required'],
];
}
public function authorize(): bool
{
return true;
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class PhotoUploadRequest extends FormRequest
{
public function rules(): array
{
return [
'image' => ['required', 'image', 'max:5120'],
];
}
public function authorize(): bool
{
return true;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'username' => ['required', 'string', 'max:50'],
'password' => ['required', 'string', 'min:6'],
'mail' => ['required', 'email', 'max:255'],
'look' => ['nullable', 'string'],
'motto' => ['nullable', 'string', 'max:100'],
];
}
public function authorize(): bool
{
return true;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class UpdatePasswordRequest extends FormRequest
{
public function rules(): array
{
return [
'current_password' => ['required', 'current_password'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
];
}
public function authorize(): bool
{
return true;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'motto' => ['nullable', 'string', 'max:100'],
'look' => ['nullable', 'string'],
];
}
public function authorize(): bool
{
return true;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ArticleResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'image' => $this->image,
'excerpt' => $this->excerpt,
'user' => $this->whenLoaded('user', fn () => new UserBriefResource($this->user)),
'created_at' => $this->created_at,
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class HelpTicketReplyResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'ticket_id' => $this->ticket_id,
'user_id' => $this->user_id,
'message' => $this->message,
'user' => $this->whenLoaded('user', fn () => new UserBriefResource($this->user)),
'created_at' => $this->created_at,
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class HelpTicketResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'user_id' => $this->user_id,
'subject' => $this->subject,
'category' => $this->category,
'status' => $this->status,
'user' => $this->whenLoaded('user', fn () => new UserBriefResource($this->user)),
'replies' => $this->whenLoaded('replies', fn () => HelpTicketReplyResource::collection($this->replies)),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class LeaderboardUserResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'username' => $this->username,
'look' => $this->look,
'motto' => $this->motto,
'credits' => $this->credits,
'pixels' => $this->pixels,
];
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PhotoResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'image' => $this->image,
'user' => $this->whenLoaded('user', fn () => new UserBriefResource($this->user)),
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ShopPackageResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->name,
'description' => $this->description,
'price' => $this->price(),
'credits' => null,
'pixels' => null,
'diamonds' => null,
'image' => null,
'currency' => 'credits',
];
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserApiResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'email' => $this->mail,
'username' => $this->username,
'look' => $this->look,
'motto' => $this->motto ?? '',
'credits' => $this->credits ?? 0,
'pixels' => $this->pixels ?? 0,
'diamonds' => $this->diamonds ?? 0,
];
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserBriefResource extends JsonResource
{
#[\Override]
public function toArray(Request $request): array
{
return [
'id' => (string) $this->id,
'username' => $this->username,
'look' => $this->look,
];
}
}