diff --git a/app/Http/Controllers/Admin/AdminController.php b/app/Http/Controllers/Admin/AdminController.php new file mode 100644 index 00000000..67eac5e9 --- /dev/null +++ b/app/Http/Controllers/Admin/AdminController.php @@ -0,0 +1,155 @@ +middleware('moderator'); + } + + public function fetchUser($identifier) + { + $user = null; + if (is_numeric($identifier)) { + $user = User::find($identifier); + } elseif (filter_var($identifier, FILTER_VALIDATE_EMAIL)) { + $user = User::whereEmail($identifier)->first(); + } else { + // Find by form slug + $form = Form::whereSlug($identifier)->first(); + if ($form) { + $user = $form->creator; + } + } + + if (!$user) { + return $this->error([ + 'message' => 'User not found.' + ]); + } elseif ($user->admin) { + return $this->error([ + 'message' => 'You cannot fetch an admin.' + ]); + } + + return $this->success([ + 'user' => $user + ]); + } + + public function applyDiscount(Request $request) + { + $request->validate([ + 'user_id' => 'required' + ]); + $user = User::find($request->get("user_id")); + + $activeSubscriptions = $user->subscriptions()->where(function ($q) { + $q->where('stripe_status', 'trialing') + ->orWhere('stripe_status', 'active'); + })->get(); + + if ($activeSubscriptions->count() != 1) { + return $this->error([ + "message" => "The user has more than one active subscriptions or doesn't have one." + ]); + } + + $couponId = config('pricing.discount_coupon_id'); + if (is_null($couponId)) { + return $this->error([ + "message" => "Coupon id not defined." + ]); + } + + $subscription = $activeSubscriptions->first(); + Cashier::stripe()->subscriptions->update($subscription->stripe_id, [ + 'coupon' => $couponId + ]); + + \Log::warning(self::ADMIN_LOG_PREFIX . 'Applying NGO/Student discount to sub', [ + 'user_id' => $user->id, + 'subcription_id' => $subscription->id, + 'coupon_id' => $couponId, + 'subscription_stripe_id' => $subscription->stripe_id, + 'moderator_id' => auth()->id(), + ]); + + return $this->success([ + "message" => "40% Discount applied for the next 12 months." + ]); + } + + public function extendTrial(Request $request) + { + $request->validate([ + 'user_id' => 'required', + 'number_of_day' => 'required|numeric|max:14' + ]); + $user = User::find($request->get("user_id")); + + $subscription = $user->subscriptions() + ->where('stripe_status', 'trialing') + ->firstOrFail(); + + $trialEndDate = now()->addDays($request->get('number_of_day')); + $subscription->extendTrial($trialEndDate); + + \Log::warning(self::ADMIN_LOG_PREFIX . 'Trial extended', [ + 'user_id' => $user->id, + 'subcription_id' => $subscription->id, + 'nb_days' => $request->get('number_of_day'), + 'subscription_stripe_id' => $subscription->stripe_id, + 'moderator_id' => auth()->id(), + ]); + + return $this->success([ + "message" => "Subscription trial extend until the " . $trialEndDate->format('d/m/Y') + ]); + } + + public function cancelSubscription(Request $request) + { + $request->validate([ + 'user_id' => 'required', + 'cancellation_reason' => 'required' + ]); + $user = User::find($request->get("user_id")); + + $activeSubscriptions = $user->subscriptions()->where(function ($q) { + $q->where('stripe_status', 'trialing') + ->orWhere('stripe_status', 'active'); + })->get(); + + if ($activeSubscriptions->count() != 1) { + return $this->error([ + "message" => "The user has more than one active subscriptions or doesn't have one." + ]); + } + + $subscription = $activeSubscriptions->first(); + $subscription->cancel(); + + \Log::warning(self::ADMIN_LOG_PREFIX . 'Cancel Subscription', [ + 'user_id' => $user->id, + 'cancel_reason' => $request->get('cancellation_reason'), + 'moderator_id' => auth()->id(), + 'subcription_id' => $subscription->id, + 'subscription_stripe_id' => $subscription->stripe_id + ]); + + return $this->success([ + "message" => "The subscription cancellation has been successfully completed." + ]); + } +} diff --git a/app/Http/Controllers/Admin/ImpersonationController.php b/app/Http/Controllers/Admin/ImpersonationController.php index 356ba292..8155ecdc 100644 --- a/app/Http/Controllers/Admin/ImpersonationController.php +++ b/app/Http/Controllers/Admin/ImpersonationController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use App\Models\Forms\Form; use App\Models\User; class ImpersonationController extends Controller @@ -13,22 +12,10 @@ class ImpersonationController extends Controller $this->middleware('moderator'); } - public function impersonate($identifier) + public function impersonate($userId) { - $user = null; - if (is_numeric($identifier)) { - $user = User::find($identifier); - } elseif (filter_var($identifier, FILTER_VALIDATE_EMAIL)) { - $user = User::whereEmail($identifier)->first(); - } else { - // Find by form slug - $form = Form::whereSlug($identifier)->first(); - if ($form) { - $user = $form->creator; - } - } - - if (! $user) { + $user = User::find($userId); + if (!$user) { return $this->error([ 'message' => 'User not found.', ]); @@ -38,7 +25,7 @@ class ImpersonationController extends Controller ]); } - \Log::warning('Impersonation started', [ + \Log::warning(AdminController::ADMIN_LOG_PREFIX . 'Impersonation started', [ 'from_id' => auth()->id(), 'from_email' => auth()->user()->email, 'target_id' => $user->id, diff --git a/client/components/global/VButton.vue b/client/components/global/VButton.vue index b3edf7a1..ea56dca7 100644 --- a/client/components/global/VButton.vue +++ b/client/components/global/VButton.vue @@ -146,6 +146,14 @@ export default { "ring-offset": "focus:ring-offset-gray-200", text: "text-gray-500 hover:text-white", } + } else if (this.color === "outline-red") { + return { + main: "bg-white border border-gray-300 text-red-700", + hover: "hover:bg-gray-100 hover:text-red-500", + ring: "focus:ring-gray-500", + "ring-offset": "focus:ring-offset-gray-200", + text: "text-gray-500", + } } else if (this.color === "red") { return { main: "bg-red-600", diff --git a/client/components/pages/admin/AdminCard.vue b/client/components/pages/admin/AdminCard.vue new file mode 100644 index 00000000..2fda4615 --- /dev/null +++ b/client/components/pages/admin/AdminCard.vue @@ -0,0 +1,23 @@ + + + diff --git a/client/components/pages/admin/CancelSubscription.vue b/client/components/pages/admin/CancelSubscription.vue new file mode 100644 index 00000000..d708d469 --- /dev/null +++ b/client/components/pages/admin/CancelSubscription.vue @@ -0,0 +1,71 @@ + + + diff --git a/client/components/pages/admin/DiscountOnSubscription.vue b/client/components/pages/admin/DiscountOnSubscription.vue new file mode 100644 index 00000000..0299bb31 --- /dev/null +++ b/client/components/pages/admin/DiscountOnSubscription.vue @@ -0,0 +1,52 @@ + + + diff --git a/client/components/pages/admin/ExtendTrial.vue b/client/components/pages/admin/ExtendTrial.vue new file mode 100644 index 00000000..f86c2ea2 --- /dev/null +++ b/client/components/pages/admin/ExtendTrial.vue @@ -0,0 +1,63 @@ + + + diff --git a/client/components/pages/admin/ImpersonateUser.vue b/client/components/pages/admin/ImpersonateUser.vue new file mode 100644 index 00000000..f1c6d158 --- /dev/null +++ b/client/components/pages/admin/ImpersonateUser.vue @@ -0,0 +1,53 @@ + + + diff --git a/client/pages/settings/admin.vue b/client/pages/settings/admin.vue index 4e584188..eafe37e7 100644 --- a/client/pages/settings/admin.vue +++ b/client/pages/settings/admin.vue @@ -1,108 +1,154 @@ + +
+
+ +
+
+ + + +
+
- + \ No newline at end of file diff --git a/client/stores/auth.js b/client/stores/auth.js index 099db9cf..8e2e67cd 100644 --- a/client/stores/auth.js +++ b/client/stores/auth.js @@ -21,7 +21,7 @@ export const useAuthStore = defineStore("auth", { // Stop admin impersonation stopImpersonating() { this.setToken(this.admin_token) - this.admin_token = null + this.setAdminToken(null) }, setToken(token) { diff --git a/config/pricing.php b/config/pricing.php index 965ce1f3..5465bca1 100644 --- a/config/pricing.php +++ b/config/pricing.php @@ -22,4 +22,5 @@ return [ ], ], + 'discount_coupon_id' => env('STRIPE_DISCOUNT_COUPON_ID', null), ]; diff --git a/routes/api.php b/routes/api.php index b7f84cab..8f55b10d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -160,11 +160,27 @@ Route::group(['middleware' => 'auth:api'], function () { }); }); - Route::group(['middleware' => 'moderator', 'prefix' => 'admin'], function () { + Route::group(['middleware' => 'moderator', 'prefix' => 'moderator'], function () { Route::get( - 'impersonate/{identifier}', + 'fetch-user/{identifier}', + [\App\Http\Controllers\Admin\AdminController::class, 'fetchUser'] + ); + Route::get( + 'impersonate/{userId}', [\App\Http\Controllers\Admin\ImpersonationController::class, 'impersonate'] ); + Route::patch( + 'apply-discount', + [\App\Http\Controllers\Admin\AdminController::class, 'applyDiscount'] + ); + Route::patch( + 'extend-trial', + [\App\Http\Controllers\Admin\AdminController::class, 'extendTrial'] + ); + Route::patch( + 'cancellation-subscription', + [\App\Http\Controllers\Admin\AdminController::class, 'cancelSubscription'] + ); }); });