From 80cdce95029d5cedc2aada79a5c331dc19bbc6db Mon Sep 17 00:00:00 2001 From: Favour Olayinka Date: Mon, 6 May 2024 13:12:05 +0100 Subject: [PATCH] A227b new admin features (#388) * wip: adminfeatures * wip: admin features * wip: admin features, password reset, deleted forms * fix linting * fix pinting * fix bug * fix bug * remove testing * fixes on deleted forms, removed unused functions * fix pint * admin feature updated * fix linting warning * fix workspace subscription tag * Final touches * Clean console.log * Added admin logs * Fix linting --------- Co-authored-by: Julien Nahum --- .../Controllers/Admin/AdminController.php | 59 ++++++++- .../Controllers/Admin/BillingController.php | 103 ++++++++++++++++ app/Http/Controllers/Admin/FormController.php | 38 ++++++ .../Admin/ImpersonationController.php | 12 +- client/components/global/Collapse.vue | 4 +- client/components/pages/admin/AdminCard.vue | 33 +++-- .../components/pages/admin/BillingEmail.vue | 84 +++++++++++++ .../pages/admin/CancelSubscription.vue | 2 + .../components/pages/admin/DeletedForms.vue | 113 ++++++++++++++++++ .../pages/admin/DiscountOnSubscription.vue | 2 + client/components/pages/admin/ExtendTrial.vue | 2 + .../pages/admin/SendPasswordResetEmail.vue | 40 +++++++ .../components/pages/admin/UserPayments.vue | 106 ++++++++++++++++ .../pages/admin/UserSubscriptions.vue | 107 +++++++++++++++++ .../components/pages/admin/UserWorkspaces.vue | 79 ++++++++++++ .../components/pages/welcome/Testimonials.vue | 1 - client/pages/settings/admin.vue | 63 ++++++++-- routes/api.php | 17 +++ 18 files changed, 831 insertions(+), 34 deletions(-) create mode 100644 app/Http/Controllers/Admin/BillingController.php create mode 100644 app/Http/Controllers/Admin/FormController.php create mode 100644 client/components/pages/admin/BillingEmail.vue create mode 100644 client/components/pages/admin/DeletedForms.vue create mode 100644 client/components/pages/admin/SendPasswordResetEmail.vue create mode 100644 client/components/pages/admin/UserPayments.vue create mode 100644 client/components/pages/admin/UserSubscriptions.vue create mode 100644 client/components/pages/admin/UserWorkspaces.vue diff --git a/app/Http/Controllers/Admin/AdminController.php b/app/Http/Controllers/Admin/AdminController.php index 67eac5e9..657e8461 100644 --- a/app/Http/Controllers/Admin/AdminController.php +++ b/app/Http/Controllers/Admin/AdminController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Models\Forms\Form; use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Password; use Laravel\Cashier\Cashier; class AdminController extends Controller @@ -41,9 +42,30 @@ class AdminController extends Controller 'message' => 'You cannot fetch an admin.' ]); } - + $workspaces = $user->workspaces() + ->withCount('forms') + ->get() + ->map(function ($workspace) { + $plan = 'free'; + if ($workspace->is_trialing) { + $plan = 'trialing'; + } + if ($workspace->is_pro) { + $plan = 'pro'; + } + if ($workspace->is_enterprise) { + $plan = 'enterprise'; + } + return [ + 'id' => $workspace->id, + 'name' => $workspace->name, + 'plan' => $plan, + 'forms_count' => $workspace->forms_count + ]; + }); return $this->success([ - 'user' => $user + 'user' => $user, + 'workspaces' => $workspaces ]); } @@ -77,7 +99,7 @@ class AdminController extends Controller 'coupon' => $couponId ]); - \Log::warning(self::ADMIN_LOG_PREFIX . 'Applying NGO/Student discount to sub', [ + self::log('Applying NGO/Student discount to sub', [ 'user_id' => $user->id, 'subcription_id' => $subscription->id, 'coupon_id' => $couponId, @@ -105,7 +127,7 @@ class AdminController extends Controller $trialEndDate = now()->addDays($request->get('number_of_day')); $subscription->extendTrial($trialEndDate); - \Log::warning(self::ADMIN_LOG_PREFIX . 'Trial extended', [ + self::log('Trial extended', [ 'user_id' => $user->id, 'subcription_id' => $subscription->id, 'nb_days' => $request->get('number_of_day'), @@ -140,7 +162,7 @@ class AdminController extends Controller $subscription = $activeSubscriptions->first(); $subscription->cancel(); - \Log::warning(self::ADMIN_LOG_PREFIX . 'Cancel Subscription', [ + self::log('Cancel Subscription', [ 'user_id' => $user->id, 'cancel_reason' => $request->get('cancellation_reason'), 'moderator_id' => auth()->id(), @@ -152,4 +174,31 @@ class AdminController extends Controller "message" => "The subscription cancellation has been successfully completed." ]); } + + public function sendPasswordResetEmail(Request $request) + { + $user = User::findOrFail($request->user_id); + + $status = Password::sendResetLink(['email' => $user->email]); + + if ($status !== Password::RESET_LINK_SENT) { + return $this->error([ + 'message' => "Password reset email failed to send" + ]); + } + + self::log('Sent password reset email', [ + 'user_id' => $user->id, + 'moderator_id' => auth()->id(), + ]); + + return $this->success([ + 'message' => "Password reset email has been sent to the user's email address" + ]); + } + + public static function log($message, $data = []) + { + \Log::warning(self::ADMIN_LOG_PREFIX . $message, $data); + } } diff --git a/app/Http/Controllers/Admin/BillingController.php b/app/Http/Controllers/Admin/BillingController.php new file mode 100644 index 00000000..d716c08d --- /dev/null +++ b/app/Http/Controllers/Admin/BillingController.php @@ -0,0 +1,103 @@ +middleware('moderator'); + } + + public function getEmail($userId) + { + $user = User::find($userId); + + if (!$user->hasStripeId()) { + return $this->error([ + "message" => "Stripe user not created", + ]); + } + + $user = $user->asStripeCustomer(); + + return $this->success([ + 'billing_email' => $user->email + ]); + } + + public function updateEmail(Request $request) + { + $request->validate([ + 'user_id' => 'required', + 'billing_email' => 'required|email' + ]); + + $user = User::findOrFail($request->get("user_id")); + + if (!$user->hasStripeId()) { + return $this->error([ + "message" => "Stripe user not created", + ]); + } + AdminController::log('Update billing email', [ + 'user_id' => $user->id, + 'stripe_id' => $user->stripe_id, + 'moderator_id' => auth()->id() + ]); + $user->updateStripeCustomer(['email' => $request->billing_email]); + + return $this->success(['message' => 'Billing email updated successfully']); + } + + public function getSubscriptions($userId) + { + $user = User::find($userId); + if (!$user->hasStripeId()) { + return $this->error([ + "message" => "Stripe user not created", + ]); + } + $subscriptions = $user->subscriptions()->latest()->take(100)->get()->map(function ($subscription) use ($user) { + return [ + "id" => $subscription->id, + "stripe_id" => $subscription->stripe_id, + "name" => ucfirst($user->name), + "plan" => $subscription->name, + "status" => $subscription->stripe_status, + "creation_date" => $subscription->created_at->format('Y-m-d') + ]; + }); + return $this->success([ + 'subscriptions' => $subscriptions, + ]); + } + + public function getPayments($userId) + { + $user = User::find($userId); + if (!$user->hasStripeId()) { + return $this->error([ + "message" => "Stripe user not created", + ]); + } + $payments = $user->invoices(); + $payments = $payments->map(function ($payment) use ($user) { + return [ + "id" => $payment->id, + "amount_paid" => ($payment->amount_paid), + "name" => ucfirst($payment->account_name), + "creation_date" => Carbon::parse($payment->created)->format("Y-m-d H:i:s"), + "status" => $payment->status, + ]; + }); + return $this->success([ + 'payments' => $payments, + ]); + } +} diff --git a/app/Http/Controllers/Admin/FormController.php b/app/Http/Controllers/Admin/FormController.php new file mode 100644 index 00000000..316167f4 --- /dev/null +++ b/app/Http/Controllers/Admin/FormController.php @@ -0,0 +1,38 @@ +forms()->with('creator')->onlyTrashed()->get()->map(function ($form) { + return [ + "id" => $form->id, + "slug" => $form->slug, + "title" => $form->title, + "created_by" => $form->creator->email, + "deleted_at" => $form->deleted_at->format('Y-m-d'), + ]; + }); + return $this->success(['forms' => $deletedForms]); + } + + public function restoreDeletedForm(string $slug) + { + $form = Form::onlyTrashed()->whereSlug($slug)->firstOrFail(); + $form->restore(); + + AdminController::log('Restore deleted form', [ + 'form_id' => $form->id, + 'moderator_id' => auth()->id() + ]); + + return $this->success(['message' => 'Form restored successfully']); + } +} diff --git a/app/Http/Controllers/Admin/ImpersonationController.php b/app/Http/Controllers/Admin/ImpersonationController.php index 8155ecdc..ac9c0dea 100644 --- a/app/Http/Controllers/Admin/ImpersonationController.php +++ b/app/Http/Controllers/Admin/ImpersonationController.php @@ -25,17 +25,19 @@ class ImpersonationController extends Controller ]); } - \Log::warning(AdminController::ADMIN_LOG_PREFIX . 'Impersonation started', [ + AdminController::log('Impersonation started', [ 'from_id' => auth()->id(), 'from_email' => auth()->user()->email, 'target_id' => $user->id, 'target_email' => $user->id, ]); - $token = auth()->claims(auth()->user()->admin ? [] : [ - 'impersonating' => true, - 'impersonator_id' => auth()->id(), - ])->login($user); + $token = auth()->claims( + auth()->user()->admin ? [] : [ + 'impersonating' => true, + 'impersonator_id' => auth()->id(), + ] + )->login($user); return $this->success([ 'token' => $token, diff --git a/client/components/global/Collapse.vue b/client/components/global/Collapse.vue index e403cb42..e520c696 100644 --- a/client/components/global/Collapse.vue +++ b/client/components/global/Collapse.vue @@ -1,6 +1,6 @@ @@ -20,4 +27,6 @@ const props = defineProps({ title: { type: String, required: true }, icon: { type: String, required: true } }) + +const show = ref(true) diff --git a/client/components/pages/admin/BillingEmail.vue b/client/components/pages/admin/BillingEmail.vue new file mode 100644 index 00000000..11216ca2 --- /dev/null +++ b/client/components/pages/admin/BillingEmail.vue @@ -0,0 +1,84 @@ + + + diff --git a/client/components/pages/admin/CancelSubscription.vue b/client/components/pages/admin/CancelSubscription.vue index d708d469..6773569b 100644 --- a/client/components/pages/admin/CancelSubscription.vue +++ b/client/components/pages/admin/CancelSubscription.vue @@ -1,5 +1,6 @@