Add ranks and ACP user enhancements
This commit is contained in:
@@ -12,7 +12,9 @@ class PostController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Post::query()->withoutTrashed()->with('user');
|
||||
$query = Post::query()->withoutTrashed()->with([
|
||||
'user' => fn ($query) => $query->withCount('posts')->with('rank'),
|
||||
]);
|
||||
|
||||
$threadParam = $request->query('thread');
|
||||
if (is_string($threadParam)) {
|
||||
@@ -46,7 +48,9 @@ class PostController extends Controller
|
||||
'body' => $data['body'],
|
||||
]);
|
||||
|
||||
$post->loadMissing('user');
|
||||
$post->loadMissing([
|
||||
'user' => fn ($query) => $query->withCount('posts')->with('rank'),
|
||||
]);
|
||||
|
||||
return response()->json($this->serializePost($post), 201);
|
||||
}
|
||||
@@ -88,6 +92,14 @@ class PostController extends Controller
|
||||
'user_avatar_url' => $post->user?->avatar_path
|
||||
? Storage::url($post->user->avatar_path)
|
||||
: null,
|
||||
'user_posts_count' => $post->user?->posts_count,
|
||||
'user_created_at' => $post->user?->created_at?->toIso8601String(),
|
||||
'user_rank_name' => $post->user?->rank?->name,
|
||||
'user_rank_badge_type' => $post->user?->rank?->badge_type,
|
||||
'user_rank_badge_text' => $post->user?->rank?->badge_text,
|
||||
'user_rank_badge_url' => $post->user?->rank?->badge_image_path
|
||||
? Storage::url($post->user->rank->badge_image_path)
|
||||
: null,
|
||||
'created_at' => $post->created_at?->toIso8601String(),
|
||||
'updated_at' => $post->updated_at?->toIso8601String(),
|
||||
];
|
||||
|
||||
153
app/Http/Controllers/RankController.php
Normal file
153
app/Http/Controllers/RankController.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Rank;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class RankController extends Controller
|
||||
{
|
||||
private function ensureAdmin(Request $request): ?JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user || !$user->roles()->where('name', 'ROLE_ADMIN')->exists()) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$ranks = Rank::query()
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn (Rank $rank) => [
|
||||
'id' => $rank->id,
|
||||
'name' => $rank->name,
|
||||
'badge_type' => $rank->badge_type,
|
||||
'badge_text' => $rank->badge_text,
|
||||
'badge_image_url' => $rank->badge_image_path
|
||||
? Storage::url($rank->badge_image_path)
|
||||
: null,
|
||||
]);
|
||||
|
||||
return response()->json($ranks);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
if ($error = $this->ensureAdmin($request)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100', 'unique:ranks,name'],
|
||||
'badge_type' => ['nullable', 'in:text,image'],
|
||||
'badge_text' => ['nullable', 'string', 'max:40'],
|
||||
]);
|
||||
|
||||
$badgeType = $data['badge_type'] ?? 'text';
|
||||
$badgeText = $badgeType === 'text'
|
||||
? ($data['badge_text'] ?? $data['name'])
|
||||
: null;
|
||||
|
||||
$rank = Rank::create([
|
||||
'name' => $data['name'],
|
||||
'badge_type' => $badgeType,
|
||||
'badge_text' => $badgeText,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'id' => $rank->id,
|
||||
'name' => $rank->name,
|
||||
'badge_type' => $rank->badge_type,
|
||||
'badge_text' => $rank->badge_text,
|
||||
'badge_image_url' => null,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, Rank $rank): JsonResponse
|
||||
{
|
||||
if ($error = $this->ensureAdmin($request)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100', "unique:ranks,name,{$rank->id}"],
|
||||
'badge_type' => ['nullable', 'in:text,image'],
|
||||
'badge_text' => ['nullable', 'string', 'max:40'],
|
||||
]);
|
||||
|
||||
$badgeType = $data['badge_type'] ?? $rank->badge_type ?? 'text';
|
||||
$badgeText = $badgeType === 'text'
|
||||
? ($data['badge_text'] ?? $rank->badge_text ?? $data['name'])
|
||||
: null;
|
||||
|
||||
if ($badgeType === 'text' && $rank->badge_image_path) {
|
||||
Storage::disk('public')->delete($rank->badge_image_path);
|
||||
$rank->badge_image_path = null;
|
||||
}
|
||||
|
||||
$rank->update([
|
||||
'name' => $data['name'],
|
||||
'badge_type' => $badgeType,
|
||||
'badge_text' => $badgeText,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'id' => $rank->id,
|
||||
'name' => $rank->name,
|
||||
'badge_type' => $rank->badge_type,
|
||||
'badge_text' => $rank->badge_text,
|
||||
'badge_image_url' => $rank->badge_image_path
|
||||
? Storage::url($rank->badge_image_path)
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Rank $rank): JsonResponse
|
||||
{
|
||||
if ($error = $this->ensureAdmin($request)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
if ($rank->badge_image_path) {
|
||||
Storage::disk('public')->delete($rank->badge_image_path);
|
||||
}
|
||||
|
||||
$rank->delete();
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
|
||||
public function uploadBadgeImage(Request $request, Rank $rank): JsonResponse
|
||||
{
|
||||
if ($error = $this->ensureAdmin($request)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'file' => ['required', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:2048'],
|
||||
]);
|
||||
|
||||
if ($rank->badge_image_path) {
|
||||
Storage::disk('public')->delete($rank->badge_image_path);
|
||||
}
|
||||
|
||||
$path = $data['file']->store('rank-badges', 'public');
|
||||
$rank->badge_type = 'image';
|
||||
$rank->badge_text = null;
|
||||
$rank->badge_image_path = $path;
|
||||
$rank->save();
|
||||
|
||||
return response()->json([
|
||||
'id' => $rank->id,
|
||||
'badge_type' => $rank->badge_type,
|
||||
'badge_text' => $rank->badge_text,
|
||||
'badge_image_url' => Storage::url($path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ class ThreadController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Thread::query()->withoutTrashed()->with('user');
|
||||
$query = Thread::query()->withoutTrashed()->with([
|
||||
'user' => fn ($query) => $query->withCount('posts')->with('rank'),
|
||||
]);
|
||||
|
||||
$forumParam = $request->query('forum');
|
||||
if (is_string($forumParam)) {
|
||||
@@ -32,7 +34,9 @@ class ThreadController extends Controller
|
||||
|
||||
public function show(Thread $thread): JsonResponse
|
||||
{
|
||||
$thread->loadMissing('user');
|
||||
$thread->loadMissing([
|
||||
'user' => fn ($query) => $query->withCount('posts')->with('rank'),
|
||||
]);
|
||||
return response()->json($this->serializeThread($thread));
|
||||
}
|
||||
|
||||
@@ -99,6 +103,14 @@ class ThreadController extends Controller
|
||||
'user_avatar_url' => $thread->user?->avatar_path
|
||||
? Storage::url($thread->user->avatar_path)
|
||||
: null,
|
||||
'user_posts_count' => $thread->user?->posts_count,
|
||||
'user_created_at' => $thread->user?->created_at?->toIso8601String(),
|
||||
'user_rank_name' => $thread->user?->rank?->name,
|
||||
'user_rank_badge_type' => $thread->user?->rank?->badge_type,
|
||||
'user_rank_badge_text' => $thread->user?->rank?->badge_text,
|
||||
'user_rank_badge_url' => $thread->user?->rank?->badge_image_path
|
||||
? Storage::url($thread->user->rank->badge_image_path)
|
||||
: null,
|
||||
'created_at' => $thread->created_at?->toIso8601String(),
|
||||
'updated_at' => $thread->updated_at?->toIso8601String(),
|
||||
];
|
||||
|
||||
@@ -6,13 +6,15 @@ use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$users = User::query()
|
||||
->with('roles')
|
||||
->with(['roles', 'rank'])
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (User $user) => [
|
||||
@@ -20,6 +22,10 @@ class UserController extends Controller
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'rank' => $user->rank ? [
|
||||
'id' => $user->rank->id,
|
||||
'name' => $user->rank->name,
|
||||
] : null,
|
||||
'roles' => $user->roles->pluck('name')->values(),
|
||||
]);
|
||||
|
||||
@@ -39,6 +45,10 @@ class UserController extends Controller
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'rank' => $user->rank ? [
|
||||
'id' => $user->rank->id,
|
||||
'name' => $user->rank->name,
|
||||
] : null,
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
]);
|
||||
}
|
||||
@@ -49,10 +59,94 @@ class UserController extends Controller
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'rank' => $user->rank ? [
|
||||
'id' => $user->rank->id,
|
||||
'name' => $user->rank->name,
|
||||
] : null,
|
||||
'created_at' => $user->created_at?->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateRank(Request $request, User $user): JsonResponse
|
||||
{
|
||||
$actor = $request->user();
|
||||
if (!$actor || !$actor->roles()->where('name', 'ROLE_ADMIN')->exists()) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'rank_id' => ['nullable', 'exists:ranks,id'],
|
||||
]);
|
||||
|
||||
$user->rank_id = $data['rank_id'] ?? null;
|
||||
$user->save();
|
||||
|
||||
$user->loadMissing('rank');
|
||||
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'rank' => $user->rank ? [
|
||||
'id' => $user->rank->id,
|
||||
'name' => $user->rank->name,
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, User $user): JsonResponse
|
||||
{
|
||||
$actor = $request->user();
|
||||
if (!$actor || !$actor->roles()->where('name', 'ROLE_ADMIN')->exists()) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users', 'email')->ignore($user->id),
|
||||
],
|
||||
'rank_id' => ['nullable', 'exists:ranks,id'],
|
||||
]);
|
||||
|
||||
$nameCanonical = Str::lower(trim($data['name']));
|
||||
$nameConflict = User::query()
|
||||
->where('id', '!=', $user->id)
|
||||
->where('name_canonical', $nameCanonical)
|
||||
->exists();
|
||||
|
||||
if ($nameConflict) {
|
||||
return response()->json(['message' => 'Name already exists.'], 422);
|
||||
}
|
||||
|
||||
if ($data['email'] !== $user->email) {
|
||||
$user->email_verified_at = null;
|
||||
}
|
||||
|
||||
$user->forceFill([
|
||||
'name' => $data['name'],
|
||||
'name_canonical' => $nameCanonical,
|
||||
'email' => $data['email'],
|
||||
'rank_id' => $data['rank_id'] ?? null,
|
||||
])->save();
|
||||
|
||||
$user->loadMissing('rank');
|
||||
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar_url' => $this->resolveAvatarUrl($user),
|
||||
'rank' => $user->rank ? [
|
||||
'id' => $user->rank->id,
|
||||
'name' => $user->rank->name,
|
||||
] : null,
|
||||
'roles' => $user->roles()->pluck('name')->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAvatarUrl(User $user): ?string
|
||||
{
|
||||
if (!$user->avatar_path) {
|
||||
|
||||
28
app/Models/Rank.php
Normal file
28
app/Models/Rank.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, User> $users
|
||||
*/
|
||||
class Rank extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'badge_type',
|
||||
'badge_text',
|
||||
'badge_image_path',
|
||||
];
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use Database\Factories\UserFactory;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\DatabaseNotification;
|
||||
@@ -66,6 +67,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'name',
|
||||
'name_canonical',
|
||||
'avatar_path',
|
||||
'rank_id',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
@@ -97,4 +99,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
public function posts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
|
||||
public function rank()
|
||||
{
|
||||
return $this->belongsTo(Rank::class);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user