diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index e49923ba..496c1bf3 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -78,7 +78,7 @@ class UserController extends Controller "name" => "required|string|min:4|max:30", "pterodactyl_id" => "required|numeric|unique:users,pterodactyl_id,{$user->id}", "email" => "required|string|email", - "credits" => "required|numeric|min:0|max:999999", + "credits" => "required|numeric|min:0|max:99999999", "server_limit" => "required|numeric|min:0|max:1000000", "role" => Rule::in(['admin', 'mod', 'client', 'member']), ]); diff --git a/app/Http/Controllers/Admin/VoucherController.php b/app/Http/Controllers/Admin/VoucherController.php new file mode 100644 index 00000000..8aaab14f --- /dev/null +++ b/app/Http/Controllers/Admin/VoucherController.php @@ -0,0 +1,196 @@ +validate([ + 'memo' => 'nullable|string|max:191', + 'code' => 'required|string|alpha_dash|max:36|min:4', + 'uses' => 'required|numeric|max:2147483647|min:1', + 'credits' => 'required|numeric|between:0,99999999', + 'expires_at' => ['nullable','date_format:d-m-Y','after:today',"before:10 years"], + ]); + + Voucher::create($request->except('_token')); + + return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been created!'); + } + + /** + * Display the specified resource. + * + * @param Voucher $voucher + * @return Response + */ + public function show(Voucher $voucher) + { + // + } + + /** + * Show the form for editing the specified resource. + * + * @param Voucher $voucher + * @return Application|Factory|View + */ + public function edit(Voucher $voucher) + { + return view('admin.vouchers.edit' , [ + 'voucher' => $voucher + ]); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param Voucher $voucher + * @return RedirectResponse + */ + public function update(Request $request, Voucher $voucher) + { + $request->validate([ + 'memo' => 'nullable|string|max:191', + 'code' => 'required|string|alpha_dash|max:36|min:4', + 'uses' => 'required|numeric|max:2147483647|min:1', + 'credits' => 'required|numeric|between:0,99999999', + 'expires_at' => ['nullable','date_format:d-m-Y','after:today',"before:10 years"], + ]); + + $voucher->update($request->except('_token')); + + return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been updated!'); + } + + /** + * Remove the specified resource from storage. + * + * @param Voucher $voucher + * @return RedirectResponse + */ + public function destroy(Voucher $voucher) + { + $voucher->delete(); + return redirect()->back()->with('success', 'voucher has been removed!'); + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ValidationException + */ + public function redeem(Request $request) + { + #general validations + $request->validate([ + 'code' => 'required|exists:vouchers,code' + ]); + + #get voucher by code + $voucher = Voucher::where('code' , '=' , $request->input('code'))->firstOrFail(); + + #extra validations + if ($voucher->getStatus() == 'USES_LIMIT_REACHED') throw ValidationException::withMessages([ + 'code' => 'This voucher has reached the maximum amount of uses' + ]); + + if ($voucher->getStatus() == 'EXPIRED') throw ValidationException::withMessages([ + 'code' => 'This voucher has expired' + ]); + + if (!$request->user()->vouchers()->where('id' , '=' , $voucher->id)->get()->isEmpty()) throw ValidationException::withMessages([ + 'code' => 'You already redeemed this voucher code' + ]); + + if ($request->user()->credits + $voucher->credits >= 99999999) throw ValidationException::withMessages([ + 'code' => "You can't redeem this voucher because you would exceed the credit limit" + ]); + + #redeem voucher + $voucher->redeem($request->user()); + + return response()->json([ + 'success' => "{$voucher->credits} credits have been added to your balance!" + ]); + } + + public function dataTable() + { + $query = Voucher::query(); + + return datatables($query) + ->addColumn('actions', function (Voucher $voucher) { + return ' + + +
+ ' . csrf_field() . ' + ' . method_field("DELETE") . ' + +
+ '; + }) + ->addColumn('status', function (Voucher $voucher) { + $color = 'success'; + if ($voucher->getStatus() != 'VALID') $color = 'danger'; + return '' . $voucher->getStatus() . ''; + }) + ->editColumn('uses', function (Voucher $voucher) { + $userCount = $voucher->users()->count(); + return "{$userCount} / {$voucher->uses}"; + }) + ->editColumn('credits', function (Voucher $voucher) { + return number_format($voucher->credits, 2, '.', ''); + }) + ->editColumn('expires_at', function (Voucher $voucher) { + if (!$voucher->expires_at) return ""; + return $voucher->expires_at ? $voucher->expires_at->diffForHumans() : ''; + }) + ->editColumn('code', function (Voucher $voucher) { + return "{$voucher->code}"; + }) + ->rawColumns(['actions', 'code', 'status']) + ->make(); + } + +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 18a0d088..d80fb5bf 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Illuminate\Http\Request; class LoginController extends Controller { @@ -37,4 +38,34 @@ class LoginController extends Controller { $this->middleware('guest')->except('logout'); } + + public function login(Request $request) + { + $request->validate([ + $this->username() => 'required|string', + 'password' => 'required|string', + 'g-recaptcha-response' => ['required','recaptcha'], + ]); + + // If the class is using the ThrottlesLogins trait, we can automatically throttle + // the login attempts for this application. We'll key this by the username and + // the IP address of the client making these requests into this application. + if (method_exists($this, 'hasTooManyLoginAttempts') && + $this->hasTooManyLoginAttempts($request)) { + $this->fireLockoutEvent($request); + + return $this->sendLockoutResponse($request); + } + + if ($this->attemptLogin($request)) { + return $this->sendLoginResponse($request); + } + + // If the login attempt was unsuccessful we will increment the number of attempts + // to login and redirect the user back to the login form. Of course, when this + // user surpasses their maximum number of attempts they will get locked out. + $this->incrementLoginAttempts($request); + + return $this->sendFailedLoginResponse($request); + } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 952238bd..bdeb024c 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -20,7 +20,8 @@ class ProfileController extends Controller return view('profile.index')->with([ 'user' => Auth::user(), 'credits_reward_after_verify_discord' => Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_DISCORD'), - 'discord_verify_command' => Configuration::getValueByKey('DISCORD_VERIFY_COMMAND') + 'force_email_verification' => Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION'), + 'force_discord_verification' => Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION'), ]); } diff --git a/app/Models/User.php b/app/Models/User.php index 61605aea..ff4b4df6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,17 +7,30 @@ use App\Notifications\Auth\QueuedVerifyEmail; use App\Notifications\WelcomeMessage; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Spatie\Activitylog\Traits\CausesActivity; use Spatie\Activitylog\Traits\LogsActivity; +/** + * Class User + * @package App\Models + */ class User extends Authenticatable implements MustVerifyEmail { use HasFactory, Notifiable, LogsActivity, CausesActivity; + /** + * @var string[] + */ protected static $logAttributes = ['name', 'email']; + /** + * @var string[] + */ protected static $ignoreChangedAttributes = [ 'remember_token', 'credits', @@ -68,6 +81,9 @@ class User extends Authenticatable implements MustVerifyEmail 'last_seen' => 'datetime', ]; + /** + * + */ public static function boot() { parent::boot(); @@ -89,24 +105,38 @@ class User extends Authenticatable implements MustVerifyEmail } }); + $user->vouchers()->detach(); + Pterodactyl::client()->delete("/application/users/{$user->pterodactyl_id}"); }); } + /** + * + */ public function sendEmailVerificationNotification() { $this->notify(new QueuedVerifyEmail); } + /** + * @return string + */ public function credits() { return number_format($this->credits, 2, '.', ''); } + /** + * @return string + */ public function getAvatar(){ return "https://www.gravatar.com/avatar/" . md5(strtolower(trim($this->email))); } + /** + * @return string + */ public function creditUsage() { $usage = 0; @@ -118,6 +148,9 @@ class User extends Authenticatable implements MustVerifyEmail return number_format($usage, 2, '.', ''); } + /** + * @return array|string|string[] + */ public function getVerifiedStatus(){ $status = ''; if ($this->hasVerifiedEmail()) $status .= 'email '; @@ -126,15 +159,31 @@ class User extends Authenticatable implements MustVerifyEmail return $status; } + /** + * @return BelongsToMany + */ + public function vouchers(){ + return $this->belongsToMany(Voucher::class); + } + + /** + * @return HasOne + */ public function discordUser(){ return $this->hasOne(DiscordUser::class); } + /** + * @return HasMany + */ public function servers() { return $this->hasMany(Server::class); } + /** + * @return HasMany + */ public function payments() { return $this->hasMany(Payment::class); diff --git a/app/Models/Voucher.php b/app/Models/Voucher.php new file mode 100644 index 00000000..bc267c5e --- /dev/null +++ b/app/Models/Voucher.php @@ -0,0 +1,98 @@ +users()->detach(); + }); + } + + /** + * @return BelongsToMany + */ + public function users() + { + return $this->belongsToMany(User::class); + } + + /** + * @return string + */ + public function getStatus() + { + if ($this->users()->count() >= $this->uses) return 'USES_LIMIT_REACHED'; + if (!is_null($this->expires_at)) { + if ($this->expires_at->isPast()) return 'EXPIRED'; + } + + return 'VALID'; + } + + /** + * @param User $user + * @return float + * @throws Exception + */ + public function redeem(User $user) + { + try { + $user->increment('credits', $this->credits); + $this->users()->attach($user); + $this->logRedeem($user); + } catch (Exception $exception) { + throw $exception; + } + + return $this->credits; + } + + /** + * @param User $user + * @return null + */ + private function logRedeem(User $user) + { + activity() + ->performedOn($this) + ->causedBy($user) + ->log('redeemed'); + + return null; + } +} diff --git a/database/factories/VoucherFactory.php b/database/factories/VoucherFactory.php new file mode 100644 index 00000000..9a381296 --- /dev/null +++ b/database/factories/VoucherFactory.php @@ -0,0 +1,34 @@ + $this->faker->word(), + 'code' => Str::random(36), + 'credits' => $this->faker->numberBetween(100, 1000), + 'uses' => $this->faker->numberBetween(1, 1000), + 'expires_at' => now()->addDays($this->faker->numberBetween(1, 90))->format('d-m-Y') + ]; + + } +} diff --git a/database/migrations/2021_07_09_190453_create_vouchers_table.php b/database/migrations/2021_07_09_190453_create_vouchers_table.php new file mode 100644 index 00000000..30a58d92 --- /dev/null +++ b/database/migrations/2021_07_09_190453_create_vouchers_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('code', 36)->unique(); + $table->string('memo')->nullable(); + $table->unsignedFloat('credits', 10); + $table->unsignedInteger('uses')->default(1); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('vouchers'); + } +} diff --git a/database/migrations/2021_07_09_191913_create_user_voucher_table.php b/database/migrations/2021_07_09_191913_create_user_voucher_table.php new file mode 100644 index 00000000..b75f7d26 --- /dev/null +++ b/database/migrations/2021_07_09_191913_create_user_voucher_table.php @@ -0,0 +1,32 @@ +foreignId('user_id')->constrained(); + $table->foreignId('voucher_id')->constrained(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_voucher'); + } +} diff --git a/database/migrations/2021_07_10_062140_update_credits_to_users_table.php b/database/migrations/2021_07_10_062140_update_credits_to_users_table.php new file mode 100644 index 00000000..bce7b344 --- /dev/null +++ b/database/migrations/2021_07_10_062140_update_credits_to_users_table.php @@ -0,0 +1,32 @@ +unsignedFloat('credits', 10)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->unsignedFloat('credits')->change(); + }); + } +} diff --git a/resources/views/admin/activitylogs/index.blade.php b/resources/views/admin/activitylogs/index.blade.php index d617153a..e3cc7946 100644 --- a/resources/views/admin/activitylogs/index.blade.php +++ b/resources/views/admin/activitylogs/index.blade.php @@ -78,6 +78,9 @@ @case('created') @break + @case('redeemed') + + @break @case('deleted') @break diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index 59988edf..0358f189 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -72,7 +72,7 @@
@error('credits') diff --git a/resources/views/admin/vouchers/create.blade.php b/resources/views/admin/vouchers/create.blade.php new file mode 100644 index 00000000..1aca487d --- /dev/null +++ b/resources/views/admin/vouchers/create.blade.php @@ -0,0 +1,181 @@ +@extends('layouts.main') + +@section('content') + +
+
+
+
+

Vouchers

+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+
+ Voucher details +
+
+
+
+ @csrf + +
+ + + @error('memo') +
+ {{$message}} +
+ @enderror +
+ +
+ + + @error('credits') +
+ {{$message}} +
+ @enderror +
+ + +
+ +
+ +
+ +
+
+ @error('code') +
+ {{$message}} +
+ @enderror +
+ +
+ +
+ +
+ +
+
+ @error('uses') +
+ {{$message}} +
+ @enderror +
+ +
+ +
+ +
+
+
+
+ @error('expires_at') +
+ {{$message}} +
+ @enderror +
+ +
+ +
+
+
+
+
+
+ + + +
+
+ + + + + + +@endsection diff --git a/resources/views/admin/vouchers/edit.blade.php b/resources/views/admin/vouchers/edit.blade.php new file mode 100644 index 00000000..467a30de --- /dev/null +++ b/resources/views/admin/vouchers/edit.blade.php @@ -0,0 +1,186 @@ +@extends('layouts.main') + +@section('content') + +
+
+
+
+

Vouchers

+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+
+ Voucher details +
+
+
+
+ @csrf + @method('PATCH') + +
+ + + @error('memo') +
+ {{$message}} +
+ @enderror +
+ +
+ + + @error('credits') +
+ {{$message}} +
+ @enderror +
+ + +
+ +
+ +
+ +
+
+ @error('code') +
+ {{$message}} +
+ @enderror +
+ +
+ +
+ +
+ +
+
+ @error('uses') +
+ {{$message}} +
+ @enderror +
+ +
+ +
+ +
+
+
+
+ @error('expires_at') +
+ {{$message}} +
+ @enderror +
+ + +
+ +
+
+
+
+
+
+ +
+
+ + + + + + +@endsection diff --git a/resources/views/admin/vouchers/index.blade.php b/resources/views/admin/vouchers/index.blade.php new file mode 100644 index 00000000..bfe4cef6 --- /dev/null +++ b/resources/views/admin/vouchers/index.blade.php @@ -0,0 +1,94 @@ +@extends('layouts.main') + +@section('content') + +
+
+
+
+

Vouchers

+
+
+ +
+
+
+
+ + + +
+
+ +
+ +
+
+
Vouchers
+ Create new +
+
+ +
+ + + + + + + + + + + + + + + +
StatusCodeMemoCreditsUsed / UsesExpires
+ +
+
+ + +
+ + +
+ + + + + + +@endsection diff --git a/resources/views/admin/vouchers/show.blade.php b/resources/views/admin/vouchers/show.blade.php new file mode 100644 index 00000000..46d13b5d --- /dev/null +++ b/resources/views/admin/vouchers/show.blade.php @@ -0,0 +1,240 @@ +@extends('layouts.main') + +@section('content') + +
+
+
+
+

Products

+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
Product
+
+ +
+ {{ csrf_field() }} + {{ method_field("DELETE") }} + +
+
+
+
+
+ +
+
+
+ +
+
+ + {{$product->id}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->name}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->price}} + +
+
+
+ + +
+
+
+ +
+
+ + {{$product->memory}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->cpu}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->swap}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->disk}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->io}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->databases}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->allocations}} + +
+
+
+ +
+
+
+ +
+
+ + {{$product->created_at ? $product->created_at->diffForHumans() : ''}} + +
+
+
+ + +
+
+
+ +
+
+ + {{$product->description}} + +
+
+
+ + +
+
+
+ +
+
+ + {{$product->updated_at ? $product->updated_at->diffForHumans() : ''}} + +
+
+
+ +
+
+
+ +
+
+
Servers
+
+
+ + @include('admin.servers.table' , ['filter' => '?product=' . $product->id]) + +
+
+ + +
+ +
+ + + + + +@endsection diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 2e3f4953..420d1b85 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -54,6 +54,15 @@ @enderror +
+ {!! htmlFormSnippet() !!} + @error('g-recaptcha-response') + + {{ $message }} + + @enderror +
+
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 44d73ba4..8f47bbf2 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -117,6 +117,9 @@ @case('created') @break + @case('redeemed') + + @break @case('deleted') @break diff --git a/resources/views/layouts/main.blade.php b/resources/views/layouts/main.blade.php index 5cfa4957..c9ff4bfd 100644 --- a/resources/views/layouts/main.blade.php +++ b/resources/views/layouts/main.blade.php @@ -16,6 +16,9 @@ {{-- summernote --}} + {{-- datetimepicker --}} + + @@ -96,6 +99,11 @@ Log back in @endif + + + Redeem code +
@csrf @@ -146,13 +154,15 @@ - + @if(env('PAYPAL_SECRET') && env('PAYPAL_CLIENT_ID') || env('APP_ENV', 'local') == 'local') + + @endif @if(Auth::user()->role == 'admin') @@ -189,6 +199,14 @@ + + + + + + + - - - @endif @@ -276,7 +296,7 @@
@if(!Auth::user()->hasVerifiedEmail()) - @if(Auth::user()->created_at->diffInHours(now(), false) > 2) + @if(Auth::user()->created_at->diffInHours(now(), false) > 1)
Warning!
You have not yet verified your email address