Browse Source

Update CuratedOnboarding, add new app:curated-onboarding command, extend email verification window to 7 days and fix resend verification mails

Daniel Supernault 4 months ago
parent
commit
4960421073

+ 170 - 0
app/Console/Commands/CuratedOnboardingCommand.php

@@ -0,0 +1,170 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Mail\CuratedRegisterConfirmEmail;
+use App\Models\CuratedRegister;
+use App\Models\CuratedRegisterActivity;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Support\Str;
+
+use function Laravel\Prompts\confirm;
+use function Laravel\Prompts\search;
+use function Laravel\Prompts\select;
+use function Laravel\Prompts\table;
+
+class CuratedOnboardingCommand extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'app:curated-onboarding';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Manage curated onboarding applications';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $this->line(' ');
+        $this->info('   Welcome to the Curated Onboarding manager');
+        $this->line(' ');
+
+        $action = select(
+            label: 'Select an action:',
+            options: ['Stats', 'Edit'],
+            default: 'Stats',
+            hint: 'You can manage this via the admin dashboard.'
+        );
+
+        switch ($action) {
+            case 'Stats':
+                return $this->stats();
+                break;
+
+            case 'Edit':
+                return $this->edit();
+                break;
+
+            default:
+                exit;
+                break;
+        }
+    }
+
+    protected function stats()
+    {
+        $total = CuratedRegister::count();
+        $approved = CuratedRegister::whereIsApproved(true)->whereIsRejected(false)->whereNotNull('email_verified_at')->count();
+        $awaitingMoreInfo = CuratedRegister::whereIsAwaitingMoreInfo(true)->whereIsRejected(false)->whereIsClosed(false)->whereNotNull('email_verified_at')->count();
+        $open = CuratedRegister::whereIsApproved(false)->whereIsRejected(false)->whereIsClosed(false)->whereNotNull('email_verified_at')->whereIsAwaitingMoreInfo(false)->count();
+        $nonVerified = CuratedRegister::whereIsApproved(false)->whereIsRejected(false)->whereIsClosed(false)->whereNull('email_verified_at')->whereIsAwaitingMoreInfo(false)->count();
+        table(
+            ['Total', 'Approved', 'Open', 'Awaiting More Info', 'Unverified Emails'],
+            [
+                [$total, $approved, $open, $awaitingMoreInfo, $nonVerified],
+            ]
+        );
+    }
+
+    protected function edit()
+    {
+        $id = search(
+            label: 'Search for a username or email',
+            options: fn (string $value) => strlen($value) > 0
+                ? CuratedRegister::where(function ($query) use ($value) {
+                    $query->whereLike('username', "%{$value}%")
+                        ->orWhereLike('email', "%{$value}%");
+                })->get()
+                    ->mapWithKeys(fn ($user) => [
+                      $user->id => "{$user->username} ({$user->email})",
+                  ])
+                    ->all()
+                : []
+        );
+
+        $register = CuratedRegister::findOrFail($id);
+        if ($register->is_approved) {
+            $status = 'Approved';
+        } elseif ($register->is_rejected) {
+            $status = 'Rejected';
+        } elseif ($register->is_closed) {
+            $status = 'Closed';
+        } elseif ($register->is_awaiting_more_info) {
+            $status = 'Awaiting more info';
+        } elseif ($register->user_has_responded) {
+            $status = 'Awaiting Admin Response';
+        } else {
+            $status = 'Unknown';
+        }
+        table(
+            ['Field', 'Value'],
+            [
+                ['ID', $register->id],
+                ['Username', $register->username],
+                ['Email', $register->email],
+                ['Status', $status],
+                ['Created At', $register->created_at->format('Y-m-d H:i')],
+                ['Updated At', $register->updated_at->format('Y-m-d H:i')],
+            ]
+        );
+        if (in_array($status, ['Approved', 'Rejected', 'Closed'])) {
+            return;
+        }
+
+        $options = ['Cancel', 'Delete'];
+
+        if ($register->email_verified_at == null) {
+            $options[] = 'Resend Email Verification';
+        }
+
+        $action = select(
+            label: 'Select an action:',
+            options: $options,
+            default: 'Cancel',
+        );
+
+        if ($action === 'Resend Email Verification') {
+            $confirmed = confirm('Are you sure you want to send another email to '.$register->email.' ?');
+
+            if (! $confirmed) {
+                $this->error('Aborting...');
+                exit;
+            }
+
+            DB::transaction(function () use ($register) {
+                $register->verify_code = Str::random(40);
+                $register->created_at = now();
+                $register->save();
+                Mail::to($register->email)->send(new CuratedRegisterConfirmEmail($register));
+                $this->info('Mail sent!');
+            });
+        } elseif ($action === 'Delete') {
+            $confirmed = confirm('Are you sure you want to delete the application from '.$register->email.' ?');
+
+            if (! $confirmed) {
+                $this->error('Aborting...');
+                exit;
+            }
+
+            DB::transaction(function () use ($register) {
+                CuratedRegisterActivity::whereRegisterId($register->id)->delete();
+                $register->delete();
+                $this->info('Successfully deleted!');
+            });
+        } else {
+            $this->info('Cancelled.');
+            exit;
+        }
+    }
+}

