diff --git a/app/Http/Controllers/Api/NotificationController.php b/app/Http/Controllers/Api/NotificationController.php index ceecbb24..9fe61c79 100644 --- a/app/Http/Controllers/Api/NotificationController.php +++ b/app/Http/Controllers/Api/NotificationController.php @@ -6,11 +6,13 @@ use App\Http\Controllers\Controller; use App\Models\DiscordUser; use App\Models\User; use App\Notifications\DynamicNotification; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Notification; use Illuminate\Support\HtmlString; +use Illuminate\Validation\ValidationException; use Spatie\ValidationRules\Rules\Delimited; class NotificationController extends Controller @@ -55,19 +57,21 @@ class NotificationController extends Controller * * @param Request $request * @return JsonResponse + * @throws ValidationException */ public function send(Request $request) { $data = $request->validate([ "via" => ["required", new Delimited("in:mail,database")], "all" => "required_without:users|boolean", - "users" => ["required_without:all", new Delimited("exists:users,id")], + "users" => ["required_without:all"], "title" => "required|string|min:1", "content" => "required|string|min:1" ]); $via = explode(",", $data["via"]); $mail = null; $database = null; + if (in_array("database", $via)) { $database = [ "title" => $data["title"], @@ -79,10 +83,28 @@ class NotificationController extends Controller ->subject($data["title"]) ->line(new HtmlString($data["content"])); } + $all = $data["all"] ?? false; - $users = $all ? User::all() : User::whereIn("id", explode(",", $data["users"]))->get(); + if ($all) { + $users = User::all(); + } else { + $userIds = explode(",", $data["users"]); + $users = User::query() + ->whereIn("id", $userIds) + ->orWhereHas('discordUser', function (Builder $builder) use ($userIds) { + $builder->whereIn('id', $userIds); + }) + ->get(); + } + + if ($users->count() == 0) { + throw ValidationException::withMessages([ + 'users' => ['No users found!'], + ]); + } + Notification::send($users, new DynamicNotification($via, $database, $mail)); - return response()->json(["message" => "Notification successfully sent."]); + return response()->json(["message" => "Notification successfully sent.", 'user_count' => $users->count()]); } /** diff --git a/app/Http/Controllers/Api/ServerController.php b/app/Http/Controllers/Api/ServerController.php index fcfa1b14..566bec71 100644 --- a/app/Http/Controllers/Api/ServerController.php +++ b/app/Http/Controllers/Api/ServerController.php @@ -6,11 +6,16 @@ use App\Http\Controllers\Controller; use App\Models\Server; use Exception; use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Spatie\QueryBuilder\QueryBuilder; class ServerController extends Controller { + const ALLOWED_INCLUDES = ['product', 'user']; + const ALLOWED_FILTERS = ['name', 'suspended', 'identifier', 'pterodactyl_id', 'user_id', 'product_id']; + /** * Display a listing of the resource. * @@ -19,21 +24,27 @@ class ServerController extends Controller */ public function index(Request $request) { - return Server::with('product')->paginate($request->query('per_page') ?? 50); - } + $query = QueryBuilder::for(Server::class) + ->allowedIncludes(self::ALLOWED_INCLUDES) + ->allowedFilters(self::ALLOWED_FILTERS); + return $query->paginate($request->input('per_page') ?? 50); + } /** * Display the specified resource. * * @param Server $server - * @return Server + * @return Server|Collection */ public function show(Server $server) { - return $server->load('product'); - } + $query = QueryBuilder::for(Server::class) + ->where('id', '=', $server->id) + ->allowedIncludes(self::ALLOWED_INCLUDES); + return $query->get(); + } /** * Remove the specified resource from storage. diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index a5f7263c..fbf0c0e8 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -2,28 +2,44 @@ namespace App\Http\Controllers\Api; +use App\Classes\Pterodactyl; use App\Events\UserUpdateCreditsEvent; use App\Http\Controllers\Controller; +use App\Models\Configuration; use App\Models\DiscordUser; use App\Models\User; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Routing\ResponseFactory; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; +use Spatie\QueryBuilder\QueryBuilder; class UserController extends Controller { + const ALLOWED_INCLUDES = ['servers', 'notifications', 'payments', 'vouchers', 'discordUser']; + const ALLOWED_FILTERS = ['name', 'server_limit', 'email', 'pterodactyl_id', 'role', 'suspended']; + /** * Display a listing of the resource. * * @param Request $request - * @return Response + * @return LengthAwarePaginator */ public function index(Request $request) { - return User::paginate($request->query('per_page') ?? 50); + $query = QueryBuilder::for(User::class) + ->allowedIncludes(self::ALLOWED_INCLUDES) + ->allowedFilters(self::ALLOWED_FILTERS); + + return $query->paginate($request->input('per_page') ?? 50); } @@ -31,12 +47,22 @@ class UserController extends Controller * Display the specified resource. * * @param int $id - * @return User + * @return User|Collection */ public function show(int $id) { $discordUser = DiscordUser::find($id); - return $discordUser ? $discordUser->user : User::findOrFail($id); + $user = $discordUser ? $discordUser->user : User::findOrFail($id); + + $query = QueryBuilder::for($user) + ->with('discordUser') + ->allowedIncludes(self::ALLOWED_INCLUDES) + ->where('users.id', '=', $id) + ->orWhereHas('discordUser', function (Builder $builder) use ($id) { + $builder->where('id', '=', $id); + }); + + return $query->get(); } @@ -53,11 +79,11 @@ class UserController extends Controller $user = $discordUser ? $discordUser->user : User::findOrFail($id); $request->validate([ - "name" => "sometimes|string|min:4|max:30", - "email" => "sometimes|string|email", - "credits" => "sometimes|numeric|min:0|max:1000000", + "name" => "sometimes|string|min:4|max:30", + "email" => "sometimes|string|email", + "credits" => "sometimes|numeric|min:0|max:1000000", "server_limit" => "sometimes|numeric|min:0|max:1000000", - "role" => ['sometimes', Rule::in(['admin', 'mod', 'client', 'member'])], + "role" => ['sometimes', Rule::in(['admin', 'mod', 'client', 'member'])], ]); $user->update($request->all()); @@ -81,23 +107,23 @@ class UserController extends Controller $user = $discordUser ? $discordUser->user : User::findOrFail($id); $request->validate([ - "credits" => "sometimes|numeric|min:0|max:1000000", + "credits" => "sometimes|numeric|min:0|max:1000000", "server_limit" => "sometimes|numeric|min:0|max:1000000", ]); - if($request->credits){ - if ($user->credits + $request->credits >= 99999999) throw ValidationException::withMessages([ + if ($request->credits) { + if ($user->credits + $request->credits >= 99999999) throw ValidationException::withMessages([ 'credits' => "You can't add this amount of credits because you would exceed the credit limit" ]); event(new UserUpdateCreditsEvent($user)); $user->increment('credits', $request->credits); - } + } - if($request->server_limit){ + if ($request->server_limit) { if ($user->server_limit + $request->server_limit >= 2147483647) throw ValidationException::withMessages([ 'server_limit' => "You cannot add this amount of servers because it would exceed the server limit." ]); - $user->increment('server_limit', $request->server_limit); + $user->increment('server_limit', $request->server_limit); } return $user; @@ -117,27 +143,72 @@ class UserController extends Controller $user = $discordUser ? $discordUser->user : User::findOrFail($id); $request->validate([ - "credits" => "sometimes|numeric|min:0|max:1000000", + "credits" => "sometimes|numeric|min:0|max:1000000", "server_limit" => "sometimes|numeric|min:0|max:1000000", ]); - if($request->credits){ - if($user->credits - $request->credits < 0) throw ValidationException::withMessages([ + if ($request->credits) { + if ($user->credits - $request->credits < 0) throw ValidationException::withMessages([ 'credits' => "You can't remove this amount of credits because you would exceed the minimum credit limit" ]); $user->decrement('credits', $request->credits); - } + } - if($request->server_limit){ - if($user->server_limit - $request->server_limit < 0) throw ValidationException::withMessages([ + if ($request->server_limit) { + if ($user->server_limit - $request->server_limit < 0) throw ValidationException::withMessages([ 'server_limit' => "You cannot remove this amount of servers because it would exceed the minimum server." ]); - $user->decrement('server_limit', $request->server_limit); + $user->decrement('server_limit', $request->server_limit); } return $user; } + /** + * @throws ValidationException + */ + public function store(Request $request) + { + $request->validate([ + 'name' => ['required', 'string', 'max:30', 'min:4', 'alpha_num', 'unique:users'], + 'email' => ['required', 'string', 'email', 'max:64', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'max:191'], + ]); + + $user = User::create([ + 'name' => $request->input('name'), + 'email' => $request->input('email'), + 'credits' => Configuration::getValueByKey('INITIAL_CREDITS', 150), + 'server_limit' => Configuration::getValueByKey('INITIAL_SERVER_LIMIT', 1), + 'password' => Hash::make($request->input('password')), + ]); + + $response = Pterodactyl::client()->post('/application/users', [ + "external_id" => App::environment('local') ? Str::random(16) : (string)$user->id, + "username" => $user->name, + "email" => $user->email, + "first_name" => $user->name, + "last_name" => $user->name, + "password" => $request->input('password'), + "root_admin" => false, + "language" => "en" + ]); + + if ($response->failed()) { + $user->delete(); + throw ValidationException::withMessages([ + 'pterodactyl_error_message' => $response->toException()->getMessage(), + 'pterodactyl_error_status' => $response->toException()->getCode() + ]); + } + + $user->update([ + 'pterodactyl_id' => $response->json()['attributes']['id'] + ]); + + return $user; + } + /** * Remove the specified resource from storage. * diff --git a/app/Http/Controllers/Api/VoucherController.php b/app/Http/Controllers/Api/VoucherController.php index f7acfdd5..69f0176f 100644 --- a/app/Http/Controllers/Api/VoucherController.php +++ b/app/Http/Controllers/Api/VoucherController.php @@ -5,20 +5,29 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Voucher; use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Validation\Rule; +use Spatie\QueryBuilder\QueryBuilder; class VoucherController extends Controller { + const ALLOWED_INCLUDES = ['users']; + const ALLOWED_FILTERS = ['code', 'memo', 'credits', 'uses']; + /** * Display a listing of the resource. * - * @return Response + * @return LengthAwarePaginator */ public function index(Request $request) { - return Voucher::paginate($request->query('per_page') ?? 50); + $query = QueryBuilder::for(Voucher::class) + ->allowedIncludes(self::ALLOWED_INCLUDES) + ->allowedFilters(self::ALLOWED_FILTERS); + + return $query->paginate($request->input('per_page') ?? 50); } /** @@ -40,10 +49,10 @@ class VoucherController extends Controller public function store(Request $request) { $request->validate([ - 'memo' => 'nullable|string|max:191', - 'code' => 'required|string|alpha_dash|max:36|min:4|unique:vouchers', - 'uses' => 'required|numeric|max:2147483647|min:1', - 'credits' => 'required|numeric|between:0,99999999', + 'memo' => 'nullable|string|max:191', + 'code' => 'required|string|alpha_dash|max:36|min:4|unique:vouchers', + 'uses' => 'required|numeric|max:2147483647|min:1', + 'credits' => 'required|numeric|between:0,99999999', 'expires_at' => 'nullable|multiple_date_format:d-m-Y H:i:s,d-m-Y|after:now|before:10 years' ]); @@ -54,11 +63,15 @@ class VoucherController extends Controller * Display the specified resource. * * @param int $id - * @return Response + * @return Collection|Voucher */ public function show(int $id) { - return Voucher::findOrFail($id); + $query = QueryBuilder::for(Voucher::class) + ->where('id', '=', $id) + ->allowedIncludes(self::ALLOWED_INCLUDES); + + return $query->get(); } /** @@ -84,10 +97,10 @@ class VoucherController extends Controller $voucher = Voucher::findOrFail($id); $request->validate([ - 'memo' => 'nullable|string|max:191', - 'code' => "required|string|alpha_dash|max:36|min:4|unique:vouchers,code,{$voucher->id}", - 'uses' => 'required|numeric|max:2147483647|min:1', - 'credits' => 'required|numeric|between:0,99999999', + 'memo' => 'nullable|string|max:191', + 'code' => "required|string|alpha_dash|max:36|min:4|unique:vouchers,code,{$voucher->id}", + 'uses' => 'required|numeric|max:2147483647|min:1', + 'credits' => 'required|numeric|between:0,99999999', 'expires_at' => 'nullable|multiple_date_format:d-m-Y H:i:s,d-m-Y|after:now|before:10 years' ]); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8aa705e0..189f64bf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -29,7 +29,6 @@ class AppServiceProvider extends ServiceProvider { Paginator::useBootstrap(); Schema::defaultStringLength(191); - QueryBuilderRequest::setArrayValueDelimiter('|'); Validator::extend('multiple_date_format', function ($attribute, $value, $parameters, $validator) { diff --git a/composer.json b/composer.json index 3edec1b4..50dc100a 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "paypal/rest-api-sdk-php": "^1.14", "socialiteproviders/discord": "^4.1", "spatie/laravel-activitylog": "^3.16", - "spatie/laravel-query-builder": "^3.5", + "spatie/laravel-query-builder": "^3.6", "spatie/laravel-validation-rules": "^3.0", "yajra/laravel-datatables-oracle": "~9.0" }, diff --git a/composer.lock b/composer.lock index 14abe8de..0ff1f168 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b3b61a46d5d4d6560d052cfda863d12c", + "content-hash": "f7ba581ff6641d3ab79d558070e99f3c", "packages": [ { "name": "asm89/stack-cors", @@ -3462,16 +3462,16 @@ }, { "name": "spatie/laravel-query-builder", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-query-builder.git", - "reference": "4e5257be24139836dc092f618d7c73bcb1c00302" + "reference": "03d8e1307dcb58b16fcc9c4947633fc60ae74802" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/4e5257be24139836dc092f618d7c73bcb1c00302", - "reference": "4e5257be24139836dc092f618d7c73bcb1c00302", + "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/03d8e1307dcb58b16fcc9c4947633fc60ae74802", + "reference": "03d8e1307dcb58b16fcc9c4947633fc60ae74802", "shasum": "" }, "require": { @@ -3528,7 +3528,7 @@ "type": "custom" } ], - "time": "2021-07-05T14:17:44+00:00" + "time": "2021-09-06T08:03:10+00:00" }, { "name": "spatie/laravel-validation-rules", @@ -8713,5 +8713,5 @@ "ext-intl": "*" }, "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/config/query-builder.php b/config/query-builder.php new file mode 100644 index 00000000..e9f81270 --- /dev/null +++ b/config/query-builder.php @@ -0,0 +1,46 @@ + [ + 'include' => 'include', + + 'filter' => 'filter', + + 'sort' => 'sort', + + 'fields' => 'fields', + + 'append' => 'append', + ], + + /* + * Related model counts are included using the relationship name suffixed with this string. + * For example: GET /users?include=postsCount + */ + 'count_suffix' => 'Count', + + /* + * By default the package will throw an `InvalidFilterQuery` exception when a filter in the + * URL is not allowed in the `allowedFilters()` method. + */ + 'disable_invalid_filter_query_exception' => false, + + /* + * By default the package inspects query string of request using $request->query(). + * You can change this behavior to inspect the request body using $request->input() + * by setting this value to `body`. + * + * Possible values: `query_string`, `body` + */ + 'request_data_source' => 'query_string', +]; diff --git a/routes/api.php b/routes/api.php index c964be38..bf9bc7d0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,7 +20,7 @@ use Illuminate\Support\Facades\Route; Route::middleware('api.token')->group(function () { Route::patch('/users/{user}/increment', [UserController::class, 'increment']); Route::patch('/users/{user}/decrement', [UserController::class, 'decrement']); - Route::resource('users', UserController::class)->except(['store', 'create']); + Route::resource('users', UserController::class)->except(['create']); Route::patch('/servers/{server}/suspend', [ServerController::class, 'suspend']); Route::patch('/servers/{server}/unsuspend', [ServerController::class, 'unSuspend']);