diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fabcd1d0..35063bbf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Fix GroupController, move groups enabled check to each method to fix route:list ([f260572e](https://github.com/pixelfed/pixelfed/commit/f260572e)) - Update MediaStorageService, handle local media deletes after successful S3 upload ([280f63dc](https://github.com/pixelfed/pixelfed/commit/280f63dc)) - Update status twitter:card to summary_large_image for images/albums ([9a5a9f55](https://github.com/pixelfed/pixelfed/commit/9a5a9f55)) +- Update CuratedOnboarding, add new app:curated-onboarding command, extend email verification window to 7 days and fix resend verification mails ([49604210](https://github.com/pixelfed/pixelfed/commit/49604210)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev) diff --git a/app/Console/Commands/CuratedOnboardingCommand.php b/app/Console/Commands/CuratedOnboardingCommand.php new file mode 100644 index 000000000..d8a3d6c00 --- /dev/null +++ b/app/Console/Commands/CuratedOnboardingCommand.php @@ -0,0 +1,170 @@ +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; + } + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index ad96a2ad7..716e8e235 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/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) { diff --git a/app/Http/Controllers/CuratedRegisterController.php b/app/Http/Controllers/CuratedRegisterController.php index 58bddb498..59eb075d3 100644 --- a/app/Http/Controllers/CuratedRegisterController.php +++ b/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 contact the admin team.']); } @@ -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; diff --git a/resources/views/emails/curated-register/confirm_email.blade.php b/resources/views/emails/curated-register/confirm_email.blade.php index bcadc3832..0cd739102 100644 --- a/resources/views/emails/curated-register/confirm_email.blade.php +++ b/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 -

If you did not create this account, please disregard this email. This link expires after 24 hours.

+

If you did not create this account, please disregard this email. This link expires in 7 days.


Thanks,