+ 1 - 1
app/Http/Controllers/Auth/RegisterController.php

@@ -111,7 +111,7 @@ class RegisterController extends Controller
         $emailRules = [
             'required',
             'string',
-            'email',
+            'email:rfc,dns,spoof',
             'max:255',
             'unique:users',
             function ($attribute, $value, $fail) {

+ 102 - 60
app/Http/Controllers/CuratedRegisterController.php

@@ -2,27 +2,28 @@
 
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
-use Illuminate\Support\Str;
-use App\User;
+use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
+use App\Mail\CuratedRegisterConfirmEmail;
 use App\Models\CuratedRegister;
 use App\Models\CuratedRegisterActivity;
 use App\Services\EmailService;
-use App\Services\BouncerService;
 use App\Util\Lexer\RestrictedNames;
-use App\Mail\CuratedRegisterConfirmEmail;
-use App\Mail\CuratedRegisterNotifyAdmin;
+use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Mail;
-use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
+use Illuminate\Support\Str;
 
 class CuratedRegisterController extends Controller
 {
-    public function __construct()
+    public function preCheck($allowWhenDisabled = false)
     {
-        abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
-
-        if((bool) config_cache('pixelfed.open_registration')) {
-            abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
+        if (! $allowWhenDisabled) {
+            abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
+
+            if ((bool) config_cache('pixelfed.open_registration')) {
+                abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
+            } else {
+                abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
+            }
         } else {
             abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
         }
@@ -31,26 +32,32 @@ class CuratedRegisterController extends Controller
     public function index(Request $request)
     {
         abort_if($request->user(), 404);
+
         return view('auth.curated-register.index', ['step' => 1]);
     }
 
     public function concierge(Request $request)
     {
         abort_if($request->user(), 404);
+        $this->preCheck(true);
         $emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
             $request->has('next') &&
             $request->session()->has('cur-reg-con.cr-id');
+
         return view('auth.curated-register.concierge', compact('emailConfirmed'));
     }
 
     public function conciergeResponseSent(Request $request)
     {
+        $this->preCheck(true);
+
         return view('auth.curated-register.user_response_sent');
     }
 
     public function conciergeFormShow(Request $request)
     {
         abort_if($request->user(), 404);
+        $this->preCheck(true);
         abort_unless(
             $request->session()->has('cur-reg-con.email-confirmed') &&
             $request->session()->has('cur-reg-con.cr-id') &&
@@ -58,18 +65,20 @@ class CuratedRegisterController extends Controller
         $crid = $request->session()->get('cur-reg-con.cr-id');
         $arid = $request->session()->get('cur-reg-con.ac-id');
         $showCaptcha = config('instance.curated_registration.captcha_enabled');
-        if($attempts = $request->session()->get('cur-reg-con-attempt')) {
+        if ($attempts = $request->session()->get('cur-reg-con-attempt')) {
             $showCaptcha = $attempts && $attempts >= 2;
         } else {
             $showCaptcha = false;
         }
         $activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
+
         return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
     }
 
     public function conciergeFormStore(Request $request)
     {
         abort_if($request->user(), 404);
+        $this->preCheck(true);
         $request->session()->increment('cur-reg-con-attempt');
         abort_unless(
             $request->session()->has('cur-reg-con.email-confirmed') &&
@@ -80,9 +89,9 @@ class CuratedRegisterController extends Controller
         $rules = [
             'response' => 'required|string|min:5|max:1000',
             'crid' => 'required|integer|min:1',
-            'acid' => 'required|integer|min:1'
+            'acid' => 'required|integer|min:1',
         ];
-        if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
+        if (config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
             $rules['h-captcha-response'] = 'required|captcha';
             $messages['h-captcha-response.required'] = 'The captcha must be filled';
         }
@@ -92,7 +101,7 @@ class CuratedRegisterController extends Controller
         abort_if((string) $crid !== $request->input('crid'), 404);
         abort_if((string) $acid !== $request->input('acid'), 404);
 
-        if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
+        if (CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
             return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
         }
 
@@ -115,6 +124,7 @@ class CuratedRegisterController extends Controller
     public function conciergeStore(Request $request)
     {
         abort_if($request->user(), 404);
+        $this->preCheck(true);
         $rules = [
             'sid' => 'required_if:action,email|integer|min:1|max:20000000',
             'id' => 'required_if:action,email|integer|min:1|max:20000000',
@@ -124,7 +134,7 @@ class CuratedRegisterController extends Controller
             'response' => 'required_if:action,message|string|min:20|max:1000',
         ];
         $messages = [];
-        if(config('instance.curated_registration.captcha_enabled')) {
+        if (config('instance.curated_registration.captcha_enabled')) {
             $rules['h-captcha-response'] = 'required|captcha';
             $messages['h-captcha-response.required'] = 'The captcha must be filled';
         }
@@ -139,11 +149,11 @@ class CuratedRegisterController extends Controller
         $cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
         $ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
 
-        if(!hash_equals($ac->secret_code, $code)) {
+        if (! hash_equals($ac->secret_code, $code)) {
             return redirect()->back()->withErrors(['code' => 'Invalid code']);
         }
 
-        if(!hash_equals($cr->email, $email)) {
+        if (! hash_equals($cr->email, $email)) {
             return redirect()->back()->withErrors(['email' => 'Invalid email']);
         }
 
@@ -151,44 +161,58 @@ class CuratedRegisterController extends Controller
         $request->session()->put('cur-reg-con.cr-id', $cr->id);
         $request->session()->put('cur-reg-con.ac-id', $ac->id);
         $emailConfirmed = true;
+
         return redirect('/auth/sign_up/concierge/form');
     }
 
     public function confirmEmail(Request $request)
     {
-        if($request->user()) {
+        if ($request->user()) {
             return redirect(route('help.email-confirmation-issues'));
         }
+        $this->preCheck(true);
+
         return view('auth.curated-register.confirm_email');
     }
 
     public function emailConfirmed(Request $request)
     {
-        if($request->user()) {
+        if ($request->user()) {
             return redirect(route('help.email-confirmation-issues'));
         }
+        $this->preCheck(true);
+
         return view('auth.curated-register.email_confirmed');
     }
 
     public function resendConfirmation(Request $request)
     {
+        if ($request->user()) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        $this->preCheck(true);
+
         return view('auth.curated-register.resend-confirmation');
     }
 
     public function resendConfirmationProcess(Request $request)
     {
+        if ($request->user()) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        $this->preCheck(true);
         $rules = [
             'email' => [
                 'required',
                 'string',
                 app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
                 'exists:curated_registers',
-            ]
+            ],
         ];
 
         $messages = [];
 
-        if(config('instance.curated_registration.captcha_enabled')) {
+        if (config('instance.curated_registration.captcha_enabled')) {
             $rules['h-captcha-response'] = 'required|captcha';
             $messages['h-captcha-response.required'] = 'The captcha must be filled';
         }
@@ -196,7 +220,7 @@ class CuratedRegisterController extends Controller
         $this->validate($request, $rules, $messages);
 
         $cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
-        if(!$cur) {
+        if (! $cur) {
             return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
         }
 
@@ -204,7 +228,7 @@ class CuratedRegisterController extends Controller
             ->whereType('user_resend_email_confirmation')
             ->count();
 
-        if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
+        if ($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
             return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
         }
 
@@ -213,75 +237,92 @@ class CuratedRegisterController extends Controller
             ->where('created_at', '>', now()->subHours(12))
             ->count();
 
-        if($count) {
+        if ($count) {
             return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
         }
 
-        CuratedRegisterActivity::create([
-            'register_id' => $cur->id,
-            'type' => 'user_resend_email_confirmation',
-            'admin_only_view' => true,
-            'from_admin' => false,
-            'from_user' => false,
-            'action_required' => false,
-        ]);
+        DB::transaction(function () use ($cur) {
+            $cur->verify_code = Str::random(40);
+            $cur->created_at = now();
+            $cur->save();
+
+            CuratedRegisterActivity::create([
+                'register_id' => $cur->id,
+                'type' => 'user_resend_email_confirmation',
+                'admin_only_view' => true,
+                'from_admin' => false,
+                'from_user' => false,
+                'action_required' => false,
+            ]);
+
+            Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
+        });
 
-        Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
         return view('auth.curated-register.resent-confirmation');
-        return $request->all();
     }
 
     public function confirmEmailHandle(Request $request)
     {
+        if ($request->user()) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        $this->preCheck(true);
         $rules = [
             'sid' => 'required',
-            'code' => 'required'
+            'code' => 'required',
         ];
         $messages = [];
-        if(config('instance.curated_registration.captcha_enabled')) {
+        if (config('instance.curated_registration.captcha_enabled')) {
             $rules['h-captcha-response'] = 'required|captcha';
             $messages['h-captcha-response.required'] = 'The captcha must be filled';
         }
         $this->validate($request, $rules, $messages);
 
         $cr = CuratedRegister::whereNull('email_verified_at')
-            ->where('created_at', '>', now()->subHours(24))
+            ->where('created_at', '>', now()->subDays(7))
             ->find($request->input('sid'));
-        if(!$cr) {
+        if (! $cr) {
             return redirect(route('help.email-confirmation-issues'));
         }
-        if(!hash_equals($cr->verify_code, $request->input('code'))) {
+        if (! hash_equals($cr->verify_code, $request->input('code'))) {
             return redirect(route('help.email-confirmation-issues'));
         }
         $cr->email_verified_at = now();
         $cr->save();
 
-        if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
+        if (config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
             CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
         }
+
         return view('auth.curated-register.email_confirmed');
     }
 
     public function proceed(Request $request)
     {
+        if ($request->user()) {
+            return redirect(route('help.email-confirmation-issues'));
+        }
+        $this->preCheck(false);
         $this->validate($request, [
-            'step' => 'required|integer|in:1,2,3,4'
+            'step' => 'required|integer|in:1,2,3,4',
         ]);
         $step = $request->input('step');
 
-        switch($step) {
+        switch ($step) {
             case 1:
                 $step = 2;
                 $request->session()->put('cur-step', 1);
+
                 return view('auth.curated-register.index', compact('step'));
-            break;
+                break;
 
             case 2:
                 $this->stepTwo($request);
                 $step = 3;
                 $request->session()->put('cur-step', 2);
+
                 return view('auth.curated-register.index', compact('step'));
-            break;
+                break;
 
             case 3:
                 $this->stepThree($request);
@@ -289,27 +330,28 @@ class CuratedRegisterController extends Controller
                 $request->session()->put('cur-step', 3);
                 $verifiedEmail = true;
                 $request->session()->pull('cur-reg');
+
                 return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
-            break;
+                break;
         }
     }
 
     protected function stepTwo($request)
     {
-        if($request->filled('reason')) {
+        if ($request->filled('reason')) {
             $request->session()->put('cur-reg.form-reason', $request->input('reason'));
         }
-        if($request->filled('username')) {
+        if ($request->filled('username')) {
             $request->session()->put('cur-reg.form-username', $request->input('username'));
         }
-        if($request->filled('email')) {
+        if ($request->filled('email')) {
             $request->session()->put('cur-reg.form-email', $request->input('email'));
         }
         $this->validate($request, [
             'username' => [
                 'required',
                 'min:2',
-                'max:15',
+                'max:30',
                 'unique:curated_registers',
                 'unique:users',
                 function ($attribute, $value, $fail) {
@@ -317,24 +359,24 @@ class CuratedRegisterController extends Controller
                     $underscore = substr_count($value, '_');
                     $period = substr_count($value, '.');
 
-                    if(ends_with($value, ['.php', '.js', '.css'])) {
+                    if (ends_with($value, ['.php', '.js', '.css'])) {
                         return $fail('Username is invalid.');
                     }
 
-                    if(($dash + $underscore + $period) > 1) {
+                    if (($dash + $underscore + $period) > 1) {
                         return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
                     }
 
-                    if (!ctype_alnum($value[0])) {
+                    if (! ctype_alnum($value[0])) {
                         return $fail('Username is invalid. Must start with a letter or number.');
                     }
 
-                    if (!ctype_alnum($value[strlen($value) - 1])) {
+                    if (! ctype_alnum($value[strlen($value) - 1])) {
                         return $fail('Username is invalid. Must end with a letter or number.');
                     }
 
                     $val = str_replace(['_', '.', '-'], '', $value);
-                    if(!ctype_alnum($val)) {
+                    if (! ctype_alnum($val)) {
                         return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
                     }
 
@@ -353,7 +395,7 @@ class CuratedRegisterController extends Controller
                 'unique:curated_registers',
                 function ($attribute, $value, $fail) {
                     $banned = EmailService::isBanned($value);
-                    if($banned) {
+                    if ($banned) {
                         return $fail('Email is invalid.');
                     }
                 },
@@ -361,7 +403,7 @@ class CuratedRegisterController extends Controller
             'password' => 'required|min:8',
             'password_confirmation' => 'required|same:password',
             'reason' => 'required|min:20|max:1000',
-            'agree' => 'required|accepted'
+            'agree' => 'required|accepted',
         ]);
         $request->session()->put('cur-reg.form-email', $request->input('email'));
         $request->session()->put('cur-reg.form-password', $request->input('password'));
@@ -379,11 +421,11 @@ class CuratedRegisterController extends Controller
                 'unique:curated_registers',
                 function ($attribute, $value, $fail) {
                     $banned = EmailService::isBanned($value);
-                    if($banned) {
+                    if ($banned) {
                         return $fail('Email is invalid.');
                     }
                 },
-            ]
+            ],
         ]);
         $cr = new CuratedRegister;
         $cr->email = $request->email;

+ 1 - 1
resources/views/emails/curated-register/confirm_email.blade.php

@@ -10,7 +10,7 @@ Please confirm your email address so we can process your new registration applic
 </x-mail::button>
 
 
-<p style="font-size:10pt;">If you did not create this account, please disregard this email. This link expires after 24 hours.</p>
+<p style="font-size:10pt;">If you did not create this account, please disregard this email. This link expires in 7 days.</p>
 <br>
 
 Thanks,<br>